Skip to content

Commit

Permalink
Refactor config schema classes, add ScopeName annotation
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Sherman <[email protected]>
  • Loading branch information
bentsherman committed Feb 27, 2025
1 parent 1a412f0 commit 908a5c9
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 101 deletions.
105 changes: 10 additions & 95 deletions modules/compiler/src/main/java/config/dsl/ConfigSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@
*/
package nextflow.config.dsl;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import nextflow.config.scopes.NextflowConfig;
import nextflow.config.scopes.ProcessConfig;
Expand All @@ -33,102 +29,21 @@

public class ConfigSchema {

public static final ScopeNode ROOT = scopeNode(RootConfig.class, "");
public static final ScopeNode ROOT = rootScope();

/**
* Get the schema node for a given config scope.
*
* @param names
*/
public static SchemaNode getScope(List<String> names) {
SchemaNode node = ROOT;
for( var name : names ) {
if( node instanceof ScopeNode sn )
node = sn.scopes().get(name);
else if( node instanceof PlaceholderNode pn )
node = pn.scope();
else
return null;
}
return node;
}

/**
* Get the description for a given config option.
*
* @param names
*/
public static String getOption(List<String> names) {
SchemaNode node = ROOT;
for( int i = 0; i < names.size() - 1; i++ ) {
var name = names.get(i);
if( node instanceof ScopeNode sn )
node = sn.scopes().get(name);
else if( node instanceof PlaceholderNode pn )
node = pn.scope();
else
return null;
}
var optionName = names.get(names.size() - 1);
return node instanceof ScopeNode sn
? sn.options().get(optionName)
: null;
}

private static ScopeNode scopeNode(Class<? extends ConfigScope> scope, String description) {
if( scope.equals(NextflowConfig.class) )
return nextflowScope(description);
if( scope.equals(ProcessConfig.class) )
return processScope(description);
var options = new HashMap<String, String>();
var scopes = new HashMap<String, SchemaNode>();
for( var field : scope.getDeclaredFields() ) {
var name = field.getName();
var type = field.getType();
var placeholderName = field.getAnnotation(PlaceholderName.class);
// fields annotated with @ConfigOption are config options
if( field.getAnnotation(ConfigOption.class) != null ) {
var desc = annotatedDescription(field, "");
options.put(name, desc);
}
// fields of type ConfigScope are nested config scopes
else if( ConfigScope.class.isAssignableFrom(type) ) {
var desc = annotatedDescription(field, description);
scopes.put(name, scopeNode((Class<? extends ConfigScope>) type, desc));
}
// fields of type Map<String, ConfigScope> are placeholder scopes
// (e.g. `azure.batch.pools.<name>`)
else if( Map.class.isAssignableFrom(type) && placeholderName != null ) {
var desc = annotatedDescription(field, description);
var pt = (ParameterizedType)field.getGenericType();
var valueType = (Class<? extends ConfigScope>)pt.getActualTypeArguments()[1];
scopes.put(name, placeholderNode(desc, placeholderName.value(), valueType));
}
}
for( var method : scope.getDeclaredMethods() ) {
if( method.getAnnotation(ConfigOption.class) != null ) {
var desc = annotatedDescription(method, "");
options.put(method.getName(), desc);
}
}
return new ScopeNode(description, options, scopes);
}

private static String annotatedDescription(AnnotatedElement el, String defaultValue) {
var annot = el.getAnnotation(Description.class);
return annot != null ? annot.value() : defaultValue;
}

private static PlaceholderNode placeholderNode(String description, String placeholderName, Class<? extends ConfigScope> valueType) {
return new PlaceholderNode(description, placeholderName, scopeNode(valueType, description));
private static ScopeNode rootScope() {
var result = ScopeNode.of(RootConfig.class, "");
result.scopes().put("nextflow", nextflowScope(""));
result.scopes().put("process", processScope(""));
return result;
}

/**
* Derive `nextflow` config options from feature flags.
*
* @param description
*/
private static ScopeNode nextflowScope(String description) {
private static SchemaNode nextflowScope(String description) {
var enableOpts = new HashMap<String, String>();
var previewOpts = new HashMap<String, String>();
for( var field : FeatureFlagDsl.class.getDeclaredFields() ) {
Expand All @@ -144,8 +59,8 @@ else if( fqName.startsWith("nextflow.preview.") )
throw new IllegalArgumentException();
}
var scopes = Map.ofEntries(
Map.entry("enable", new ScopeNode(description, enableOpts, Collections.emptyMap())),
Map.entry("preview", new ScopeNode(description, previewOpts, Collections.emptyMap()))
Map.entry("enable", (SchemaNode) new ScopeNode(description, enableOpts, Collections.emptyMap())),
Map.entry("preview", (SchemaNode) new ScopeNode(description, previewOpts, Collections.emptyMap()))
);
return new ScopeNode(description, Collections.emptyMap(), scopes);
}
Expand All @@ -155,7 +70,7 @@ else if( fqName.startsWith("nextflow.preview.") )
*
* @param description
*/
private static ScopeNode processScope(String description) {
private static SchemaNode processScope(String description) {
var options = new HashMap<String, String>();
for( var method : ProcessDsl.DirectiveDsl.class.getDeclaredMethods() ) {
var desc = method.getAnnotation(Description.class);
Expand Down
43 changes: 43 additions & 0 deletions modules/compiler/src/main/java/config/dsl/SchemaNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,49 @@
*/
package nextflow.config.dsl;

import java.util.List;

public sealed interface SchemaNode permits ScopeNode, PlaceholderNode {
String description();

/**
* Get the schema node for a given config scope.
*
* @param names
*/
default SchemaNode getScope(List<String> names) {
SchemaNode node = this;
for( var name : names ) {
if( node instanceof ScopeNode sn )
node = sn.scopes().get(name);
else if( node instanceof PlaceholderNode pn )
node = pn.scope();
else
return null;
}
return node;
}

/**
* Get the description for a given config option.
*
* @param names
*/
default String getOption(List<String> names) {
SchemaNode node = this;
for( int i = 0; i < names.size() - 1; i++ ) {
var name = names.get(i);
if( node instanceof ScopeNode sn )
node = sn.scopes().get(name);
else if( node instanceof PlaceholderNode pn )
node = pn.scope();
else
return null;
}
var optionName = names.get(names.size() - 1);
return node instanceof ScopeNode sn
? sn.options().get(optionName)
: null;
}

}
27 changes: 27 additions & 0 deletions modules/compiler/src/main/java/config/dsl/ScopeName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.dsl;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface ScopeName {
String value();
}
61 changes: 59 additions & 2 deletions modules/compiler/src/main/java/config/dsl/ScopeNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,67 @@
*/
package nextflow.config.dsl;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;

import nextflow.script.dsl.Description;

public record ScopeNode(
String description,
Map<String, String> options,
Map<String, ? extends SchemaNode> scopes
) implements SchemaNode {}
Map<String, SchemaNode> scopes
) implements SchemaNode {

/**
* Create a scope node from a ConfigScope class.
*
* @param scope
* @param description
*/
public static ScopeNode of(Class<? extends ConfigScope> scope, String description) {
var options = new HashMap<String, String>();
var scopes = new HashMap<String, SchemaNode>();
for( var field : scope.getDeclaredFields() ) {
var name = field.getName();
var type = field.getType();
var placeholderName = field.getAnnotation(PlaceholderName.class);
// fields annotated with @ConfigOption are config options
if( field.getAnnotation(ConfigOption.class) != null ) {
var desc = annotatedDescription(field, "");
options.put(name, desc);
}
// fields of type ConfigScope are nested config scopes
else if( ConfigScope.class.isAssignableFrom(type) ) {
var desc = annotatedDescription(field, description);
scopes.put(name, ScopeNode.of((Class<? extends ConfigScope>) type, desc));
}
// fields of type Map<String, ConfigScope> are placeholder scopes
// (e.g. `azure.batch.pools.<name>`)
else if( Map.class.isAssignableFrom(type) && placeholderName != null ) {
var desc = annotatedDescription(field, description);
var pt = (ParameterizedType)field.getGenericType();
var valueType = (Class<? extends ConfigScope>)pt.getActualTypeArguments()[1];
scopes.put(name, placeholderNode(desc, placeholderName.value(), valueType));
}
}
for( var method : scope.getDeclaredMethods() ) {
if( method.getAnnotation(ConfigOption.class) != null ) {
var desc = annotatedDescription(method, "");
options.put(method.getName(), desc);
}
}
return new ScopeNode(description, options, scopes);
}

private static String annotatedDescription(AnnotatedElement el, String defaultValue) {
var annot = el.getAnnotation(Description.class);
return annot != null ? annot.value() : defaultValue;
}

private static PlaceholderNode placeholderNode(String description, String placeholderName, Class<? extends ConfigScope> valueType) {
return new PlaceholderNode(description, placeholderName, ScopeNode.of(valueType, description));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private List<String> getCurrentScope(List<ASTNode> nodeStack) {
}

private List<CompletionItem> getConfigOptions(List<String> names) {
var scope = ConfigSchema.getScope(names);
var scope = ConfigSchema.ROOT.getScope(names);
if( scope instanceof ScopeNode sn ) {
return sn.options().entrySet().stream()
.map(entry -> getConfigOption(entry.getKey(), entry.getValue()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ protected String getHoverContent(List<ASTNode> nodeStack) {
names.addAll(assign.names);

var fqName = String.join(".", names);
var option = ConfigSchema.getOption(names);
var option = ConfigSchema.ROOT.getOption(names);
if( option != null ) {
var description = StringGroovyMethods.stripIndent(option, true).trim();
var builder = new StringBuilder();
Expand All @@ -117,7 +117,7 @@ else if( Logger.isDebugEnabled() ) {
if( names.isEmpty() )
return null;

var scope = ConfigSchema.getScope(names);
var scope = ConfigSchema.ROOT.getScope(names);
if( scope != null ) {
return StringGroovyMethods.stripIndent(scope.description(), true).trim();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public void visitConfigAssign(ConfigAssignNode node) {
var fqName = String.join(".", names);
if( fqName.startsWith("process.ext.") )
return;
var option = ConfigSchema.getOption(names);
var option = ConfigSchema.ROOT.getOption(names);
if( option == null )
addWarning("Unrecognized config option '" + fqName + "'", String.join(".", node.names), node.getLineNumber(), node.getColumnNumber());
}
Expand Down

0 comments on commit 908a5c9

Please sign in to comment.