Skip to content

Commit e18016b

Browse files
authored
Merge branch '2.5.0dev' into update/examples
2 parents 1c36b83 + 9c9ad0f commit e18016b

File tree

9 files changed

+158
-124
lines changed

9 files changed

+158
-124
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
4. Updated the error message when the JSON schema file is invalid to include the full file name.
1616
5. The ANSI log setting of Nextflow is now used to determine whether or not the log should be monochrome. This setting will take priority over the `validation.monochromeLogs` configuration option.
1717
6. Updated the examples in the [examples](examples/) directory and added automatic checking for validity of these examples.
18+
7. Refactored logic for parsing the `jsonschema` validation result into a new `ValidationResult` class.
1819

1920
## Bug fixes
2021

2122
1. CSV and TSV files with trailing commas or tabs will now be properly sanitized. This fixes issues with CSV and TSV files that contained empty header columns.
2223
2. Unidentified parameters are no longer printed out on failure of the parameter validation. This is to prevent a bug where all parameters would be printed out on failure.
2324
3. Fixed an issue where default values of `null` weren't being set correctly in `samplesheetToList()`.
25+
4. Fixed an undocumented limit of `3MB` for `YAML` format samplesheets (new limit is `50MB`).
26+
5. Fixed issue where an empty string in a yaml for a file type string format would throw a Java error instead of reporting a proper validation error.
2427

2528
## Logging configuration
2629

src/main/groovy/nextflow/validation/ValidationExtension.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import nextflow.validation.samplesheet.SamplesheetConverter
1919
import nextflow.validation.summary.SummaryCreator
2020
import nextflow.validation.parameters.ParameterValidator
2121
import nextflow.validation.validators.JsonSchemaValidator
22+
import nextflow.validation.validators.ValidationResult
2223
import static nextflow.validation.utils.Colors.getLogColors
2324
import static nextflow.validation.utils.Common.getBasePath
2425
import static nextflow.validation.utils.Common.getLongestKeyLength
@@ -132,7 +133,8 @@ class ValidationExtension extends PluginExtensionPoint {
132133
} else {
133134
jsonObj = input
134135
}
135-
def List<String> errors = validator.validateObj(jsonObj, getBasePath(session.baseDir.toString(), schema))[0]
136+
def ValidationResult result = validator.validate(jsonObj, getBasePath(session.baseDir.toString(), schema))
137+
def List<String> errors = result.getErrors('object')
136138
if(exitOnError && errors != []) {
137139
def colors = getLogColors(config.monochromeLogs)
138140
def String msg = "${colors.red}${errors.join('\n')}${colors.reset}\n"

src/main/groovy/nextflow/validation/parameters/ParameterValidator.groovy

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import nextflow.validation.validators.JsonSchemaValidator
1414
import static nextflow.validation.utils.Colors.getLogColors
1515
import static nextflow.validation.utils.Common.getBasePath
1616
import static nextflow.validation.utils.Common.getValueFromJsonPointer
17+
import nextflow.validation.validators.ValidationResult
1718

1819
/**
1920
* @author : mirpedrol <[email protected]>
@@ -138,22 +139,23 @@ class ParameterValidator {
138139
def colors = getLogColors(config.monochromeLogs)
139140

140141
// Validate
141-
Tuple2<List<String>,List<String>> validationResult = validator.validate(paramsJSON, getBasePath(baseDir, schemaFilename))
142-
def validationErrors = validationResult[0]
143-
def unevaluatedParams = validationResult[1]
144-
this.errors.addAll(validationErrors)
142+
def ValidationResult validationResult = validator.validate(paramsJSON, getBasePath(baseDir, schemaFilename))
143+
def List<String> paramErrors = validationResult.getErrors('parameter')
144+
this.errors.addAll(paramErrors)
145145

146146
//=====================================================================//
147147
// Check for nextflow core params and unexpected params
148148
//=====================================================================//
149149
def List<String> unexpectedParams = []
150-
unevaluatedParams.each{ param ->
151-
def String dotParam = param.replaceAll("/", ".")
152-
if (NF_OPTIONS.contains(param)) {
153-
errors << "You used a core Nextflow option with two hyphens: '--${param}'. Please resubmit with '-${param}'".toString()
154-
}
155-
else if (!config.ignoreParams.any { dotParam == it || dotParam.startsWith(it + ".") } ) { // Check if an ignore param is present
156-
unexpectedParams << "* --${param.replaceAll("/", ".")}: ${getValueFromJsonPointer("/"+param, paramsJSON)}".toString()
150+
if(paramErrors.size() == 0) {
151+
validationResult.getUnevaluated().each{ param ->
152+
def String dotParam = param.replaceAll("/", ".")
153+
if (NF_OPTIONS.contains(param)) {
154+
errors << "You used a core Nextflow option with two hyphens: '--${param}'. Please resubmit with '-${param}'".toString()
155+
}
156+
else if (!config.ignoreParams.any { dotParam == it || dotParam.startsWith(it + ".") } ) { // Check if an ignore param is present
157+
unexpectedParams << "* --${param.replaceAll("/", ".")}: ${getValueFromJsonPointer("/"+param, paramsJSON)}".toString()
158+
}
157159
}
158160
}
159161

src/main/groovy/nextflow/validation/samplesheet/SamplesheetConverter.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import static nextflow.validation.utils.Common.hasDeepKey
1616
import nextflow.validation.config.ValidationConfig
1717
import nextflow.validation.exceptions.SchemaValidationException
1818
import nextflow.validation.validators.JsonSchemaValidator
19+
import nextflow.validation.validators.ValidationResult
1920

2021
/**
2122
* @author : mirpedrol <[email protected]>
@@ -98,8 +99,8 @@ class SamplesheetConverter {
9899
// Validate
99100
final validator = new JsonSchemaValidator(config)
100101
def JSONArray samplesheet = fileToJson(samplesheetFile, schemaFile) as JSONArray
101-
def Tuple2<List<String>,List<String>> validationResults = validator.validate(samplesheet, schemaFile.toString())
102-
def validationErrors = validationResults[0]
102+
def ValidationResult validationResult = validator.validate(samplesheet, schemaFile.toString())
103+
def validationErrors = validationResult.getErrors('field')
103104
if (validationErrors) {
104105
def msg = "${colors.red}The following errors have been detected in ${samplesheetFile.toString()}:\n\n" + validationErrors.join('\n').trim() + "\n${colors.reset}\n"
105106
log.error("Validation of samplesheet failed!")

src/main/groovy/nextflow/validation/utils/Files.groovy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package nextflow.validation.utils
22

33
import org.yaml.snakeyaml.Yaml
4+
import org.yaml.snakeyaml.LoaderOptions
45
import org.json.JSONArray
56
import org.json.JSONObject
67
import org.json.JSONPointer
@@ -85,7 +86,9 @@ public class Files {
8586
}
8687

8788
if(fileType == "yaml"){
88-
return new Yaml().load((file.text))
89+
def LoaderOptions yamlLoaderOptions = new LoaderOptions()
90+
yamlLoaderOptions.setCodePointLimit(50 * 1024 * 1024)
91+
return new Yaml(yamlLoaderOptions).load((file.text))
8992
}
9093
else if(fileType == "json"){
9194
return new JsonSlurper().parseText(file.text)

src/main/groovy/nextflow/validation/validators/JsonSchemaValidator.groovy

Lines changed: 9 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,11 @@ import dev.harrel.jsonschema.FormatEvaluatorFactory
1212
import dev.harrel.jsonschema.JsonNode
1313
import dev.harrel.jsonschema.providers.OrgJsonNode
1414

15-
import java.util.regex.Pattern
16-
import java.util.regex.Matcher
17-
18-
import static nextflow.validation.utils.Common.getValueFromJsonPointer
19-
import static nextflow.validation.utils.Common.findAllKeys
20-
import static nextflow.validation.utils.Common.kebabToCamel
21-
import static nextflow.validation.utils.Types.isInteger
2215
import nextflow.validation.config.ValidationConfig
2316
import nextflow.validation.exceptions.SchemaValidationException
2417
import nextflow.validation.validators.evaluators.CustomEvaluatorFactory
18+
import nextflow.validation.validators.ValidationResult
19+
import static nextflow.validation.utils.Common.getValueFromJsonPointer
2520

2621
/**
2722
* @author : nvnieuwk <[email protected]>
@@ -31,7 +26,6 @@ import nextflow.validation.validators.evaluators.CustomEvaluatorFactory
3126
public class JsonSchemaValidator {
3227

3328
private ValidatorFactory validator
34-
private Pattern uriPattern = Pattern.compile('^#/(\\d*)?/?(.*)$')
3529
private ValidationConfig config
3630

3731
JsonSchemaValidator(ValidationConfig config) {
@@ -42,10 +36,12 @@ public class JsonSchemaValidator {
4236
this.config = config
4337
}
4438

45-
private Tuple2<List<String>,List<String>> validateObject(JsonNode input, String validationType, Object rawJson, String schemaFileName) {
39+
private ValidationResult validateObject(JsonNode input, Object rawJson, String schemaFileName) {
4640
def JSONObject schema
41+
def String schemaString
4742
try {
48-
schema = new JSONObject(Files.readString(Path.of(schemaFileName)))
43+
schemaString = Files.readString(Path.of(schemaFileName))
44+
schema = new JSONObject(schemaString)
4945
} catch (org.json.JSONException e) {
5046
throw new SchemaValidationException("""Failed to load JSON schema (${schemaFileName}):
5147
${e.message}
@@ -64,106 +60,13 @@ public class JsonSchemaValidator {
6460
""")
6561
throw new SchemaValidationException("", [])
6662
}
67-
6863
def Validator.Result result = this.validator.validate(schema, input)
69-
def List<String> errors = []
70-
result.getErrors().each { error ->
71-
def String errorString = error.getError()
72-
73-
// Skip double error in the parameter schema
74-
if (errorString.startsWith("Value does not match against the schemas at indexes") && validationType == "parameter") {
75-
return
76-
}
77-
78-
def String instanceLocation = error.getInstanceLocation()
79-
def String value = getValueFromJsonPointer(instanceLocation, rawJson)
80-
if(config.maxErrValSize >= 1 && value.size() > config.maxErrValSize) {
81-
value = "${value[0..(config.maxErrValSize/2-1)]}...${value[-config.maxErrValSize/2..-1]}" as String
82-
}
83-
84-
// Return a standard error message for object validation
85-
if (validationType == "object") {
86-
errors.add("${instanceLocation ? instanceLocation + ' ' : ''}(${value}): ${errorString}" as String)
87-
return
88-
}
89-
90-
// Get the custom errorMessage if there is one and the validation errors are not about the content of the file
91-
def String schemaLocation = error.getSchemaLocation().replaceFirst(/^[^#]+/, "")
92-
def String customError = ""
93-
if (!errorString.startsWith("Validation of file failed:")) {
94-
customError = getValueFromJsonPointer("${schemaLocation}/errorMessage", schema) as String
95-
}
96-
97-
// Change some error messages to make them more clear
98-
def String keyword = error.getKeyword()
99-
if (keyword == "required") {
100-
def Matcher matcher = errorString =~ ~/\[\[([^\[\]]*)\]\]$/
101-
def String missingKeywords = matcher.findAll().flatten().last()
102-
errorString = "Missing required ${validationType}(s): ${missingKeywords}"
103-
}
104-
105-
def List<String> locationList = instanceLocation.split("/").findAll { it != "" } as List
106-
107-
def String printableError = "${validationType == 'field' ? '->' : '*'} ${errorString}" as String
108-
if (locationList.size() > 0 && isInteger(locationList[0]) && validationType == "field") {
109-
def Integer entryInteger = locationList[0] as Integer
110-
def String entryString = "Entry ${entryInteger + 1}" as String
111-
def String fieldError = "${errorString}" as String
112-
if(locationList.size() > 1) {
113-
fieldError = "Error for ${validationType} '${locationList[1..-1].join("/")}' (${value}): ${errorString}"
114-
}
115-
printableError = "-> ${entryString}: ${fieldError}" as String
116-
} else if (validationType == "parameter") {
117-
def String fieldName = locationList.join(".")
118-
if(fieldName != "") {
119-
printableError = "* --${fieldName} (${value}): ${errorString}" as String
120-
}
121-
}
122-
123-
if(customError != "") {
124-
printableError = printableError + " (${customError})"
125-
}
126-
127-
errors.add(printableError)
128-
129-
}
130-
def List<String> unevaluated = []
131-
if(errors.size() == 0) {
132-
unevaluated = getUnevaluated(result, rawJson)
133-
}
134-
return Tuple.tuple(errors, unevaluated)
135-
}
136-
137-
public Tuple2<List<String>,List<String>> validate(JSONArray input, String schemaFileName) {
138-
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
139-
return this.validateObject(jsonInput, "field", input, schemaFileName)
64+
return new ValidationResult(result, rawJson, schemaString, config)
14065
}
14166

142-
public Tuple2<List<String>,List<String>> validate(JSONObject input, String schemaFileName) {
67+
public ValidationResult validate(Object input, String schemaFileName) {
14368
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
144-
return this.validateObject(jsonInput, "parameter", input, schemaFileName)
69+
return this.validateObject(jsonInput, input, schemaFileName)
14570
}
14671

147-
public Tuple2<List<String>,List<String>> validateObj(Object input, String schemaFileName) {
148-
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
149-
return this.validateObject(jsonInput, "object", input, schemaFileName)
150-
}
151-
152-
public static List<String> getUnevaluated(Validator.Result result, Object rawJson) {
153-
def Set<String> evaluated = []
154-
result.getAnnotations().each{ anno ->
155-
if(anno.keyword in ["properties", "patternProperties", "additionalProperties"]){
156-
evaluated.addAll(
157-
anno.annotation.collect{ it ->
158-
"${anno.instanceLocation.toString()}/${it.toString()}".replaceAll("^/+", "")
159-
}
160-
)
161-
}
162-
}
163-
def Set<String> all_keys = []
164-
findAllKeys(rawJson, null, all_keys, '/')
165-
def unevaluated_ = all_keys - evaluated
166-
def unevaluated = unevaluated_.collect{ it -> !evaluated.contains(kebabToCamel(it)) ? it : null }
167-
return unevaluated - null
168-
}
16972
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package nextflow.validation.validators
2+
3+
import groovy.util.logging.Slf4j
4+
5+
import java.util.regex.Matcher
6+
7+
import org.json.JSONObject
8+
import dev.harrel.jsonschema.ValidatorFactory
9+
import dev.harrel.jsonschema.Validator
10+
11+
import nextflow.validation.config.ValidationConfig
12+
import static nextflow.validation.utils.Common.getValueFromJsonPointer
13+
import static nextflow.validation.utils.Common.findAllKeys
14+
import static nextflow.validation.utils.Common.kebabToCamel
15+
import static nextflow.validation.utils.Types.isInteger
16+
17+
18+
@Slf4j
19+
public class ValidationResult {
20+
21+
public Validator.Result result
22+
private Object rawInput
23+
private String schemaString
24+
private JSONObject schemaJson
25+
private ValidationConfig config
26+
27+
ValidationResult(Validator.Result result, Object rawInput, String schemaString, ValidationConfig config ) {
28+
this.result = result
29+
this.rawInput = rawInput
30+
this.schemaString = schemaString
31+
this.schemaJson = new JSONObject(schemaString)
32+
this.config = config
33+
}
34+
35+
public List<String> getUnevaluated() {
36+
def Set<String> evaluated = []
37+
this.result.getAnnotations().each{ anno ->
38+
if(anno.keyword in ["properties", "patternProperties", "additionalProperties"]){
39+
evaluated.addAll(
40+
anno.annotation.collect{ it ->
41+
"${anno.instanceLocation.toString()}/${it.toString()}".replaceAll("^/+", "")
42+
}
43+
)
44+
}
45+
}
46+
def Set<String> all_keys = []
47+
findAllKeys(this.rawInput, null, all_keys, '/')
48+
def unevaluated_ = all_keys - evaluated
49+
def unevaluated = unevaluated_.collect{ it -> !evaluated.contains(kebabToCamel(it)) ? it : null }
50+
return unevaluated - null
51+
}
52+
53+
public List<String> getErrors(String validationType) {
54+
def List<String> errors = []
55+
this.result.getErrors().each { error ->
56+
def String errorString = error.getError()
57+
58+
// Skip double error in the parameter schema
59+
if (errorString.startsWith("Value does not match against the schemas at indexes") && validationType == "parameter") {
60+
return
61+
}
62+
63+
def String instanceLocation = error.getInstanceLocation()
64+
def String value = getValueFromJsonPointer(instanceLocation, this.rawInput)
65+
if(config.maxErrValSize >= 1 && value.size() > config.maxErrValSize) {
66+
value = "${value[0..(config.maxErrValSize/2-1)]}...${value[-config.maxErrValSize/2..-1]}" as String
67+
}
68+
69+
// Return a standard error message for object validation
70+
if (validationType == "object") {
71+
errors.add("${instanceLocation ? instanceLocation + ' ' : ''}(${value}): ${errorString}" as String)
72+
return
73+
}
74+
75+
// Get the custom errorMessage if there is one and the validation errors are not about the content of the file
76+
def String schemaLocation = error.getSchemaLocation().replaceFirst(/^[^#]+/, "")
77+
def String customError = ""
78+
if (!errorString.startsWith("Validation of file failed:")) {
79+
customError = getValueFromJsonPointer("${schemaLocation}/errorMessage", this.schemaJson) as String
80+
}
81+
82+
// Change some error messages to make them more clear
83+
def String keyword = error.getKeyword()
84+
if (keyword == "required") {
85+
def Matcher matcher = errorString =~ ~/\[\[([^\[\]]*)\]\]$/
86+
def String missingKeywords = matcher.findAll().flatten().last()
87+
errorString = "Missing required ${validationType}(s): ${missingKeywords}"
88+
}
89+
90+
def List<String> locationList = instanceLocation.split("/").findAll { it != "" } as List
91+
92+
def String printableError = "${validationType == 'field' ? '->' : '*'} ${errorString}" as String
93+
if (locationList.size() > 0 && isInteger(locationList[0]) && validationType == "field") {
94+
def Integer entryInteger = locationList[0] as Integer
95+
def String entryString = "Entry ${entryInteger + 1}" as String
96+
def String fieldError = "${errorString}" as String
97+
if(locationList.size() > 1) {
98+
fieldError = "Error for ${validationType} '${locationList[1..-1].join("/")}' (${value}): ${errorString}"
99+
}
100+
printableError = "-> ${entryString}: ${fieldError}" as String
101+
} else if (validationType == "parameter") {
102+
def String fieldName = locationList.join(".")
103+
if(fieldName != "") {
104+
printableError = "* --${fieldName} (${value}): ${errorString}" as String
105+
}
106+
}
107+
108+
if(customError != "") {
109+
printableError = printableError + " (${customError})"
110+
}
111+
112+
errors.add(printableError)
113+
114+
}
115+
return errors
116+
}
117+
118+
}

src/main/groovy/nextflow/validation/validators/evaluators/ExistsEvaluator.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class ExistsEvaluator implements Evaluator {
3030
}
3131

3232
def String value = node.asString()
33-
def Path file = Nextflow.file(value) as Path
3433
def Boolean exists
34+
def Path file
3535

3636
try {
3737
file = Nextflow.file(value) as Path

0 commit comments

Comments
 (0)