Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
*/
package nextflow.plugin.spec

import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

import groovy.transform.CompileStatic
import nextflow.config.spec.SpecNode
import nextflow.script.types.Types
import org.codehaus.groovy.ast.ClassNode
import nextflow.script.dsl.Types

/**
* Generate specs for config scopes.
Expand All @@ -44,7 +46,7 @@ class ConfigSpec {

private static Map<String,?> fromOption(SpecNode.Option node, String name) {
final description = node.description().stripIndent(true).trim()
final types = node.types().collect { t -> fromType(new ClassNode(t)) }
final types = node.types().collect { t -> fromType(t) }

return [
type: 'ConfigOption',
Expand Down Expand Up @@ -89,14 +91,13 @@ class ConfigSpec {
]
}

private static Object fromType(ClassNode cn) {
final name = Types.getName(cn.getTypeClass())
if( !cn.isGenericsPlaceHolder() && cn.getGenericsTypes() != null ) {
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
private static Object fromType(Type type) {
if( type instanceof ParameterizedType ) {
final name = Types.getName(type.getRawType())
final typeArguments = type.getActualTypeArguments().collect { t -> fromType(t) }
return [ name: name, typeArguments: typeArguments ]
}
else {
return name
}

return Types.getName(type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
package nextflow.plugin.spec

import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

import groovy.transform.CompileStatic
import nextflow.script.dsl.Description
import nextflow.script.types.Types
import org.codehaus.groovy.ast.ClassNode
import nextflow.script.dsl.Types

/**
* Generate specs for functions, channel factories, and operators.
Expand All @@ -37,7 +38,7 @@ class FunctionSpec {
final parameters = method.getParameters().collect { param ->
[
name: param.getName(),
type: fromType(param.getType())
type: fromType(param.getParameterizedType())
]
}

Expand All @@ -52,18 +53,13 @@ class FunctionSpec {
]
}

private static Object fromType(Class c) {
return fromType(new ClassNode(c))
}

private static Object fromType(ClassNode cn) {
final name = Types.getName(cn.getTypeClass())
if( !cn.isGenericsPlaceHolder() && cn.getGenericsTypes() != null ) {
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
private static Object fromType(Type type) {
if( type instanceof ParameterizedType ) {
final name = Types.getName(type.getRawType())
final typeArguments = type.getActualTypeArguments().collect { t -> fromType(t) }
return [ name: name, typeArguments: typeArguments ]
}
else {
return name
}

return Types.getName(type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import nextflow.script.ProcessConfigV2
import nextflow.script.ScriptMeta
import nextflow.script.ScriptType
import nextflow.script.bundle.ResourcesBundle
import nextflow.script.dsl.Types
import nextflow.script.params.BaseOutParam
import nextflow.script.params.CmdEvalParam
import nextflow.script.params.DefaultOutParam
Expand All @@ -97,7 +98,6 @@ import nextflow.script.params.v2.ProcessInput
import nextflow.script.params.v2.ProcessTupleInput
import nextflow.script.types.Record
import nextflow.script.types.Tuple
import nextflow.script.types.Types
import nextflow.trace.TraceRecord
import nextflow.util.Escape
import nextflow.util.HashBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import groovy.util.logging.Slf4j
import nextflow.Session
import nextflow.file.FileHelper
import nextflow.exception.ScriptRuntimeException
import nextflow.script.dsl.Types
import nextflow.script.types.Bag
import nextflow.script.types.Types
import nextflow.splitter.CsvSplitter
import nextflow.util.ArrayBag
import nextflow.util.Duration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class TypeDef extends ComponentDef {
this.alias = alias
}

Class getTarget() { target }

@Override
String getType() { 'type' }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,57 +47,65 @@ class PluginSpecTest extends Specification {
expect:
definitions.size() == 4
and:
definitions[0] == [
type: 'ConfigScope',
spec: [
name: 'hello',
description: 'The `hello` scope controls the behavior of the `nf-hello` plugin.',
children: [
[
type: 'ConfigOption',
spec: [
name: 'message',
description: 'Message to print to standard output when the plugin is enabled.',
type: 'String',
additionalTypes: []
]
]
definitions[0].type == 'ConfigScope'
definitions[0].spec.name == 'hello'
definitions[0].spec.description == 'The `hello` scope controls the behavior of the `nf-hello` plugin.'
definitions[0].spec.children.sort { it.spec.name } == [
[
type: 'ConfigOption',
spec: [
name: 'message',
description: 'Message to print to standard output when the plugin is enabled.',
type: 'String',
additionalTypes: []
]
],
[
type: 'ConfigOption',
spec: [
name: 'names',
description: 'Names to address when the plugin is enabled.',
type: [
name: 'List',
typeArguments: [ 'String' ]
],
additionalTypes: []
]
]
]
definitions[1] == [
type: 'Factory',
spec: [
name: 'helloFactory',
description: null,
returnType: 'DataflowWriteChannel',
parameters: []
]
and:
definitions[1].type == 'Factory'
definitions[1].spec == [
name: 'helloFactory',
description: null,
returnType: 'DataflowWriteChannel',
parameters: []
]
definitions[2] == [
type: 'Operator',
spec: [
name: 'helloOperator',
description: null,
returnType: 'DataflowWriteChannel',
parameters: [
[
name: 'arg0',
type: 'DataflowReadChannel'
]
and:
definitions[2].type == 'Operator'
definitions[2].spec == [
name: 'helloOperator',
description: null,
returnType: 'DataflowWriteChannel',
parameters: [
[
name: 'arg0',
type: 'DataflowReadChannel'
]
]
]
definitions[3] == [
type: 'Function',
spec: [
name: 'sayHello',
description: 'Say hello to the given targets',
returnType: 'void',
parameters: [
[
name: 'arg0',
type: 'List'
and:
definitions[3].type == 'Function'
definitions[3].spec == [
name: 'sayHello',
description: 'Say hello to the given targets',
returnType: 'void',
parameters: [
[
name: 'arg0',
type: [
name: 'List',
typeArguments: [ 'String' ]
]
]
]
Expand All @@ -115,6 +123,12 @@ class TestConfig implements ConfigScope {
Message to print to standard output when the plugin is enabled.
''')
String message

@ConfigOption
@Description('''
Names to address when the plugin is enabled.
''')
List<String> names
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package nextflow.script

import java.lang.reflect.ParameterizedType
import java.nio.file.Files
import java.nio.file.Path

import nextflow.exception.ScriptCompilationException
import test.Dsl2Spec
Expand Down Expand Up @@ -195,4 +197,49 @@ class ScriptTypesTest extends Dsl2Spec {
cleanup:
folder?.deleteDir()
}

def 'should expose type annotations via reflection'() {
when:
def script = loadScript(
'''\
record Sample {
id: String
reads: List<Path>
}
''',
module: true
)
def meta = ScriptMeta.get(script)
def typeDef = meta.getComponent('Sample') as TypeDef
def type = typeDef.getTarget()
then:
type.getField('id').getType() == String
type.getField('id').getGenericType() instanceof Class
type.getField('id').getGenericType() == String
type.getField('reads').getType() == List
type.getField('reads').getGenericType() instanceof ParameterizedType
type.getField('reads').getGenericType().getRawType() == List
type.getField('reads').getGenericType().getActualTypeArguments()[0] instanceof Class
type.getField('reads').getGenericType().getActualTypeArguments()[0] == Path

when:
script = loadScript(
'''\
def greet(message: String, names: List<String>) {
}
''',
module: true
)
def method = script.getClass().getDeclaredMethods().find { m -> m.name == 'greet' }
def params = method.getParameters()
then:
params[0].getType() == String
params[0].getParameterizedType() instanceof Class
params[0].getParameterizedType() == String
params[1].getType() == List
params[1].getParameterizedType() instanceof ParameterizedType
params[1].getParameterizedType().getRawType() == List
params[1].getParameterizedType().getActualTypeArguments()[0] instanceof Class
params[1].getParameterizedType().getActualTypeArguments()[0] == String
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import nextflow.config.parser.ConfigParserPluginFactory;
import nextflow.script.control.Compiler;
import nextflow.script.control.LazyErrorCollector;
import nextflow.script.types.Types;
import nextflow.script.dsl.Types;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.WarningMessage;
Expand Down
33 changes: 21 additions & 12 deletions modules/nf-lang/src/main/java/nextflow/config/spec/SpecNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -109,10 +110,10 @@ private static String annotatedDescription(AnnotatedElement el, String defaultVa
return annot != null ? annot.value() : defaultValue;
}

private static List<Class> optionTypes(Field field) {
var result = new ArrayList<Class>();
private static List<Type> optionTypes(Field field) {
var result = new ArrayList<Type>();
// use the field type
result.add(field.getType());
result.add(field.getGenericType());
// append types from ConfigOption annotation if specified
var annot = field.getAnnotation(ConfigOption.class);
if( annot != null ) {
Expand All @@ -122,6 +123,14 @@ private static List<Class> optionTypes(Field field) {
return result;
}

private static Class rawType(Type type) {
if( type instanceof Class c )
return c;
if( type instanceof ParameterizedType pt )
return (Class) pt.getRawType();
throw new IllegalStateException();
}

/**
* Models a config option that is defined through a DSL
* instead of an assignment (i.e. `plugins`).
Expand All @@ -136,7 +145,7 @@ public static record DslOption(
*/
public static record Option(
String description,
List<Class> types
List<Type> types
) implements SpecNode {}

/**
Expand Down Expand Up @@ -212,23 +221,23 @@ public static Scope of(Class<? extends ConfigScope> scope, String description) {
var children = new HashMap<String, SpecNode>();
for( var field : scope.getDeclaredFields() ) {
var name = field.getName();
var type = field.getType();
var type = field.getGenericType();
var rawType = rawType(type);
var desc = annotatedDescription(field, description);
var placeholderName = field.getAnnotation(PlaceholderName.class);
// fields annotated with @ConfigOption are config options
if( field.getAnnotation(ConfigOption.class) != null ) {
if( DslScope.class.isAssignableFrom(type) )
children.put(name, new DslOption(desc, type));
if( DslScope.class.isAssignableFrom(rawType) )
children.put(name, new DslOption(desc, rawType));
else
children.put(name, new Option(desc, optionTypes(field)));
}
// fields of type ConfigScope are nested config scopes
else if( ConfigScope.class.isAssignableFrom(type) ) {
children.put(name, Scope.of((Class<? extends ConfigScope>) type, desc));
// fields of rawType ConfigScope are nested config scopes
else if( ConfigScope.class.isAssignableFrom(rawType) ) {
children.put(name, Scope.of((Class<? extends ConfigScope>) rawType, desc));
}
// fields of type Map<String, ConfigScope> are placeholder scopes
else if( Map.class.isAssignableFrom(type) && placeholderName != null ) {
var pt = (ParameterizedType)field.getGenericType();
else if( Map.class.isAssignableFrom(rawType) && type instanceof ParameterizedType pt && placeholderName != null ) {
var valueType = (Class<? extends ConfigScope>)pt.getActualTypeArguments()[1];
children.put(name, new Placeholder(desc, placeholderName.value(), Scope.of(valueType, desc)));
}
Expand Down
Loading
Loading