diff --git a/lib/Validator.js b/lib/Validator.js index b526a823..248c91d9 100644 --- a/lib/Validator.js +++ b/lib/Validator.js @@ -57,9 +57,13 @@ class Validator { if (!todos || !todos.length) { return; } + // todos is an array of strings. + const slotType = this.getSlotType(slotDefinition);// LinkML schema.type object - const slotType = this.getSlotType(slotDefinition); - + // Slot type could be: + // a number, string, date ... + // null - if it is only a menu + // both date and NullValueList if (slotType?.uri === 'xsd:date') { for (const todo of todos) { if (todo.substring(0, 2) === '>=') { @@ -70,16 +74,11 @@ class Validator { } } - for (const def of slotDefinition.any_of || []) { - processTodos(def, todos); - } - for (const def of slotDefinition.all_of || []) { - processTodos(def, todos); - } - for (const def of slotDefinition.exactly_one_of || []) { - processTodos(def, todos); - } - for (const def of slotDefinition.none_of || []) { + // Cycle through each slotDefinition any_of etc. object entries and get + // the datatype of its .range (or recurse) and in LinkML fashion attach + // minimum_value and maximum_value to the slotDefinition OR its any_of + // etc array BASED ON top level todos. E.g. inheriting min/max criteria. + for (const def of slotDefinition.any_of ?? slotDefinition.all_of ?? slotDefinition.exactly_one_of ?? slotDefinition.none_of ?? []) { processTodos(def, todos); } }; @@ -94,13 +93,15 @@ class Validator { */ this.#dependantMinimumValuesMap = new Map(); this.#dependantMaximumValuesMap = new Map(); + const RE_TODOS = new RegExp(/^([><])={(.*?)}$/); + for (const slotDefinition of Object.values(this.#targetClassInducedSlots)) { const { todos } = slotDefinition; if (!todos || !todos.length) { continue; } for (const todo of todos) { - const match = todo.match(/^([><])={(.*?)}$/); + const match = todo.match(RE_TODOS); if (match == null) { continue; } @@ -140,7 +141,7 @@ class Validator { this.#valueValidatorMap = new Map(); } - /* This returns a single primitve data type for a slot - a decimal, date, + /* This returns a single primitive data type for a slot - a decimal, date, string etc. or possibly an enumeration. Enumerations are handled separately however (by const slotEnum = ...). Slots either use "range" attribute, OR they use any_of or exactly_one_of etc. attribute expression @@ -164,6 +165,12 @@ class Validator { return slotType; } + /* A validation function is prepared and cached for every slot presented, so + that validation throug rows of data can make use of already established + validator for each column. Particular columns may have sensitivity to other + column values (via min/max references) or special {today} e.g. so + + */ getValidatorForSlot(slot, options = {}) { const { cacheKey, inheritedRange } = options; if (typeof cacheKey === 'string' && this.#valueValidatorMap.has(cacheKey)) { @@ -177,11 +184,13 @@ class Validator { slotDefinition = slot; } + // This digs down into any_of, all_of etc to find first date, number etc. + // slotType is LinkML schema.type object if (!slotDefinition.range && inheritedRange) { slotDefinition.range = inheritedRange; } - const slotType = this.getSlotType(slotDefinition); + const slotType = this.getSlotType(slotDefinition); // LinkML schema.type object const slotEnum = this.#schema.enums?.[slotDefinition.range]; const slotPermissibleValues = Object.values( @@ -194,11 +203,13 @@ class Validator { // TEST CASE: // if (slotDefinition.name == "sample_collection_date") // console.log("any_of", DEBUG INFO) + // inheritedRange comes from original slot, so it might be a date or number + menu const anyOfValidators = (slotDefinition.any_of ?? []).map((subSlot) => this.getValidatorForSlot(subSlot, { inheritedRange: slotDefinition.range, }) ); + const allOfValidators = (slotDefinition.all_of ?? []).map((subSlot) => this.getValidatorForSlot(subSlot, { inheritedRange: slotDefinition.range, @@ -239,6 +250,7 @@ class Validator { if (!value) { if (slotDefinition.required) return 'This field is required'; + // value_presence is subject to dynamic rules? if (slotDefinition.value_presence === 'PRESENT') return 'Value is not present'; return; @@ -266,39 +278,62 @@ class Validator { splitValues = [value]; } + // For any of the value(s), whether they are valid depends on either + // an ok primitive data type parsing, OR a categorical menu match. + // Message needs for (const value of splitValues) { - if (slotType) { + let parse_error = false; + if (slotType) {// Doesn't pertain to slots which are ONLY enumerations. const parsed = this.#parser.parse(value, slotType.uri); + // Issue: parse can fail on decimal but menu has "Missing" if (parsed === undefined) { - return `Value does not match format for ${slotType.uri}`; - } + parse_error = `Value does not match format for ${slotType.uri}`; - if (slotMinimumValue !== undefined && parsed < slotMinimumValue) { - return 'Value is less than minimum value'; + //if (!(anyOfValidators.length || allOfValidators.length || exactlyOneOfValidators.length || noneOfValidators.length)) { + // return parse_error; + //} } + // All these cases have encountered an item which matches basic data + // datatype and so sudden death is ok. + else { - if (slotMaximumValue !== undefined && parsed > slotMaximumValue) { - return 'Value is greater than maximum value'; - } + if (slotMinimumValue !== undefined && parsed < slotMinimumValue) { + return 'Value is less than minimum value'; + } + + if (slotMaximumValue !== undefined && parsed > slotMaximumValue) { + return 'Value is greater than maximum value'; + } + + if ( + (slotDefinition.equals_string !== undefined && + parsed !== slotDefinition.equals_string) || + (slotDefinition.equals_number !== undefined && + parsed !== slotDefinition.equals_number) + ) { + return 'Value does not match constant'; + } - if ( - (slotDefinition.equals_string !== undefined && - parsed !== slotDefinition.equals_string) || - (slotDefinition.equals_number !== undefined && - parsed !== slotDefinition.equals_number) - ) { - return 'Value does not match constant'; - } + if ( + slotDefinition.pattern !== undefined && + !value.match(slotDefinition.pattern) + ) { + return 'Value does not match pattern'; + } - if ( - slotDefinition.pattern !== undefined && - !value.match(slotDefinition.pattern) - ) { - return 'Value does not match pattern'; + // Here slotType value tested and is ok! + continue; } + + // Here value didn't parse to slotType + + } + else { + // No basic slot type here so only enumeration handling. } + // Single range for slot given so no need to evaluate all_of, any_of etc. if (slotEnum && !slotPermissibleValues.includes(value)) { return 'Value is not allowed'; } @@ -342,12 +377,17 @@ class Validator { return 'One or more expressions of none_of held'; } } + + if (anyOfValidators.length || allOfValidators.length || exactlyOneOfValidators.length || noneOfValidators.length) { + // We passed validation here which means a parse error can be overriden + } + else if (parse_error.length) { + //There were no other ranges besides basic slotType so + return parse_error; + } } }; - if (typeof cacheKey === 'string') { - this.#valueValidatorMap.set(cacheKey, validate); - } return validate; }