Skip to content

Commit 9c9ad0f

Browse files
authored
Merge pull request #168 from nextflow-io/refactor-validation-result
Refactor validation result handling
2 parents ff121fd + 73eb6be commit 9c9ad0f

File tree

7 files changed

+151
-122
lines changed

7 files changed

+151
-122
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
3. Rework the deprecated `paramsHelp()` function to allow pipeline developers a fully fledged alternative to the help messages created via the configuration options. This change enables the use of dynamic help message with the strict configuration syntax introduced in Nextflow 25.04.0.
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.
17+
6. Refactored logic for parsing the `jsonschema` validation result into a new `ValidationResult` class.
1718

1819
## Bug fixes
1920

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/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/SchemaEvaluator.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package nextflow.validation.validators.evaluators
22

3+
import org.json.JSONObject
34
import dev.harrel.jsonschema.Evaluator
45
import dev.harrel.jsonschema.EvaluationContext
56
import dev.harrel.jsonschema.JsonNode
@@ -12,6 +13,7 @@ import static nextflow.validation.utils.Common.getBasePath
1213
import static nextflow.validation.utils.Files.fileToJson
1314
import nextflow.validation.config.ValidationConfig
1415
import nextflow.validation.validators.JsonSchemaValidator
16+
import nextflow.validation.validators.ValidationResult
1517

1618
/**
1719
* @author : nvnieuwk <[email protected]>
@@ -54,8 +56,8 @@ class SchemaEvaluator implements Evaluator {
5456
def Object json = fileToJson(file, Path.of(schemaFull))
5557
def validator = new JsonSchemaValidator(config)
5658

57-
def Tuple2<List<String>,List<String>> validationResult = validator.validate(json, schemaFull)
58-
def validationErrors = validationResult[0]
59+
def ValidationResult validationResult = validator.validate(json, schemaFull)
60+
def List<String> validationErrors = validationResult.getErrors((json instanceof JSONObject) ? "parameter" : "field")
5961
if (validationErrors) {
6062
def List<String> errors = ["Validation of file failed:"] + validationErrors.collect { "\t${it}" as String}
6163
return Evaluator.Result.failure(errors.join("\n"))

0 commit comments

Comments
 (0)