Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add validation with nested schemas #15

Closed
wants to merge 2 commits into from
Closed
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 @@ -51,7 +51,6 @@ class SchemaValidator extends PluginExtensionPoint {
'quiet',
'syslog',
'v',
'version',

// Options for `nextflow run` command
'ansi',
Expand Down Expand Up @@ -183,6 +182,9 @@ class SchemaValidator extends PluginExtensionPoint {
expectedParams.push('fail_unrecognised_params')
expectedParams.push('lenient_mode')

def Boolean lenient_mode = params.lenient_mode ? params.lenient_mode : false
def Boolean fail_unrecognised_params = params.fail_unrecognised_params ? params.fail_unrecognised_params : false

for (String specifiedParam in specifiedParamKeys) {
// nextflow params
if (NF_OPTIONS.contains(specifiedParam)) {
Expand All @@ -198,7 +200,7 @@ class SchemaValidator extends PluginExtensionPoint {
def specifiedParamLowerCase = specifiedParam.replace("-", "").toLowerCase()
def isCamelCaseBug = (specifiedParam.contains("-") && !expectedParams.contains(specifiedParam) && expectedParamsLowerCase.contains(specifiedParamLowerCase))
if (!expectedParams.contains(specifiedParam) && !params_ignore.contains(specifiedParam) && !isCamelCaseBug) {
if (params.fail_unrecognised_params) {
if (fail_unrecognised_params) {
errors << "* --${specifiedParam}: ${paramsJSON[specifiedParam]}".toString()
} else {
warnings << "* --${specifiedParam}: ${paramsJSON[specifiedParam]}".toString()
Expand All @@ -224,7 +226,7 @@ class SchemaValidator extends PluginExtensionPoint {

// Validate
try {
if (params.lenient_mode) {
if (lenient_mode) {
// Create new validator with LENIENT mode
Validator validator = Validator.builder()
.primitiveValidationStrategy(PrimitiveValidationStrategy.LENIENT)
Expand All @@ -248,7 +250,7 @@ class SchemaValidator extends PluginExtensionPoint {
}

//=====================================================================//
// Look for other schemas to validate
// Look for other schemas or nested params to validate
for (group in schemaParams) {
def Map properties = (Map) group.value['properties']
for (p in properties) {
Expand All @@ -269,7 +271,9 @@ class SchemaValidator extends PluginExtensionPoint {
if (validateFile(params, key, fileContent, schema_name, baseDir)) {
log.debug "Validation passed: '$key': '$file_path' with '$schema_name'"
}
}
}/* else if (property.containesKey('properties')) {
validateNestedSchema(params, properties)
}*/
}
}
}
Expand Down Expand Up @@ -300,10 +304,13 @@ class SchemaValidator extends PluginExtensionPoint {
def List expectedParams = (List) enumsTuple[0]
def Map enums = (Map) enumsTuple[1]

// Declare variables
def Boolean lenient_mode = params.lenient_mode ? params.lenient_mode : false

//=====================================================================//
// Validate
try {
if (params.lenient_mode) {
if (lenient_mode) {
// Create new validator with LENIENT mode
Validator validator = Validator.builder()
.primitiveValidationStrategy(PrimitiveValidationStrategy.LENIENT)
Expand All @@ -327,6 +334,106 @@ class SchemaValidator extends PluginExtensionPoint {
return true
}

//
// Function to validate a nested schema
//
/*void validateNestedSchema(Map params, Map schema) {

def String baseDir = session.baseDir

// Clean the parameters
def cleanedParams = cleanParameters(params)
// Convert to JSONObject
def paramsJSON = new JSONObject(new JsonBuilder(cleanedParams).toString())

// Collect expected parameters from the schema
def enumsTuple = collectEnums(schemaParams) //TODO: run this function without groups for loop
def List expectedParams = (List) enumsTuple[0]
def Map enums = (Map) enumsTuple[1]

def Boolean lenient_mode = params.lenient_mode ? params.lenient_mode : false
def Boolean fail_unrecognised_params = params.fail_unrecognised_params ? params.fail_unrecognised_params : false

def specifiedParamKeys = params.keySet()

for (String specifiedParam in specifiedParamKeys) {
// unexpected params
def String schema_ignore_params = params.schema_ignore_params
def List params_ignore = schema_ignore_params ? schema_ignore_params.split(',') + 'schema_ignore_params' as List : []
def expectedParamsLowerCase = expectedParams.collect{ it ->
def String p = it
p.replace("-", "").toLowerCase()
}
def specifiedParamLowerCase = specifiedParam.replace("-", "").toLowerCase()
def isCamelCaseBug = (specifiedParam.contains("-") && !expectedParams.contains(specifiedParam) && expectedParamsLowerCase.contains(specifiedParamLowerCase))
if (!expectedParams.contains(specifiedParam) && !params_ignore.contains(specifiedParam) && !isCamelCaseBug) {
if (fail_unrecognised_params) {
errors << "* --${specifiedParam}: ${paramsJSON[specifiedParam]}".toString()
} else {
warnings << "* --${specifiedParam}: ${paramsJSON[specifiedParam]}".toString()
}
}
}

// check for warnings
if( this.hasWarnings() ) {
def msg = "The following invalid input values have been detected:\n\n" + this.getWarnings().join('\n').trim() + "\n\n"
log.warn(msg)
}

// Colors
def Boolean monochrome_logs = params.monochrome_logs
def colors = logColours(monochrome_logs)

// Validate
try {
if (lenient_mode) {
// Create new validator with LENIENT mode
Validator validator = Validator.builder()
.primitiveValidationStrategy(PrimitiveValidationStrategy.LENIENT)
.build();
validator.performValidation(schema, paramsJSON);
} else {
schema.validate(paramsJSON)
}
if (this.hasErrors()) {
// Needed when fail_unrecognised_params is true
def msg = "${colors.red}The following invalid input values have been detected:\n\n" + this.getErrors().join('\n').trim() + "\n${colors.reset}\n"
log.error("ERROR: Validation of pipeline parameters failed!")
throw new SchemaValidationException(msg, this.getErrors())
}
} catch (ValidationException e) {
JSONObject exceptionJSON = (JSONObject) e.toJSON()
collectErrors(exceptionJSON, paramsJSON, enums)
def msg = "${colors.red}The following invalid input values have been detected:\n\n" + this.getErrors().join('\n').trim() + "\n${colors.reset}\n"
log.error("ERROR: Validation of pipeline parameters failed!")
throw new SchemaValidationException(msg, this.getErrors())
}

//=====================================================================//
// Look for other schemas or nested parameters to validate
for (p in schema) {
def String key = (String) p.key
def Map property = properties[key] as Map
if (property.containsKey('schema')) {
def String schema_name = property['schema']
def Path file_path = Nextflow.file(params[key]) as Path
def String fileType = SamplesheetConverter.getFileType(file_path)
def String delimiter = fileType == "csv" ? "," : fileType == "tsv" ? "\t" : null
def List<Map<String,String>> fileContent
if(fileType == "yaml"){
fileContent = new Yaml().load((file_path.text))
}
else {
fileContent = file_path.splitCsv(header:true, strip:true, sep:delimiter)
}
if (validateFile(params, key, fileContent, schema_name, baseDir)) {
log.debug "Validation passed: '$key': '$file_path' with '$schema_name'"
}
}
}

}*/

//
// Function to collect enums (options) of a parameter and expected parameters (present in the schema)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import org.pf4j.PluginDescriptorFinder
import spock.lang.Shared
import test.Dsl2Spec
import test.OutputCapture

import nextflow.cli.CmdRun
import java.nio.file.Files
import nextflow.Session
import nextflow.Global


/**
* @author : jorge <jorge.aguilera@seqera.io>
*
Expand All @@ -20,6 +27,9 @@ class PluginExtensionMethodsTest extends Dsl2Spec{
@Rule
OutputCapture capture = new OutputCapture()

Session getSession(){
Global.getSession() as Session
}

@Shared String pluginsMode

Expand Down Expand Up @@ -374,6 +384,75 @@ class PluginExtensionMethodsTest extends Dsl2Spec{
!stdout
}

//
// Nested parameters tests
//

def 'should validate a schema with nested params' () {
given:
def schema = Path.of('src/testResources/nested_parameters_schema.json').toAbsolutePath().toString()
def SCRIPT_TEXT = """
include { validateParameters } from 'plugin/nf-validation'

validateParameters('$schema')
"""
and:
def ENV = [
NXF_PARAMS_FILE:'src/testResources/nested_paramteres.yml',
]

when:
dsl_eval(SCRIPT_TEXT)
def stdout = capture
.toString()
.readLines()
.findResults {it.contains('WARN nextflow.validation.SchemaValidator') || it.startsWith('* --') ? it : null }

then:
noExceptionThrown()
!stdout
}

def 'should find validation errors in nested param' () {
given:
def schema = Path.of('src/testResources/nexted_parameters_fail_schema.json').toAbsolutePath().toString()
def folder = Files.createTempDirectory('test')
def YAML = '''
foo: bar
baz:
qux: corge
grault: garply
'''.stripIndent()
def SCRIPT_TEXT = """
include { validateParameters } from 'plugin/nf-validation'

validateParameters('$schema')

println(params.baz)
println(params.foo)
"""

when:
def file = folder.resolve('params.yaml')
file.text = YAML
and:
def cmd = new CmdRun(sysEnv: [NXF_PARAMS_FILE: file.toString()])
def params = cmd.parsedParams()
session.params = params
and:
dsl_eval(SCRIPT_TEXT)
def stdout = capture
.toString()
.readLines()
.findResults {it.contains('WARN nextflow.validation.SchemaValidator') || it.startsWith('* --') ? it : null }

then:
//params.foo == "bar"
def error = thrown(SchemaValidationException)
//error.message == "The following invalid input values have been detected:\n\n* --publish_dir_mode: 'incorrect' is not a valid choice (Available choices (5 of 6): symlink, rellink, link, copy, copyNoFollow, ... )\n\n"
//!stdout
}

//
// --help argument tests
//
Expand Down
4 changes: 4 additions & 0 deletions plugins/nf-validation/src/testResources/nested_parameters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
foo: bar
baz:
qux: corge
grault: garply
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/nf-core/testpipeline/master/nextflow_schema.json",
"title": "nf-core/testpipeline pipeline parameters",
"description": "this is a test",
"type": "object",
"definitions": {
"nested parameters": {
"title": "Some nested parameters",
"type": "object",
"properties": {
"foo": {
"type": "string",
"default": "bar" },
"baz": {
"type": "object",
"properties": {
"qux": {
"type": "string",
"default": "corge"
},
"grault": {
"type": "string",
"default": "garply"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/nf-core/testpipeline/master/nextflow_schema.json",
"title": "nf-core/testpipeline pipeline parameters",
"description": "this is a test",
"type": "object",
"definitions": {
"nested parameters": {
"title": "Some nested parameters",
"type": "object",
"properties": {
"foo": {
"type": "integer",
"default": 2
},
"baz": {
"type": "object",
"properties": {
"qux": {
"type": "integer",
"default": 1
},
"grault": {
"type": "string",
"default": "garply"
}
}
}
}
}
}
}