Skip to content
Open
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 @@ -18,9 +18,8 @@ package nextflow.config.schema
import groovy.json.JsonOutput
import groovy.transform.TypeChecked
import nextflow.plugin.Plugins
import nextflow.plugin.index.ConfigSchema
import nextflow.script.dsl.Description
import nextflow.script.types.Types
import org.codehaus.groovy.ast.ClassNode

@TypeChecked
class JsonRenderer {
Expand All @@ -38,86 +37,16 @@ class JsonRenderer {
final description = clazz.getAnnotation(Description)?.value()
if( scopeName == '' ) {
SchemaNode.Scope.of(clazz, '').children().each { name, node ->
result.put(name, fromNode(node))
result.put(name, ConfigSchema.of(node))
}
continue
}
if( !scopeName )
continue
final node = SchemaNode.Scope.of(clazz, description)
result.put(scopeName, fromNode(node, scopeName))
result.put(scopeName, ConfigSchema.of(node, scopeName))
}
return result
}

private static Map<String,?> fromNode(SchemaNode node, String name=null) {
if( node instanceof SchemaNode.Option )
return fromOption(node)
if( node instanceof SchemaNode.Placeholder )
return fromPlaceholder(node)
if( node instanceof SchemaNode.Scope )
return fromScope(node, name)
throw new IllegalStateException()
}

private static Map<String,?> fromOption(SchemaNode.Option node) {
final description = node.description().stripIndent(true).trim()
final type = fromType(new ClassNode(node.type()))

return [
type: 'Option',
spec: [
description: description,
type: type
]
]
}

private static Map<String,?> fromPlaceholder(SchemaNode.Placeholder node) {
final description = node.description().stripIndent(true).trim()
final placeholderName = node.placeholderName()
final scope = fromScope(node.scope())

return [
type: 'Placeholder',
spec: [
description: description,
placeholderName: placeholderName,
scope: scope.spec
]
]
}

private static Map<String,?> fromScope(SchemaNode.Scope node, String scopeName=null) {
final description = node.description().stripIndent(true).trim()
final children = node.children().collectEntries { name, child ->
Map.entry(name, fromNode(child, name))
}

return [
type: 'Scope',
spec: [
description: withLink(scopeName, description),
children: children
]
]
}

private static String withLink(String scopeName, String description) {
return scopeName
? "$description\n\n[Read more](https://nextflow.io/docs/latest/reference/config.html#$scopeName)\n"
: description
}

private static Object fromType(ClassNode cn) {
final name = Types.getName(cn.getTypeClass())
if( cn.isUsingGenerics() ) {
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
return [ name: name, typeArguments: typeArguments ]
}
else {
return name
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2024-2025, 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.plugin.index

import groovy.transform.CompileStatic
import nextflow.config.schema.SchemaNode
import nextflow.script.types.Types
import org.codehaus.groovy.ast.ClassNode

@CompileStatic
class ConfigSchema {

static Map<String,?> of(SchemaNode node, String name=null) {
return fromNode(node, name)
}

private static Map<String,?> fromNode(SchemaNode node, String name=null) {
if( node instanceof SchemaNode.Option )
return fromOption(node)
if( node instanceof SchemaNode.Placeholder )
return fromPlaceholder(node)
if( node instanceof SchemaNode.Scope )
return fromScope(node, name)
throw new IllegalStateException()
}

private static Map<String,?> fromOption(SchemaNode.Option node) {
final description = node.description().stripIndent(true).trim()
final type = fromType(new ClassNode(node.type()))

return [
type: 'ConfigOption',
spec: [
description: description,
type: type
]
]
}

private static Map<String,?> fromPlaceholder(SchemaNode.Placeholder node) {
final description = node.description().stripIndent(true).trim()
final placeholderName = node.placeholderName()
final scope = fromScope(node.scope())

return [
type: 'ConfigPlaceholderScope',
spec: [
description: description,
placeholderName: placeholderName,
scope: scope.spec
]
]
}

private static Map<String,?> fromScope(SchemaNode.Scope node, String scopeName=null) {
final description = node.description().stripIndent(true).trim()
final children = node.children().collectEntries { name, child ->
Map.entry(name, fromNode(child, name))
}

return [
type: 'ConfigScope',
spec: [
name: scopeName,
description: withLink(scopeName, description),
children: children
]
]
}

private static String withLink(String scopeName, String description) {
return scopeName
? "$description\n\n[Read more](https://nextflow.io/docs/latest/reference/config.html#$scopeName)\n"
: description
}

private static Object fromType(ClassNode cn) {
final name = Types.getName(cn.getTypeClass())
if( cn.isUsingGenerics() ) {
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
return [ name: name, typeArguments: typeArguments ]
}
else {
return name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2024-2025, 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.plugin.index

import java.lang.reflect.Method

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

@CompileStatic
class FunctionSchema {

static Map<String,?> of(Method method, String type) {
final name = method.getName()
final returnType = fromType(method.getReturnType())
final parameters = method.getParameters().collect { param ->
[
name: param.getName(),
type: fromType(param.getType())
]
}

return [
type: type,
spec: [
name: name,
returnType: returnType,
parameters: parameters
]
]
}

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.isUsingGenerics() ) {
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
return [ name: name, typeArguments: typeArguments ]
}
else {
return name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024-2025, 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.plugin.index

import groovy.json.JsonOutput
import groovy.transform.CompileStatic
import nextflow.config.schema.ConfigScope
import nextflow.config.schema.SchemaNode
import nextflow.config.schema.ScopeName
import nextflow.plugin.extension.Factory
import nextflow.plugin.extension.Function
import nextflow.plugin.extension.Operator
import nextflow.plugin.extension.PluginExtensionPoint
import nextflow.script.dsl.Description

/**
* Build an index of plugin definitions.
*
* @author Ben Sherman <[email protected]>
*/
@CompileStatic
class PluginIndex {

private List<String> extensionPoints

PluginIndex(List<String> extensionPoints) {
this.extensionPoints = extensionPoints
}

List build() {
final classLoader = Thread.currentThread().getContextClassLoader()

// extract schema for each plugin definition
final definitions = []

for( final className : extensionPoints ) {
final clazz = classLoader.loadClass(className)

if( ConfigScope.class.isAssignableFrom(clazz) ) {
final scopeName = clazz.getAnnotation(ScopeName)?.value()
final description = clazz.getAnnotation(Description)?.value()
if( !scopeName )
continue
final node = SchemaNode.Scope.of(clazz, description)

definitions.add(ConfigSchema.of(node, scopeName))
}

if( PluginExtensionPoint.class.isAssignableFrom(clazz) ) {
final methods = clazz.getDeclaredMethods()
for( final method : methods ) {
if( method.getAnnotation(Factory) )
definitions.add(FunctionSchema.of(method, 'Factory'))
else if( method.getAnnotation(Function) )
definitions.add(FunctionSchema.of(method, 'Function'))
else if( method.getAnnotation(Operator) )
definitions.add(FunctionSchema.of(method, 'Operator'))
}
}
}

return definitions
}
}


@CompileStatic
class PluginIndexWriter {

static void main(String[] args) {
if (args.length < 2) {
System.err.println("Usage: PluginIndexWriter <output-path> <class1> [class2] ...")
System.exit(1)
}

final outputPath = args[0]
final extensionPoints = args[1..-1]

// build index
final index = new PluginIndex(extensionPoints).build()

// write index file
final file = new File(outputPath)
file.parentFile.mkdirs()
file.text = JsonOutput.toJson(index)

println "Saved plugin index to $file"
}
}