15
15
*/
16
16
package nextflow .lsp .services .config ;
17
17
18
+ import java .io .IOException ;
19
+ import java .net .URI ;
20
+ import java .net .http .HttpClient ;
21
+ import java .net .http .HttpRequest ;
18
22
import java .util .ArrayList ;
23
+ import java .util .HashMap ;
24
+ import java .util .List ;
25
+ import java .util .Map ;
19
26
import java .util .Stack ;
20
27
28
+ import groovy .json .JsonSlurper ;
29
+ import nextflow .config .ast .ConfigApplyBlockNode ;
21
30
import nextflow .config .ast .ConfigAssignNode ;
22
31
import nextflow .config .ast .ConfigBlockNode ;
23
32
import nextflow .config .ast .ConfigNode ;
27
36
import nextflow .script .control .Phases ;
28
37
import nextflow .script .types .TypesEx ;
29
38
import org .codehaus .groovy .ast .ASTNode ;
39
+ import org .codehaus .groovy .ast .expr .ConstantExpression ;
30
40
import org .codehaus .groovy .control .SourceUnit ;
31
41
import org .codehaus .groovy .control .messages .SyntaxErrorMessage ;
32
42
import org .codehaus .groovy .control .messages .WarningMessage ;
33
43
import org .codehaus .groovy .runtime .DefaultGroovyMethods ;
44
+ import org .codehaus .groovy .runtime .StringGroovyMethods ;
34
45
import org .codehaus .groovy .syntax .SyntaxException ;
35
46
import org .codehaus .groovy .syntax .Token ;
36
47
48
+ import static java .net .http .HttpResponse .BodyHandlers ;
49
+ import static nextflow .script .ast .ASTUtils .*;
50
+
37
51
/**
52
+ * Validate config options against the config schema.
53
+ *
54
+ * Config scopes from third-party plugins are inferred
55
+ * from the `plugins` block, if specified.
38
56
*
39
57
* @author Ben Sherman <[email protected] >
40
58
*/
@@ -61,8 +79,95 @@ protected SourceUnit getSourceUnit() {
61
79
62
80
public void visit () {
63
81
var moduleNode = sourceUnit .getAST ();
64
- if ( moduleNode instanceof ConfigNode cn )
82
+ if ( moduleNode instanceof ConfigNode cn ) {
83
+ loadPluginScopes (cn );
65
84
super .visit (cn );
85
+ }
86
+ }
87
+
88
+ private void loadPluginScopes (ConfigNode cn ) {
89
+ try {
90
+ var defaultScopes = schema .children ();
91
+ var pluginScopes = pluginConfigScopes (cn );
92
+ var children = new HashMap <String , SchemaNode >();
93
+ children .putAll (defaultScopes );
94
+ children .putAll (pluginScopes );
95
+ this .schema = new SchemaNode .Scope (schema .description (), children );
96
+ }
97
+ catch ( Exception e ) {
98
+ System .err .println ("Failed to load plugin config scopes: " + e .toString ());
99
+ }
100
+ }
101
+
102
+ private static final String PLUGIN_REGITRY_URL = "http://localhost:8080/api/" ;
103
+
104
+ private Map <String , SchemaNode > pluginConfigScopes (ConfigNode cn ) {
105
+ var client = HttpClient .newBuilder ().build ();
106
+ var baseUri = URI .create (PLUGIN_REGITRY_URL );
107
+
108
+ var entries = cn .getConfigStatements ().stream ()
109
+
110
+ // extract plugin specs from `plugins` block
111
+ .map (stmt -> stmt instanceof ConfigApplyBlockNode node ? node : null )
112
+ .filter (node -> node != null && "plugins" .equals (node .name ))
113
+ .flatMap (node -> node .statements .stream ())
114
+ .map ((call ) -> {
115
+ var arguments = asMethodCallArguments (call );
116
+ var firstArg = arguments .get (0 );
117
+ return firstArg instanceof ConstantExpression ce ? ce .getText () : null ;
118
+ })
119
+
120
+ // fetch plugin definitions from plugin registry
121
+ .filter (spec -> spec != null )
122
+ .map ((spec ) -> {
123
+ var tokens = StringGroovyMethods .tokenize (spec , "@" );
124
+ var name = tokens .get (0 );
125
+ var version = tokens .size () == 2 ? tokens .get (1 ) : null ;
126
+ var path = version != null
127
+ ? "v1/plugins/" + name + "/" + version
128
+ : "v1/plugins/" + name ;
129
+ var request = HttpRequest .newBuilder ()
130
+ .uri (baseUri .resolve (path ))
131
+ .GET ()
132
+ .header ("Accept" , "application/json" )
133
+ .build ();
134
+ try {
135
+ var response = client .send (request , BodyHandlers .ofString ());
136
+ var json = new JsonSlurper ().parseText (response .body ());
137
+ return json instanceof Map m ? m : null ;
138
+ }
139
+ catch ( IOException | InterruptedException e ) {
140
+ return null ;
141
+ }
142
+ })
143
+
144
+ // select latest plugin version if not specified
145
+ .filter (json -> json != null )
146
+ .map ((json ) -> {
147
+ if ( json .containsKey ("plugin" ) ) {
148
+ var plugin = (Map ) json .get ("plugin" );
149
+ var releases = (List ) plugin .get ("releases" );
150
+ return (Map ) releases .get (0 );
151
+ }
152
+ if ( json .containsKey ("pluginRelease" ) ) {
153
+ return (Map ) json .get ("pluginRelease" );
154
+ }
155
+ return null ;
156
+ })
157
+
158
+ // load config scopes from JSON data
159
+ .filter (release -> release != null )
160
+ .map ((release ) -> {
161
+ var text = (String ) release .get ("definitions" );
162
+ var json = new JsonSlurper ().parseText (text );
163
+ return ConfigSchemaFactory .fromDefinitions ((List <Map >) json );
164
+ })
165
+ .toList ();
166
+
167
+ var result = new HashMap <String , SchemaNode >();
168
+ for ( var entry : entries )
169
+ result .putAll (entry );
170
+ return result ;
66
171
}
67
172
68
173
@ Override
0 commit comments