Skip to content

Commit 79b3932

Browse files
committed
Load plugin config scopes from plugin registry
1 parent 5536149 commit 79b3932

File tree

3 files changed

+146
-18
lines changed

3 files changed

+146
-18
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ configurations {
3434
}
3535

3636
dependencies {
37-
implementation 'io.nextflow:nf-lang:25.07.0-edge'
37+
implementation 'io.nextflow:nf-lang:25.08.0-edge'
3838
implementation 'org.apache.groovy:groovy:4.0.28'
3939
implementation 'org.apache.groovy:groovy-json:4.0.28'
4040
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.0'
@@ -45,7 +45,7 @@ dependencies {
4545
runtimeOnly 'org.yaml:snakeyaml:2.2'
4646

4747
// include Nextflow runtime at build-time to extract language definitions
48-
nextflowRuntime 'io.nextflow:nextflow:25.07.0-edge'
48+
nextflowRuntime 'io.nextflow:nextflow:25.08.0-edge'
4949
nextflowRuntime 'io.nextflow:nf-amazon:3.1.0'
5050
nextflowRuntime 'io.nextflow:nf-azure:1.19.0'
5151
nextflowRuntime 'io.nextflow:nf-google:1.22.2'

src/main/java/nextflow/lsp/services/config/ConfigSchemaFactory.java

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@
2828
import org.codehaus.groovy.runtime.IOGroovyMethods;
2929

3030
/**
31-
* Load the config schema from the compiler as well as
32-
* the index file.
31+
* Load config scopes from various sources, including
32+
* built-in definitions and index files.
3333
*
3434
* @author Ben Sherman <[email protected]>
3535
*/
3636
public class ConfigSchemaFactory {
3737

38+
/**
39+
* Load config scopes from the compiler and index
40+
* file, which defines config scopes for core plugins.
41+
*/
3842
public static SchemaNode.Scope load() {
3943
var scope = SchemaNode.ROOT;
4044
scope.children().putAll(fromIndex());
@@ -55,6 +59,25 @@ private static Map<String,SchemaNode> fromIndex() {
5559
}
5660
}
5761

62+
/**
63+
* Load config scopes from a list of definitions (e.g. for
64+
* third-party plugins).
65+
*
66+
* @param definitions
67+
*/
68+
public static Map<String,SchemaNode> fromDefinitions(List<Map> definitions) {
69+
var entries = definitions.stream()
70+
.filter(node -> "ConfigScope".equals(node.get("type")))
71+
.map((node) -> {
72+
var spec = (Map) node.get("spec");
73+
var name = (String) spec.get("name");
74+
var scope = fromScope(spec);
75+
return Map.entry(name, scope);
76+
})
77+
.toArray(Map.Entry[]::new);
78+
return Map.ofEntries(entries);
79+
}
80+
5881
private static Map<String,SchemaNode> fromChildren(Map<String,?> children) {
5982
var entries = children.entrySet().stream()
6083
.map((entry) -> {
@@ -70,34 +93,34 @@ private static SchemaNode fromNode(Map<String,?> node) {
7093
var type = (String) node.get("type");
7194
var spec = (Map<String,?>) node.get("spec");
7295

73-
if( "Option".equals(type) )
96+
if( "ConfigOption".equals(type) )
7497
return fromOption(spec);
7598

76-
if( "Placeholder".equals(type) )
99+
if( "ConfigPlaceholderScope".equals(type) )
77100
return fromPlaceholder(spec);
78101

79-
if( "Scope".equals(type) )
102+
if( "ConfigScope".equals(type) )
80103
return fromScope(spec);
81104

82105
throw new IllegalStateException();
83106
}
84107

85-
private static SchemaNode.Option fromOption(Map<String,?> node) {
86-
var description = (String) node.get("description");
87-
var type = fromType(node.get("type"));
108+
private static SchemaNode.Option fromOption(Map<String,?> spec) {
109+
var description = (String) spec.get("description");
110+
var type = fromType(spec.get("type"));
88111
return new SchemaNode.Option(description, type);
89112
}
90113

91-
private static SchemaNode.Placeholder fromPlaceholder(Map<String,?> node) {
92-
var description = (String) node.get("description");
93-
var placeholderName = (String) node.get("placeholderName");
94-
var scope = fromScope((Map<String,?>) node.get("scope"));
114+
private static SchemaNode.Placeholder fromPlaceholder(Map<String,?> spec) {
115+
var description = (String) spec.get("description");
116+
var placeholderName = (String) spec.get("placeholderName");
117+
var scope = fromScope((Map<String,?>) spec.get("scope"));
95118
return new SchemaNode.Placeholder(description, placeholderName, scope);
96119
}
97120

98-
private static SchemaNode.Scope fromScope(Map<String,?> node) {
99-
var description = (String) node.get("description");
100-
var children = fromChildren((Map<String,?>) node.get("children"));
121+
private static SchemaNode.Scope fromScope(Map<String,?> spec) {
122+
var description = (String) spec.get("description");
123+
var children = fromChildren((Map<String,?>) spec.get("children"));
101124
return new SchemaNode.Scope(description, children);
102125
}
103126

src/main/java/nextflow/lsp/services/config/ConfigSchemaVisitor.java

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@
1515
*/
1616
package 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;
1822
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
1926
import java.util.Stack;
2027

28+
import groovy.json.JsonSlurper;
29+
import nextflow.config.ast.ConfigApplyBlockNode;
2130
import nextflow.config.ast.ConfigAssignNode;
2231
import nextflow.config.ast.ConfigBlockNode;
2332
import nextflow.config.ast.ConfigNode;
@@ -27,14 +36,23 @@
2736
import nextflow.script.control.Phases;
2837
import nextflow.script.types.TypesEx;
2938
import org.codehaus.groovy.ast.ASTNode;
39+
import org.codehaus.groovy.ast.expr.ConstantExpression;
3040
import org.codehaus.groovy.control.SourceUnit;
3141
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
3242
import org.codehaus.groovy.control.messages.WarningMessage;
3343
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
44+
import org.codehaus.groovy.runtime.StringGroovyMethods;
3445
import org.codehaus.groovy.syntax.SyntaxException;
3546
import 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

Comments
 (0)