1515 */
1616package nextflow .lsp .services .config ;
1717
18+ import java .io .IOException ;
19+ import java .net .URI ;
20+ import java .net .http .HttpClient ;
21+ import java .net .http .HttpRequest ;
1822import java .util .ArrayList ;
23+ import java .util .HashMap ;
24+ import java .util .List ;
25+ import java .util .Map ;
1926import java .util .Stack ;
2027
28+ import groovy .json .JsonSlurper ;
29+ import nextflow .config .ast .ConfigApplyBlockNode ;
2130import nextflow .config .ast .ConfigAssignNode ;
2231import nextflow .config .ast .ConfigBlockNode ;
2332import nextflow .config .ast .ConfigNode ;
2736import nextflow .script .control .Phases ;
2837import nextflow .script .types .TypesEx ;
2938import org .codehaus .groovy .ast .ASTNode ;
39+ import org .codehaus .groovy .ast .expr .ConstantExpression ;
3040import org .codehaus .groovy .control .SourceUnit ;
3141import org .codehaus .groovy .control .messages .SyntaxErrorMessage ;
3242import org .codehaus .groovy .control .messages .WarningMessage ;
3343import org .codehaus .groovy .runtime .DefaultGroovyMethods ;
44+ import org .codehaus .groovy .runtime .StringGroovyMethods ;
3445import org .codehaus .groovy .syntax .SyntaxException ;
3546import org .codehaus .groovy .syntax .Token ;
3647
48+ import static java .net .http .HttpResponse .BodyHandlers ;
49+ import static nextflow .script .ast .ASTUtils .*;
50+
3751/**
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.
3856 *
3957 * @author Ben Sherman <[email protected] > 4058 */
@@ -61,8 +79,95 @@ protected SourceUnit getSourceUnit() {
6179
6280 public void visit () {
6381 var moduleNode = sourceUnit .getAST ();
64- if ( moduleNode instanceof ConfigNode cn )
82+ if ( moduleNode instanceof ConfigNode cn ) {
83+ loadPluginScopes (cn );
6584 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 ;
66171 }
67172
68173 @ Override
0 commit comments