Skip to content

Commit ed6bb60

Browse files
Generate plugin spec (#6361)
--------- Signed-off-by: Ben Sherman <[email protected]> Signed-off-by: Paolo Di Tommaso <[email protected]> Co-authored-by: Paolo Di Tommaso <[email protected]>
1 parent 3b70900 commit ed6bb60

File tree

22 files changed

+745
-183
lines changed

22 files changed

+745
-183
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ The project follows a modular architecture with a plugin-based system for cloud
104104
- `build.gradle`: Root build configuration with multi-module setup
105105
- `settings.gradle`: Gradle project structure definition
106106
- `plugins/*/VERSION`: Define the version of the corresponding plugin sub-project.
107+
- `adr/`: Architecture Decision Records (ADRs) documenting significant structural and technical decisions in the project
107108

108109
## Release process
109110

adr/20250922-plugin-spec.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Plugin Spec
2+
3+
- Authors: Ben Sherman
4+
- Status: accepted
5+
- Deciders: Ben Sherman, Paolo Di Tommaso
6+
- Date: 2025-09-22
7+
- Tags: plugins
8+
9+
## Summary
10+
11+
Provide a way for external systems to understand key information about third-party plugins.
12+
13+
## Problem Statement
14+
15+
Nextflow plugins need a way to statically declare extensions to the Nextflow language so that external systems can extract information about a plugin without loading it in the JVM.
16+
17+
Primary use cases:
18+
19+
- The Nextflow language server needs to know about any config scopes, custom functions, etc, defined by a plugin, in order to recognize them in Nextflow scripts and config files.
20+
21+
- The Nextflow plugin registry (or other user interfaces) can use this information to provide API documentation.
22+
23+
## Goals or Decision Drivers
24+
25+
- External systems (e.g. language server) need to be able to understand plugins without having to load them in the JVM.
26+
27+
## Non-goals
28+
29+
- Defining specs for the core runtime and core plugins: these definitions are handled separately, although they may share some functionality with plugin specs.
30+
31+
## Considered Options
32+
33+
### Nextflow plugin system
34+
35+
Require external systems to use Nextflow's plugin system to load plugins at runtime in order to extract information about them.
36+
37+
- **Pro:** Allows any information to be extracted since the entire plugin is loaded
38+
39+
- **Con:** Requires the entire Nextflow plugin system to be reused or reimplemented. Not ideal for Java applications since the plugin system is implemented in Groovy, incompatible with non-JVM applications
40+
41+
- **Con:** Requires plugins to be downloaded, cached, loaded in the JVM, even though there is no need to use the plugin.
42+
43+
### Plugin spec
44+
45+
Define a plugin spec for every plugin release which is stored and served by the plugin registry as JSON.
46+
47+
- **Pro:** Allows any system to inspect plugin definitions through a standard JSON document, instead of downloading plugins and loading them into a JVM.
48+
49+
- **Con:** Requires the plugin spec to be generated at build-time and stored in the plugin registry.
50+
51+
- **Con:** Requires a standard format to ensure interoperability across different versions of Nextflow, the language server, and third-party plugins.
52+
53+
## Solution
54+
55+
Define a plugin spec for every plugin release which is stored and served by the plugin registry as JSON.
56+
57+
- Plugin developers only need to define [extension points](https://nextflow.io/docs/latest/plugins/developing-plugins.html#extension-points) as usual, and the Gradle plugin will extract the plugin spec and store it in the plugin registry as part of each plugin release.
58+
59+
- The language server can infer which third-party plugins are required from the `plugins` block in a config file. It will retrieve the appropriate plugin specs from the plugin registry.
60+
61+
A plugin spec consists of a list of *definitions*. Each definition has a *type* and a *spec*.
62+
63+
Example:
64+
65+
```json
66+
{
67+
"$schema": "https://raw.githubusercontent.com/nextflow-io/schemas/main/plugin/v1/schema.json",
68+
"definitions": [
69+
{
70+
"type": "ConfigScope",
71+
"spec": {
72+
// ...
73+
}
74+
},
75+
{
76+
"type": "Function",
77+
"spec": {
78+
// ...
79+
}
80+
},
81+
]
82+
}
83+
```
84+
85+
The following types of definitions are allowed:
86+
87+
**ConfigScope**
88+
89+
Defines a top-level config scope. The spec consists of a *name*, an optional *description*, and *children*.
90+
91+
The children should be a list of definitions corresponding to nested config scopes and options. The following definitions are allowed:
92+
93+
- **ConfigOption**: Defines a config option. The spec consists of a *description* and *type*.
94+
95+
- **ConfigScope**: Defines a nested config scope, using the same spec as for top-level scopes.
96+
97+
Example:
98+
99+
```json
100+
{
101+
"type": "ConfigScope",
102+
"spec": {
103+
"name": "hello",
104+
"description": "The `hello` scope controls the behavior of the `nf-hello` plugin.",
105+
"children": [
106+
{
107+
"type": "ConfigOption",
108+
"spec": {
109+
"name": "message",
110+
"description": "Message to print to standard output when the plugin is enabled.",
111+
"type": "String"
112+
}
113+
}
114+
]
115+
}
116+
}
117+
```
118+
119+
**Factory**
120+
121+
Defines a channel factory that can be included in Nextflow scripts. The spec is the same as for functions.
122+
123+
**Function**
124+
125+
Defines a function that can be included in Nextflow scripts. The spec consists of a *name*, an optional *description*, a *return type*, and a list of *parameters*. Each parameter consists of a *name* and a *type*.
126+
127+
Example:
128+
129+
```json
130+
{
131+
"type": "Function",
132+
"spec": {
133+
"name": "sayHello",
134+
"description": "Say hello to the given target",
135+
"returnType": "void",
136+
"parameters": [
137+
{
138+
"name": "target",
139+
"type": "String"
140+
}
141+
]
142+
}
143+
}
144+
```
145+
146+
**Operator**
147+
148+
Defines a channel operator that can be included in Nextflow scripts. The spec is the same as for functions.
149+
150+
## Rationale & discussion
151+
152+
Now that there is a Gradle plugin for building Nextflow plugins and a registry to publish and retrieve plugins, it is possible to generate, publish, and retrieve plugin specs in a way that is transparent to plugin developers.
153+
154+
Plugins specs adhere to a pre-defined [schema](https://raw.githubusercontent.com/nextflow-io/schemas/main/plugin/v1/schema.json) to ensure consistency across different versions of Nextflow. In the future, new versions of the schema can be defined as needed to support new behaviors or requirements.

adr/YYYYMMDD-template-name.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# [short title of solved problem and solution]
2+
3+
- Authors: [who wrote the ADR]
4+
- Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [xxx](xxx.md)]
5+
- Deciders: [list everyone involved in the decision] <!-- optional - to be formalised -->
6+
- Date: [YYYY-MM-DD when the decision was last updated]
7+
- Tags: [space and/or comma separated list of tags]
8+
9+
Technical Story: [description | ticket/issue URL] <!-- optional -->
10+
11+
## Summary
12+
13+
Quick description of the problem and the context. Should not take more than 2-3 lines.
14+
15+
## Problem Statement
16+
17+
Description of the technical problem to solve or to decision to make. This should be concise but provide all required details and the context related to the technical decision to be taken.
18+
19+
## Goals or Decision Drivers
20+
21+
Depending the context define clearly what are the goals or what are the most important decision drivers.
22+
23+
- [driver 1, e.g., a force, facing concern, …]
24+
- [driver 2, e.g., a force, facing concern, …]
25+
-<!-- numbers of drivers can vary -->
26+
27+
## Non-goals
28+
29+
Define what's out of the scope of this ADR.
30+
31+
## Considered Options <!-- optional -->
32+
33+
- [option 1]
34+
- [option 2]
35+
- [option 3]
36+
-<!-- numbers of options can vary -->
37+
38+
39+
## Pros and Cons of the Options <!-- optional -->
40+
41+
### [option 1]
42+
43+
[example | description | pointer to more information | …] <!-- optional -->
44+
45+
- Good, because [argument a]
46+
- Good, because [argument b]
47+
- Bad, because [argument c]
48+
-<!-- numbers of pros and cons can vary -->
49+
50+
### [option 2]
51+
52+
[example | description | pointer to more information | …] <!-- optional -->
53+
54+
- Good, because [argument a]
55+
- Good, because [argument b]
56+
- Bad, because [argument c]
57+
-<!-- numbers of pros and cons can vary -->
58+
59+
60+
## Solution or decision outcome
61+
62+
Summarize the solution or decision outcome in one-two lines.
63+
64+
## Rationale & discussion
65+
66+
Describe the solution or the decision outcome discussing how decision drivers have been applied and how it matches the declared goals. This section is expected to be concise though providing comprehensive description of the technical solution and covering all uncertainty or ambiguous points.
67+
68+
## Links <!-- optional -->
69+
70+
- [Link type](link to adr) <!-- example: Refined by [xxx](yyyymmdd-xxx.md) -->
71+
-<!-- numbers of links can vary -->
72+
73+
## More information
74+
75+
- [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them)
76+
- [ADR GitHub organization](https://adr.github.io/)

docs/developer/diagrams/nextflow.plugin.mmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ classDiagram
66

77
Plugins --> PluginsFacade : init
88

9-
PluginsFacade "1" --> "*" PluginSpec : load
9+
PluginsFacade "1" --> "*" PluginRef : load
1010

11-
class PluginSpec {
11+
class PluginRef {
1212
id : String
1313
version : String
1414
}

modules/nextflow/src/main/groovy/nextflow/config/schema/JsonRenderer.groovy renamed to modules/nextflow/src/main/groovy/nextflow/plugin/spec/ConfigSpec.groovy

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,74 +13,58 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package nextflow.config.schema
16+
package nextflow.plugin.spec
1717

18-
import groovy.json.JsonOutput
19-
import groovy.transform.TypeChecked
20-
import nextflow.plugin.Plugins
21-
import nextflow.script.dsl.Description
18+
import groovy.transform.CompileStatic
19+
import nextflow.config.schema.SchemaNode
2220
import nextflow.script.types.Types
2321
import org.codehaus.groovy.ast.ClassNode
2422

25-
@TypeChecked
26-
class JsonRenderer {
27-
28-
String render() {
29-
final schema = getSchema()
30-
return JsonOutput.toJson(schema)
31-
}
23+
/**
24+
* Generate specs for config scopes.
25+
*
26+
* @author Ben Sherman <[email protected]>
27+
*/
28+
@CompileStatic
29+
class ConfigSpec {
3230

33-
private static Map<String,?> getSchema() {
34-
final result = new HashMap<String,?>()
35-
for( final scope : Plugins.getExtensions(ConfigScope) ) {
36-
final clazz = scope.getClass()
37-
final scopeName = clazz.getAnnotation(ScopeName)?.value()
38-
final description = clazz.getAnnotation(Description)?.value()
39-
if( scopeName == '' ) {
40-
SchemaNode.Scope.of(clazz, '').children().each { name, node ->
41-
result.put(name, fromNode(node))
42-
}
43-
continue
44-
}
45-
if( !scopeName )
46-
continue
47-
final node = SchemaNode.Scope.of(clazz, description)
48-
result.put(scopeName, fromNode(node, scopeName))
49-
}
50-
return result
31+
static Map<String,?> of(SchemaNode node, String name) {
32+
return fromNode(node, name)
5133
}
5234

53-
private static Map<String,?> fromNode(SchemaNode node, String name=null) {
35+
private static Map<String,?> fromNode(SchemaNode node, String name) {
5436
if( node instanceof SchemaNode.Option )
55-
return fromOption(node)
37+
return fromOption(node, name)
5638
if( node instanceof SchemaNode.Placeholder )
57-
return fromPlaceholder(node)
39+
return fromPlaceholder(node, name)
5840
if( node instanceof SchemaNode.Scope )
5941
return fromScope(node, name)
6042
throw new IllegalStateException()
6143
}
6244

63-
private static Map<String,?> fromOption(SchemaNode.Option node) {
45+
private static Map<String,?> fromOption(SchemaNode.Option node, String name) {
6446
final description = node.description().stripIndent(true).trim()
6547
final type = fromType(new ClassNode(node.type()))
6648

6749
return [
68-
type: 'Option',
50+
type: 'ConfigOption',
6951
spec: [
52+
name: name,
7053
description: description,
7154
type: type
7255
]
7356
]
7457
}
7558

76-
private static Map<String,?> fromPlaceholder(SchemaNode.Placeholder node) {
59+
private static Map<String,?> fromPlaceholder(SchemaNode.Placeholder node, String name) {
7760
final description = node.description().stripIndent(true).trim()
7861
final placeholderName = node.placeholderName()
7962
final scope = fromScope(node.scope())
8063

8164
return [
82-
type: 'Placeholder',
65+
type: 'ConfigPlaceholderScope',
8366
spec: [
67+
name: name,
8468
description: description,
8569
placeholderName: placeholderName,
8670
scope: scope.spec
@@ -90,34 +74,28 @@ class JsonRenderer {
9074

9175
private static Map<String,?> fromScope(SchemaNode.Scope node, String scopeName=null) {
9276
final description = node.description().stripIndent(true).trim()
93-
final children = node.children().collectEntries { name, child ->
94-
Map.entry(name, fromNode(child, name))
77+
final children = node.children().collect { name, child ->
78+
fromNode(child, name)
9579
}
9680

9781
return [
98-
type: 'Scope',
82+
type: 'ConfigScope',
9983
spec: [
100-
description: withLink(scopeName, description),
84+
name: scopeName,
85+
description: description,
10186
children: children
10287
]
10388
]
10489
}
10590

106-
private static String withLink(String scopeName, String description) {
107-
return scopeName
108-
? "$description\n\n[Read more](https://nextflow.io/docs/latest/reference/config.html#$scopeName)\n"
109-
: description
110-
}
111-
11291
private static Object fromType(ClassNode cn) {
11392
final name = Types.getName(cn.getTypeClass())
114-
if( cn.isUsingGenerics() ) {
93+
if( !cn.isGenericsPlaceHolder() && cn.getGenericsTypes() != null ) {
11594
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
11695
return [ name: name, typeArguments: typeArguments ]
11796
}
11897
else {
11998
return name
12099
}
121100
}
122-
123101
}

0 commit comments

Comments
 (0)