Skip to content

Commit

Permalink
Merge pull request #39 from nextflow-io/feat/nested-params
Browse files Browse the repository at this point in the history
Feat/nested params
  • Loading branch information
nvnieuwk committed Aug 14, 2024
2 parents 5a24da2 + 36d5f9a commit f8512ea
Show file tree
Hide file tree
Showing 19 changed files with 1,307 additions and 283 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.nextflow
.nextflow.log*
.nextflow.pid
null

# Ignore Gradle build output directory
build
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

# Version 2.1.0

## Breaking changes

1. The minimum supported Nextflow version is now `23.10.0` instead of `22.10.0`

## New features

1. The plugin now fully supports nested parameters!
2. Added a config option `validation.parametersSchema` which can be used to set the parameters JSON schema in a config file. The default is `nextflow_schema.json`
3. The parameter summary log will now automatically show nested parameters.

## Help message changes

1. The use of the `paramsHelp()` function has now been deprecated in favor of a new built-in help message functionality. `paramsHelp()` has been updated to use the reworked help message creator. If you still want to use `paramsHelp()` for some reason in your pipeline, please add the `hideWarning:true` option to it to make sure the deprecation warning will not be shown.
2. Added new configuration values to support the new help message functionality:
- `validation.help.enabled`: Enables the checker for the help message parameters. The plugin will automatically show the help message when one of these parameters have been given and exit the pipeline. Default = `false`
- `validation.help.shortParameter`: The parameter to use for the compact help message. This help message will only contain top level parameters. Default = `help`
- `validation.help.fullParameter`: The parameter to use for the expanded help message. This help message will show all parameters no matter how deeply nested they are. Default = `helpFull`
- `validation.help.showHiddenParameter`: The parameter to use to also show all parameters with the `hidden: true` keyword in the schema. Default = `showHidden`
- `validation.help.showHidden`: Set this to `true` to show hidden parameters by default. This configuration option is overwritten by the value supplied to the parameter in `validation.help.showHiddenParameter`. Default = `false`
- `validation.help.beforeText`: Some custom text to add before the help message.
- `validation.help.afterText`: Some custom text to add after the help message.
- `validation.help.command`: An example command to add to the top of the help message
3. Added support for nested parameters to the help message. A detailed help message using `--help <parameter>` will now also contain all nested parameters. The parameter supplied to `--help` can be a nested parameter too (e.g. `--help top_parameter.nested_parameter.deeper_parameter`)
4. The help message now won't show empty parameter groups.
5. The help message will now automatically contain the three parameters used to get help messages.

## JSON schema fixes

1. The `defs` keyword is now deprecated in favor of the `$defs` keyword. This to follow the JSON schema guidelines. We will continue supporting `defs` for backwards compatibility.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repositories {

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(19)
}
}

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
nextflowVersion = 22.10.0
nextflowVersion = 23.10.0
groovyVersion = 3.0.14
23 changes: 22 additions & 1 deletion plugins/nf-schema/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,26 @@ dependencies {

// use JUnit 5 platform
test {
useJUnitPlatform()
useJUnitPlatform()
}

tasks.withType(Test) {
jvmArgs ([
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-opens=java.base/java.nio.file.spi=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED',
'--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.ch=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.fs=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.http=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.https=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.ftp=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.vm=ALL-UNNAMED',
])
}
255 changes: 255 additions & 0 deletions plugins/nf-schema/src/main/nextflow/validation/HelpMessage.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package nextflow.validation

import groovy.util.logging.Slf4j

import java.nio.file.Path

import nextflow.Session

/**
* This class contains methods to write a help message
*
* @author : nvnieuwk <[email protected]>
*
*/

@Slf4j
class HelpMessage {

private final ValidationConfig config
private final Map colors
private Integer hiddenParametersCount = 0
private Map<String,Map> paramsMap

// The length of the terminal
private Integer terminalLength = System.getenv("COLUMNS")?.toInteger() ?: 100

HelpMessage(ValidationConfig inputConfig, Session session) {
config = inputConfig
colors = Utils.logColours(config.monochromeLogs)
paramsMap = Utils.paramsLoad( Path.of(Utils.getSchemaPath(session.baseDir.toString(), config.parametersSchema)) )
addHelpParameters()
}

public String getShortHelpMessage(String param) {
def String helpMessage = ""
if (param) {
def List<String> paramNames = param.tokenize(".") as List<String>
def Map paramOptions = [:]
paramsMap.each { String group, Map groupParams ->
if (groupParams.containsKey(paramNames[0])) {
paramOptions = groupParams.get(paramNames[0]) as Map
}
}
if (paramNames.size() > 1) {
paramNames.remove(0)
paramNames.each {
paramOptions = (Map) paramOptions?.properties?[it] ?: [:]
}
}
if (!paramOptions) {
throw new Exception("Unable to create help message: Specified param '${param}' does not exist in JSON schema.")
}
if(paramOptions.containsKey("properties")) {
paramOptions.properties = removeHidden(paramOptions.properties)
}
helpMessage = getDetailedHelpString(param, paramOptions)
} else {
helpMessage = getGroupHelpString()
}
return helpMessage
}

public String getFullHelpMessage() {
return getGroupHelpString(true)
}

public String getBeforeText() {
def String beforeText = config.help.beforeText
if (config.help.command) {
beforeText += "Typical pipeline command:\n\n"
beforeText += " ${colors.cyan}${config.help.command}${colors.reset}\n\n"
}
return beforeText
}

public String getAfterText() {
def String afterText = ""
if (hiddenParametersCount > 0) {
afterText += " ${colors.dim}!! Hiding ${hiddenParametersCount} param(s), use the `--${config.help.showHiddenParameter}` parameter to show them !!${colors.reset}\n"
}
afterText += "-${colors.dim}----------------------------------------------------${colors.reset}-\n"
afterText += config.help.afterText
return afterText
}

//
// Get a detailed help string from one parameter
//
private String getDetailedHelpString(String paramName, Map paramOptions) {
def String helpMessage = "${colors.underlined}${colors.bold}--${paramName}${colors.reset}\n"
def Integer optionMaxChars = Utils.longestStringLength(paramOptions.keySet().collect { it == "properties" ? "options" : it } as List<String>)
for (option in paramOptions) {
def String key = option.key
if (key == "fa_icon" || (key == "type" && option.value == "object")) {
continue
}
if (key == "properties") {
def Map subParamsOptions = [:]
flattenNestedSchemaMap(option.value as Map).each { String subParam, Map value ->
subParamsOptions.put("${paramName}.${subParam}" as String, value)
}
def Integer maxChars = Utils.longestStringLength(subParamsOptions.keySet() as List<String>) + 1
def String subParamsHelpString = getHelpListParams(subParamsOptions, maxChars, paramName)
.collect {
" --" + it[4..it.length()-1]
}
.join("\n")
helpMessage += " " + colors.dim + "options".padRight(optionMaxChars) + ": " + colors.reset + "\n" + subParamsHelpString + "\n\n"
continue
}
def String value = option.value
if (value.length() > terminalLength) {
value = wrapText(value)
}
helpMessage += " " + colors.dim + key.padRight(optionMaxChars) + ": " + colors.reset + value + '\n'
}
return helpMessage
}

//
// Get the full help message for a grouped params structure in list format
//
private String getGroupHelpString(Boolean showNested = false) {
def String helpMessage = ""
def Map<String,Map> visibleParamsMap = paramsMap.collectEntries { key, Map value -> [key, removeHidden(value)]}
def Map<String,Map> parsedParams = showNested ? visibleParamsMap.collectEntries { key, Map value -> [key, flattenNestedSchemaMap(value)] } : visibleParamsMap
def Integer maxChars = Utils.paramsMaxChars(parsedParams) + 1
if (parsedParams.containsKey(null)) {
def Map ungroupedParams = parsedParams[null]
parsedParams.remove(null)
helpMessage += getHelpListParams(ungroupedParams, maxChars + 2).collect {
it[2..it.length()-1]
}.join("\n") + "\n\n"
}
parsedParams.each { String group, Map groupParams ->
def List<String> helpList = getHelpListParams(groupParams, maxChars)
if (helpList.size() > 0) {
helpMessage += "${colors.underlined}${colors.bold}${group}${colors.reset}\n" as String
helpMessage += helpList.join("\n") + "\n\n"
}
}
return helpMessage
}

private Map<String,Map> removeHidden(Map<String,Map> map) {
if (config.help.showHidden) {
return map
}
def Map<String,Map> returnMap = [:]
map.each { String key, Map value ->
if(!value.hidden) {
returnMap[key] = value
} else if(value.containsKey("properties")) {
value.properties = removeHidden(value.properties)
returnMap[key] = value
} else {
hiddenParametersCount++
}
}
return returnMap
}

//
// Get help for params in list format
//
private List<String> getHelpListParams(Map<String,Map> params, Integer maxChars, String parentParameter = "") {
def List helpMessage = []
def Integer typeMaxChars = Utils.longestStringLength(params.collect { key, value -> value.type instanceof String ? "[${value.type}]" : value.type as String})
for (String paramName in params.keySet()) {
def Map paramOptions = params.get(paramName) as Map
def String type = paramOptions.type instanceof String ? '[' + paramOptions.type + ']' : paramOptions.type as String
def String enumsString = ""
if (paramOptions.enum != null) {
def List enums = (List) paramOptions.enum
def String chopEnums = enums.join(", ")
if(chopEnums.length() > terminalLength){
chopEnums = chopEnums.substring(0, terminalLength-5)
chopEnums = chopEnums.substring(0, chopEnums.lastIndexOf(",")) + ", ..."
}
enumsString = " (accepted: " + chopEnums + ") "
}
def String description = paramOptions.description ? paramOptions.description as String + " " : ""
def defaultValue = paramOptions.default != null ? "[default: " + paramOptions.default.toString() + "] " : ''
def String nestedParamName = parentParameter ? parentParameter + "." + paramName : paramName
def String nestedString = paramOptions.properties ? "(This parameter has sub-parameters. Use '--help ${nestedParamName}' to see all sub-parameters) " : ""
def descriptionDefault = description + colors.dim + enumsString + defaultValue + colors.reset + nestedString
// Wrap long description texts
// Loosely based on https://dzone.com/articles/groovy-plain-text-word-wrap
if (descriptionDefault.length() > terminalLength){
descriptionDefault = wrapText(descriptionDefault)
}
helpMessage.add(" --" + paramName.padRight(maxChars) + colors.dim + type.padRight(typeMaxChars + 1) + colors.reset + descriptionDefault)
}
return helpMessage
}

//
// Flattens the schema params map so all nested parameters are shown as their full name
//
private Map<String,Map> flattenNestedSchemaMap(Map params) {
def Map returnMap = [:]
params.each { String key, Map value ->
if (value.containsKey("properties")) {
def flattenedMap = flattenNestedSchemaMap(value.properties)
flattenedMap.each { String k, Map v ->
returnMap.put(key + "." + k, v)
}
} else {
returnMap.put(key, value)
}
}
return returnMap
}

//
// This function adds the help parameters to the main parameters map as ungrouped parameters
//
private void addHelpParameters() {
if (!paramsMap.containsKey(null)) {
paramsMap[null] = [:]
}
paramsMap[null][config.help.shortParameter] = [
"type": ["boolean", "string"],
"description": "Show the help message for all top level parameters. When a parameter is given to `--${config.help.shortParameter}`, the full help message of that parameter will be printed."
]
paramsMap[null][config.help.fullParameter] = [
"type": "boolean",
"description": "Show the help message for all non-hidden parameters."
]
paramsMap[null][config.help.showHiddenParameter] = [
"type": "boolean",
"description": "Show all hidden parameters in the help message. This needs to be used in combination with `--${config.help.shortParameter}` or `--${config.help.fullParameter}`."
]
}

//
// Wrap too long text
//
private String wrapText(String text) {
def List olines = []
def String oline = ""
text.split(" ").each() { wrd ->
if ((oline.size() + wrd.size()) <= terminalLength) {
oline += wrd + " "
} else {
olines += oline
oline = wrd + " "
}
}
olines += oline
return olines.join("\n")
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public class JsonSchemaValidator {
}
}

def String[] locationList = instanceLocation.split("/").findAll { it != "" }
def List<String> locationList = instanceLocation.split("/").findAll { it != "" } as List

if (locationList.size() > 0 && Utils.isInteger(locationList[0]) && validationType == "field") {
def Integer entryInteger = locationList[0] as Integer
Expand All @@ -90,7 +90,7 @@ public class JsonSchemaValidator {
}
errors.add("-> ${entryString}: ${fieldError}" as String)
} else if (validationType == "parameter") {
def String fieldName = locationList.join("/")
def String fieldName = locationList.join(".")
if(fieldName != "") {
errors.add("* --${fieldName} (${value}): ${customError ?: errorString}" as String)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class SamplesheetConverter {

if (input instanceof Map) {
def List result = []
def LinkedHashMap properties = schema["properties"]
def Map properties = schema["properties"] as Map
def Set unusedKeys = input.keySet() - properties.keySet()

// Check for properties in the samplesheet that have not been defined in the schema
Expand Down Expand Up @@ -234,7 +234,7 @@ class SamplesheetConverter {

if (input instanceof Map) {
def Map result = [:]
def LinkedHashMap properties = schema["properties"]
def Map properties = schema["properties"] as Map
def Set unusedKeys = input.keySet() - properties.keySet()

// Check for properties in the samplesheet that have not been defined in the schema
Expand Down
Loading

0 comments on commit f8512ea

Please sign in to comment.