Skip to content

Commit aadebcc

Browse files
committed
feat(ValidationResult): refactor the code for handling the jsonschema library result to make error parsing and parsing of other elements of the validation result cleaner
1 parent caee8be commit aadebcc

File tree

6 files changed

+157
-116
lines changed

6 files changed

+157
-116
lines changed

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: 15 additions & 100 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,25 @@ 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)
64+
return new ValidationResult(result, rawJson, schemaString, config)
13565
}
13666

137-
public Tuple2<List<String>,List<String>> validate(JSONArray input, String schemaFileName) {
67+
/*
68+
public ValidationResult validate(JSONArray input, String schemaString) {
13869
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
139-
return this.validateObject(jsonInput, "field", input, schemaFileName)
70+
return this.validateObject(jsonInput, input, schemaString)
14071
}
14172
142-
public Tuple2<List<String>,List<String>> validate(JSONObject input, String schemaFileName) {
73+
public ValidationResult validate(JSONObject input, String schemaString) {
14374
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
144-
return this.validateObject(jsonInput, "parameter", input, schemaFileName)
75+
return this.validateObject(jsonInput, input, schemaString)
14576
}
77+
*/
14678

147-
public Tuple2<List<String>,List<String>> validateObj(Object input, String schemaFileName) {
79+
public ValidationResult validate(Object input, String schemaFileName) {
14880
def JsonNode jsonInput = new OrgJsonNode.Factory().wrap(input)
149-
return this.validateObject(jsonInput, "object", input, schemaFileName)
81+
return this.validateObject(jsonInput, input, schemaFileName)
15082
}
15183

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-
}
16984
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
println "${error.getError()}: '${error.getKeyword()}' at '${error.getEvaluationPath()}'"
57+
def String errorString = error.getError()
58+
59+
// Skip double error in the parameter schema
60+
if (errorString.startsWith("Value does not match against the schemas at indexes") && validationType == "parameter") {
61+
return
62+
}
63+
64+
def String instanceLocation = error.getInstanceLocation()
65+
def String value = getValueFromJsonPointer(instanceLocation, this.rawInput)
66+
if(config.maxErrValSize >= 1 && value.size() > config.maxErrValSize) {
67+
value = "${value[0..(config.maxErrValSize/2-1)]}...${value[-config.maxErrValSize/2..-1]}" as String
68+
}
69+
70+
// Return a standard error message for object validation
71+
if (validationType == "object") {
72+
errors.add("${instanceLocation ? instanceLocation + ' ' : ''}(${value}): ${errorString}" as String)
73+
return
74+
}
75+
76+
// Get the custom errorMessage if there is one and the validation errors are not about the content of the file
77+
def String schemaLocation = error.getSchemaLocation().replaceFirst(/^[^#]+/, "")
78+
def String customError = ""
79+
if (!errorString.startsWith("Validation of file failed:")) {
80+
customError = getValueFromJsonPointer("${schemaLocation}/errorMessage", this.schemaJson) as String
81+
}
82+
83+
// Change some error messages to make them more clear
84+
def String keyword = error.getKeyword()
85+
if (keyword == "required") {
86+
def Matcher matcher = errorString =~ ~/\[\[([^\[\]]*)\]\]$/
87+
def String missingKeywords = matcher.findAll().flatten().last()
88+
errorString = "Missing required ${validationType}(s): ${missingKeywords}"
89+
}
90+
91+
def List<String> locationList = instanceLocation.split("/").findAll { it != "" } as List
92+
93+
def String printableError = "${validationType == 'field' ? '->' : '*'} ${errorString}" as String
94+
if (locationList.size() > 0 && isInteger(locationList[0]) && validationType == "field") {
95+
def Integer entryInteger = locationList[0] as Integer
96+
def String entryString = "Entry ${entryInteger + 1}" as String
97+
def String fieldError = "${errorString}" as String
98+
if(locationList.size() > 1) {
99+
fieldError = "Error for ${validationType} '${locationList[1..-1].join("/")}' (${value}): ${errorString}"
100+
}
101+
printableError = "-> ${entryString}: ${fieldError}" as String
102+
} else if (validationType == "parameter") {
103+
def String fieldName = locationList.join(".")
104+
if(fieldName != "") {
105+
printableError = "* --${fieldName} (${value}): ${errorString}" as String
106+
}
107+
}
108+
109+
if(customError != "") {
110+
printableError = printableError + " (${customError})"
111+
}
112+
113+
errors.add(printableError)
114+
115+
}
116+
return errors
117+
}
118+
119+
}

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)