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 ;
2433import nextflow .config .ast .ConfigVisitorSupport ;
2534import nextflow .config .schema .SchemaNode ;
35+ import nextflow .lsp .util .Logger ;
2636import nextflow .script .control .PhaseAware ;
2737import nextflow .script .control .Phases ;
2838import nextflow .script .types .TypesEx ;
2939import org .codehaus .groovy .ast .ASTNode ;
40+ import org .codehaus .groovy .ast .expr .ConstantExpression ;
3041import org .codehaus .groovy .control .SourceUnit ;
3142import org .codehaus .groovy .control .messages .SyntaxErrorMessage ;
3243import org .codehaus .groovy .control .messages .WarningMessage ;
3344import org .codehaus .groovy .runtime .DefaultGroovyMethods ;
45+ import org .codehaus .groovy .runtime .StringGroovyMethods ;
3446import org .codehaus .groovy .syntax .SyntaxException ;
3547import org .codehaus .groovy .syntax .Token ;
3648
49+ import static java .net .http .HttpResponse .BodyHandlers ;
50+ import static nextflow .script .ast .ASTUtils .*;
51+
3752/**
53+ * Validate config options against the config schema.
54+ *
55+ * Config scopes from third-party plugins are inferred
56+ * from the `plugins` block, if specified.
3857 *
3958 * @author Ben Sherman <[email protected] > 4059 */
4160public class ConfigSchemaVisitor extends ConfigVisitorSupport {
4261
62+ private static Logger log = Logger .getInstance ();
63+
4364 private SourceUnit sourceUnit ;
4465
4566 private SchemaNode .Scope schema ;
@@ -61,8 +82,100 @@ protected SourceUnit getSourceUnit() {
6182
6283 public void visit () {
6384 var moduleNode = sourceUnit .getAST ();
64- if ( moduleNode instanceof ConfigNode cn )
85+ if ( moduleNode instanceof ConfigNode cn ) {
86+ loadPluginScopes (cn );
6587 super .visit (cn );
88+ }
89+ }
90+
91+ private void loadPluginScopes (ConfigNode cn ) {
92+ try {
93+ var defaultScopes = schema .children ();
94+ var pluginScopes = pluginConfigScopes (cn );
95+ var children = new HashMap <String , SchemaNode >();
96+ children .putAll (defaultScopes );
97+ children .putAll (pluginScopes );
98+ this .schema = new SchemaNode .Scope (schema .description (), children );
99+ }
100+ catch ( Exception e ) {
101+ System .err .println ("Failed to load plugin config scopes: " + e .toString ());
102+ }
103+ }
104+
105+ private static final String PLUGIN_REGITRY_URL = "http://localhost:8080/api/" ;
106+
107+ private Map <String , SchemaNode > pluginConfigScopes (ConfigNode cn ) {
108+ var client = HttpClient .newBuilder ().build ();
109+ var baseUri = URI .create (PLUGIN_REGITRY_URL );
110+
111+ var entries = cn .getConfigStatements ().stream ()
112+
113+ // get plugin refs from `plugins` block
114+ .map (stmt ->
115+ stmt instanceof ConfigApplyBlockNode node && "plugins" .equals (node .name ) ? node : null
116+ )
117+ .filter (node -> node != null )
118+ .flatMap (node -> node .statements .stream ())
119+ .map ((call ) -> {
120+ var arguments = asMethodCallArguments (call );
121+ var firstArg = arguments .get (0 );
122+ return firstArg instanceof ConstantExpression ce ? ce .getText () : null ;
123+ })
124+
125+ // fetch plugin specs from plugin registry
126+ .filter (spec -> spec != null )
127+ .map ((spec ) -> {
128+ var tokens = StringGroovyMethods .tokenize (spec , "@" );
129+ var name = tokens .get (0 );
130+ var version = tokens .size () == 2 ? tokens .get (1 ) : null ;
131+ var path = version != null
132+ ? String .format ("v1/plugins/%s/%s" , name , version )
133+ : String .format ("v1/plugins/%s" , name );
134+ var uri = baseUri .resolve (path );
135+ log .debug ("fetch plugin " + uri );
136+ var request = HttpRequest .newBuilder ()
137+ .uri (uri )
138+ .GET ()
139+ .header ("Accept" , "application/json" )
140+ .build ();
141+ try {
142+ var response = client .send (request , BodyHandlers .ofString ());
143+ var json = new JsonSlurper ().parseText (response .body ());
144+ return json instanceof Map m ? m : null ;
145+ }
146+ catch ( IOException | InterruptedException e ) {
147+ return null ;
148+ }
149+ })
150+
151+ // select plugin release (or latest if not specified)
152+ .filter (json -> json != null )
153+ .map ((json ) -> {
154+ if ( json .containsKey ("plugin" ) ) {
155+ var plugin = (Map ) json .get ("plugin" );
156+ var releases = (List <Map >) plugin .get ("releases" );
157+ return releases .get (0 );
158+ }
159+ if ( json .containsKey ("pluginRelease" ) ) {
160+ return (Map ) json .get ("pluginRelease" );
161+ }
162+ return null ;
163+ })
164+
165+ // get spec from plugin release
166+ .filter (release -> release != null )
167+ .map ((release ) -> {
168+ var text = (String ) release .get ("spec" );
169+ var spec = (Map ) new JsonSlurper ().parseText (text );
170+ var definitions = (List <Map >) spec .get ("definitions" );
171+ return ConfigSchemaFactory .fromDefinitions (definitions );
172+ })
173+ .toList ();
174+
175+ var result = new HashMap <String , SchemaNode >();
176+ for ( var entry : entries )
177+ result .putAll (entry );
178+ return result ;
66179 }
67180
68181 @ Override
0 commit comments