Skip to content

Commit 68d6335

Browse files
committed
Load plugin config scopes from plugin registry
1 parent 5536149 commit 68d6335

File tree

5 files changed

+253
-94
lines changed

5 files changed

+253
-94
lines changed

build.gradle

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ test {
3030

3131
configurations {
3232
nextflowRuntime
33-
schemaImplementation.extendsFrom(nextflowRuntime)
33+
coreSpecImplementation.extendsFrom(nextflowRuntime)
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,37 +45,37 @@ 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'
49-
nextflowRuntime 'io.nextflow:nf-amazon:3.1.0'
50-
nextflowRuntime 'io.nextflow:nf-azure:1.19.0'
51-
nextflowRuntime 'io.nextflow:nf-google:1.22.2'
52-
nextflowRuntime 'io.nextflow:nf-k8s:1.1.1'
53-
nextflowRuntime 'io.nextflow:nf-tower:1.14.0'
54-
nextflowRuntime 'io.nextflow:nf-wave:1.14.1'
48+
nextflowRuntime 'io.nextflow:nextflow:25.08.0-edge'
49+
nextflowRuntime 'io.nextflow:nf-amazon:3.2.0'
50+
nextflowRuntime 'io.nextflow:nf-azure:1.20.0'
51+
nextflowRuntime 'io.nextflow:nf-google:1.23.0'
52+
nextflowRuntime 'io.nextflow:nf-k8s:1.2.0'
53+
nextflowRuntime 'io.nextflow:nf-tower:1.15.0'
54+
nextflowRuntime 'io.nextflow:nf-wave:1.15.0'
5555

5656
testImplementation ('org.objenesis:objenesis:3.4')
5757
testImplementation ('net.bytebuddy:byte-buddy:1.14.17')
5858
testImplementation ('org.spockframework:spock-core:2.3-groovy-4.0') { exclude group: 'org.apache.groovy' }
5959
}
6060

6161
sourceSets {
62-
schema {
63-
groovy.srcDir 'src/schema/groovy'
62+
coreSpec {
63+
groovy.srcDir 'src/spec/groovy'
6464
compileClasspath += configurations.nextflowRuntime
6565
runtimeClasspath += configurations.nextflowRuntime
6666
}
6767
}
6868

69-
task buildSchema(type: JavaExec) {
70-
description = 'Build JSON schema of Nextflow configuration'
69+
task buildCoreSpec(type: JavaExec) {
70+
description = 'Build spec of core definitions'
7171
group = 'build'
7272

73-
dependsOn compileSchemaGroovy
74-
classpath = sourceSets.schema.runtimeClasspath
75-
mainClass.set('nextflow.SchemaRenderer')
76-
args "$buildDir/generated/index.json"
73+
dependsOn compileCoreSpecGroovy
74+
classpath = sourceSets.coreSpec.runtimeClasspath
75+
mainClass.set('nextflow.SpecRenderer')
76+
args "$buildDir/generated/definitions.json"
7777

78-
outputs.file("$buildDir/generated/index.json")
78+
outputs.file("$buildDir/generated/definitions.json")
7979
outputs.cacheIf { true }
8080

8181
doFirst {
@@ -84,17 +84,17 @@ task buildSchema(type: JavaExec) {
8484
}
8585

8686
processResources {
87-
dependsOn buildSchema
88-
from(buildSchema.outputs.files) {
89-
into 'schema'
87+
dependsOn buildCoreSpec
88+
from(buildCoreSpec.outputs.files) {
89+
into 'spec'
9090
}
9191
}
9292

9393
jar {
94-
dependsOn buildSchema
94+
dependsOn buildCoreSpec
9595
from("$buildDir/generated") {
96-
include 'index.json'
97-
into 'schema'
96+
include 'definitions.json'
97+
into 'spec'
9898
}
9999
}
100100

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

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,38 +28,58 @@
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 core definitions and plugin specs.
3332
*
3433
* @author Ben Sherman <[email protected]>
3534
*/
3635
public class ConfigSchemaFactory {
3736

37+
/**
38+
* Load config scopes from core definitions.
39+
*/
3840
public static SchemaNode.Scope load() {
3941
var scope = SchemaNode.ROOT;
40-
scope.children().putAll(fromIndex());
42+
scope.children().putAll(fromCoreDefinitions());
4143
return scope;
4244
}
4345

44-
private static Map<String,SchemaNode> fromIndex() {
46+
private static Map<String,SchemaNode> fromCoreDefinitions() {
4547
try {
4648
var classLoader = ConfigSchemaFactory.class.getClassLoader();
47-
var resource = classLoader.getResourceAsStream("schema/index.json");
49+
var resource = classLoader.getResourceAsStream("spec/definitions.json");
4850
var text = IOGroovyMethods.getText(resource);
4951
var json = new JsonSlurper().parseText(text);
50-
return fromChildren((Map<String,?>) json);
52+
return fromChildren((List<Map>) json);
5153
}
5254
catch( IOException e ) {
53-
System.err.println("Failed to read index file: " + e.toString());
55+
System.err.println("Failed to read core definitions: " + e.toString());
5456
return Collections.emptyMap();
5557
}
5658
}
5759

58-
private static Map<String,SchemaNode> fromChildren(Map<String,?> children) {
59-
var entries = children.entrySet().stream()
60-
.map((entry) -> {
61-
var name = entry.getKey();
62-
var node = (Map<String,?>) entry.getValue();
60+
/**
61+
* Load config scopes from a plugin spec.
62+
*
63+
* @param definitions
64+
*/
65+
public static Map<String,SchemaNode> fromDefinitions(List<Map> definitions) {
66+
var entries = definitions.stream()
67+
.filter(node -> "ConfigScope".equals(node.get("type")))
68+
.map((node) -> {
69+
var spec = (Map) node.get("spec");
70+
var name = (String) spec.get("name");
71+
var scope = fromScope(spec);
72+
return Map.entry(name, scope);
73+
})
74+
.toArray(Map.Entry[]::new);
75+
return Map.ofEntries(entries);
76+
}
77+
78+
private static Map<String,SchemaNode> fromChildren(List<Map> children) {
79+
var entries = children.stream()
80+
.map((node) -> {
81+
var spec = (Map) node.get("spec");
82+
var name = (String) spec.get("name");
6383
return Map.entry(name, fromNode(node));
6484
})
6585
.toArray(Map.Entry[]::new);
@@ -68,36 +88,36 @@ private static Map<String,SchemaNode> fromChildren(Map<String,?> children) {
6888

6989
private static SchemaNode fromNode(Map<String,?> node) {
7090
var type = (String) node.get("type");
71-
var spec = (Map<String,?>) node.get("spec");
91+
var spec = (Map) node.get("spec");
7292

73-
if( "Option".equals(type) )
93+
if( "ConfigOption".equals(type) )
7494
return fromOption(spec);
7595

76-
if( "Placeholder".equals(type) )
96+
if( "ConfigPlaceholderScope".equals(type) )
7797
return fromPlaceholder(spec);
7898

79-
if( "Scope".equals(type) )
99+
if( "ConfigScope".equals(type) )
80100
return fromScope(spec);
81101

82102
throw new IllegalStateException();
83103
}
84104

85-
private static SchemaNode.Option fromOption(Map<String,?> node) {
86-
var description = (String) node.get("description");
87-
var type = fromType(node.get("type"));
105+
private static SchemaNode.Option fromOption(Map<String,?> spec) {
106+
var description = (String) spec.get("description");
107+
var type = fromType(spec.get("type"));
88108
return new SchemaNode.Option(description, type);
89109
}
90110

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"));
111+
private static SchemaNode.Placeholder fromPlaceholder(Map<String,?> spec) {
112+
var description = (String) spec.get("description");
113+
var placeholderName = (String) spec.get("placeholderName");
114+
var scope = fromScope((Map) spec.get("scope"));
95115
return new SchemaNode.Placeholder(description, placeholderName, scope);
96116
}
97117

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"));
118+
private static SchemaNode.Scope fromScope(Map<String,?> spec) {
119+
var description = (String) spec.get("description");
120+
var children = fromChildren((List<Map>) spec.get("children"));
101121
return new SchemaNode.Scope(description, children);
102122
}
103123

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,52 @@
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;
2433
import nextflow.config.ast.ConfigVisitorSupport;
2534
import nextflow.config.schema.SchemaNode;
35+
import nextflow.lsp.util.Logger;
2636
import nextflow.script.control.PhaseAware;
2737
import nextflow.script.control.Phases;
2838
import nextflow.script.types.TypesEx;
2939
import org.codehaus.groovy.ast.ASTNode;
40+
import org.codehaus.groovy.ast.expr.ConstantExpression;
3041
import org.codehaus.groovy.control.SourceUnit;
3142
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
3243
import org.codehaus.groovy.control.messages.WarningMessage;
3344
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
45+
import org.codehaus.groovy.runtime.StringGroovyMethods;
3446
import org.codehaus.groovy.syntax.SyntaxException;
3547
import 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
*/
4160
public 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

Comments
 (0)