From bf3a441628200c93de74598196d65a011e8def0d Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 11 Oct 2024 23:01:59 +0200 Subject: [PATCH] Add more out formats to config command Signed-off-by: Paolo Di Tommaso --- .../main/groovy/nextflow/cli/CmdConfig.groovy | 56 +++++++++--- .../groovy/nextflow/util/ConfigHelper.groovy | 37 ++++++++ .../groovy/nextflow/cli/CmdConfigTest.groovy | 81 +++++++++++++++++ .../nextflow/util/ConfigHelperTest.groovy | 88 +++++++++++++++++++ 4 files changed, 252 insertions(+), 10 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy index 5ff2156b18..e120653127 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy @@ -41,6 +41,8 @@ class CmdConfig extends CmdBase { static final public NAME = 'config' + static final List FORMATS = ['flat','properties','canonical','json','yaml'] + @Parameter(description = 'project name') List args = [] @@ -50,10 +52,12 @@ class CmdConfig extends CmdBase { @Parameter(names=['-profile'], description = 'Choose a configuration profile') String profile - @Parameter(names = '-properties', description = 'Prints config using Java properties notation') + @Deprecated + @Parameter(names = '-properties', description = 'Prints config using Java properties notation (deprecated: use `-o properties` instead)') boolean printProperties - @Parameter(names = '-flat', description = 'Print config using flat notation') + @Deprecated + @Parameter(names = '-flat', description = 'Print config using flat notation (deprecated: use `-o flat` instead)') boolean printFlatten @Parameter(names = '-sort', description = 'Sort config attributes') @@ -62,6 +66,9 @@ class CmdConfig extends CmdBase { @Parameter(names = '-value', description = 'Print the value of a config option, or fail if the option is not defined') String printValue + @Parameter(names = ['-o','-output'], description = 'Print the config using the specified format: canonical,properties,flat,json,yaml') + String outputFormat + @Override String getName() { NAME } @@ -79,13 +86,22 @@ class CmdConfig extends CmdBase { } if( printProperties && printFlatten ) - throw new AbortOperationException("Option `-flat` and `-properties` conflicts") + throw new AbortOperationException("Option `-flat` and `-properties` conflicts each other") if ( printValue && printFlatten ) - throw new AbortOperationException("Option `-value` and `-flat` conflicts") + throw new AbortOperationException("Option `-value` and `-flat` conflicts each other") if ( printValue && printProperties ) - throw new AbortOperationException("Option `-value` and `-properties` conflicts") + throw new AbortOperationException("Option `-value` and `-properties` conflicts each other") + + if( printValue && outputFormat ) + throw new AbortOperationException("Option `-value` and `-output` conflicts each other") + + if( printFlatten ) + outputFormat = 'flat' + + if( printProperties ) + outputFormat = 'properties' final builder = new ConfigBuilder() .setShowClosures(true) @@ -97,18 +113,31 @@ class CmdConfig extends CmdBase { final config = builder.buildConfigObject() - if( printProperties ) { + if( printValue ) { + printValue0(config, printValue, stdout) + } + else if( outputFormat=='properties' ) { printProperties0(config, stdout) } - else if( printFlatten ) { + else if( outputFormat=='flat' ) { printFlatten0(config, stdout) } - else if( printValue ) { - printValue0(config, printValue, stdout) + else if( outputFormat=='yaml' ) { + printYaml0(config, stdout) } - else { + else if( outputFormat=='json') { + printJson0(config, stdout) + } + else if( !outputFormat || outputFormat=='canonical' ) { printCanonical0(config, stdout) } + else { + def msg = "Unknown output format: $outputFormat" + def suggest = FORMATS.closest(outputFormat) + if( suggest ) + msg += " - did you mean '${suggest.first()}' instead?" + throw new AbortOperationException(msg) + } for( String msg : builder.warnings ) log.warn(msg) @@ -172,6 +201,13 @@ class CmdConfig extends CmdBase { config.writeTo( writer ) } + @PackageScope void printJson0(ConfigObject config, OutputStream output) { + output << ConfigHelper.toJsonString(config, sort) << '\n' + } + + @PackageScope void printYaml0(ConfigObject config, OutputStream output) { + output << ConfigHelper.toYamlString(config, sort) + } Path getBaseDir(String path) { diff --git a/modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy b/modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy index f128585a71..ad63c2f81f 100644 --- a/modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy @@ -18,10 +18,14 @@ package nextflow.util import java.nio.file.Path +import groovy.json.JsonOutput import groovy.transform.CompileStatic import groovy.transform.PackageScope import groovy.util.logging.Slf4j import org.codehaus.groovy.runtime.InvokerHelper +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml + /** * Helper method to handle configuration object * @@ -301,6 +305,19 @@ class ConfigHelper { flattenFormat(map.toConfigObject(), sort) } + static String toJsonString(ConfigObject config, boolean sort=false) { + final copy = normaliseConfig(config) + JsonOutput.prettyPrint(JsonOutput.toJson(sort ? deepSort(copy) : copy)) + } + + static String toYamlString(ConfigObject config, boolean sort=false) { + final copy = normaliseConfig(config) + final options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // Use block style instead of inline + final yaml = new Yaml(options) + return yaml.dump(sort ? deepSort(copy) : copy); + } + static boolean isValidIdentifier(String s) { // an empty or null string cannot be a valid identifier if (s == null || s.length() == 0) { @@ -321,7 +338,27 @@ class ConfigHelper { return true; } + private static Map deepSort(Map map) { + Map sortedMap = new TreeMap<>(map); // Sort current map + for (Map.Entry entry : sortedMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Map) { + // Recursively sort nested map + sortedMap.put(entry.getKey(), deepSort((Map) value)); + } + } + return sortedMap; + } + private static Map normaliseConfig(ConfigObject config) { + final result = new LinkedHashMap() + for( Map.Entry it : config ) { + if( it.value instanceof Map && !it.value ) + continue + result.put(it.key, it.value) + } + return result + } } diff --git a/modules/nextflow/src/test/groovy/nextflow/cli/CmdConfigTest.groovy b/modules/nextflow/src/test/groovy/nextflow/cli/CmdConfigTest.groovy index 232332b52b..cce8f9b0fd 100644 --- a/modules/nextflow/src/test/groovy/nextflow/cli/CmdConfigTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/cli/CmdConfigTest.groovy @@ -90,6 +90,7 @@ class CmdConfigTest extends Specification { .stripIndent().leftTrim() } + def 'should canonical notation' () { given: @@ -196,6 +197,86 @@ class CmdConfigTest extends Specification { } + def 'should print config using json' () { + given: + ByteArrayOutputStream buffer + ConfigObject config + def cmd = new CmdConfig(sort: true) + + when: + buffer = new ByteArrayOutputStream() + + config = new ConfigObject() + config.process.executor = 'slurm' + config.process.queue = 'long' + config.docker.enabled = true + config.dummy = new ConfigObject() // <-- empty config object should not be print + config.mail.from = 'yo@mail.com' + config.mail.smtp.host = 'mail.com' + config.mail.smtp.port = 25 + config.mail.smtp.user = 'yo' + + cmd.printJson0(config, buffer) + then: + buffer.toString() == '''\ + { + "docker": { + "enabled": true + }, + "mail": { + "from": "yo@mail.com", + "smtp": { + "host": "mail.com", + "port": 25, + "user": "yo" + } + }, + "process": { + "executor": "slurm", + "queue": "long" + } + } + ''' + .stripIndent() + } + + def 'should print config using yaml' () { + given: + ByteArrayOutputStream buffer + ConfigObject config + def cmd = new CmdConfig(sort: true) + + when: + buffer = new ByteArrayOutputStream() + + config = new ConfigObject() + config.process.executor = 'slurm' + config.process.queue = 'long' + config.docker.enabled = true + config.dummy = new ConfigObject() // <-- empty config object should not be print + config.mail.from = 'yo@mail.com' + config.mail.smtp.host = 'mail.com' + config.mail.smtp.port = 25 + config.mail.smtp.user = 'yo' + + cmd.printYaml0(config, buffer) + then: + buffer.toString() == '''\ + docker: + enabled: true + mail: + from: yo@mail.com + smtp: + host: mail.com + port: 25 + user: yo + process: + executor: slurm + queue: long + ''' + .stripIndent() + } + def 'should parse config file' () { given: def folder = Files.createTempDirectory('test') diff --git a/modules/nextflow/src/test/groovy/nextflow/util/ConfigHelperTest.groovy b/modules/nextflow/src/test/groovy/nextflow/util/ConfigHelperTest.groovy index b92ec8c308..7674b97054 100644 --- a/modules/nextflow/src/test/groovy/nextflow/util/ConfigHelperTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/util/ConfigHelperTest.groovy @@ -245,6 +245,94 @@ class ConfigHelperTest extends Specification { } + def 'should render config as json' () { + given: + def config = new ConfigObject() + config.process.queue = 'long' + config.process.executor = 'slurm' + config.docker.enabled = true + config.zeta.'quoted-attribute'.foo = 1 + + when: + def result = ConfigHelper.toJsonString(config, true) + then: + result == ''' + { + "docker": { + "enabled": true + }, + "process": { + "executor": "slurm", + "queue": "long" + }, + "zeta": { + "quoted-attribute": { + "foo": 1 + } + } + } + '''.stripIndent().trim() + + + when: + result = ConfigHelper.toJsonString(config, false) + then: + result == ''' + { + "process": { + "queue": "long", + "executor": "slurm" + }, + "docker": { + "enabled": true + }, + "zeta": { + "quoted-attribute": { + "foo": 1 + } + } + } + '''.stripIndent().trim() + } + + def 'should render config as yaml' () { + given: + def config = new ConfigObject() + config.process.queue = 'long' + config.process.executor = 'slurm' + config.docker.enabled = true + config.zeta.'quoted-attribute'.foo = 1 + + when: + def result = ConfigHelper.toYamlString(config, true) + then: + result == '''\ + docker: + enabled: true + process: + executor: slurm + queue: long + zeta: + quoted-attribute: + foo: 1 + '''.stripIndent() + + + when: + result = ConfigHelper.toYamlString(config, false) + then: + result == '''\ + process: + queue: long + executor: slurm + docker: + enabled: true + zeta: + quoted-attribute: + foo: 1 + '''.stripIndent() + } + def 'should verify valid identifiers' () { expect: