/**
* @namespace conforms
*/
import { TYPES } from "../equals/types.js";
function createResultAggregator() {
const results = {
passed: true,
warnings: [],
errors: [],
warn: function (msg) {
results.warnings.push(msg);
},
error: function (msg) {
results.errors.push(msg);
results.passed = false;
},
};
return results;
}
/**
* <p>Check whether an object conforms to some schema. This function does not
* return a boolean, but a result object that takes the following form:</p>
*
* <pre><code>
* {
* conforms: boolean,
* warnings: string[],
* errors: string[],
* }
* </code></pre>
*
* <p>Note that the <code>strict</code> flag determines whether or not
* to perform coercive validation. If <code>strict=false</code> and the
* code does need to perform coercion in order for validation to pass,
* the object will be updated such that it will now pass strict validation.</p>
*
* @name conforms.conforms
* @function
* @param {schema} schema - The schema that the object should conform to
* @param {object} object - The object to check conformance for
* @param {boolean} [strict] - True if strict type validation is required, false for coercive. Defaults to true.
* @param {boolean} [allowIncomplete] - Do not fail validation if there are fields missing from this object. Defaults to false.
* @returns {object} See description above
*/
export function conforms(
schema,
object,
strict = true,
allowIncomplete = false
) {
const results = __conforms(
schema,
object,
strict,
allowIncomplete,
createResultAggregator(),
false
);
results.conforms = results.errors.length === 0;
return results;
}
/**
* See above.
*
* @function
* @param {schema} schema - The schema that the object should conform to
* @param {object} object - The object to check conformance for
* @param {boolean} strict - True if strict type validation is required, false for coercive
* @param {boolean} allowIncomplete - Do not fail validation if there are fields missing from this object
* @returns {object} See above
* @ignore
*/
function __conforms(schema, object, strict, allowIncomplete, results, prefix) {
const objectKeys = Object.keys(object);
const schemaKeys = Object.keys(schema);
const { warn, error } = results;
objectKeys.forEach((key) => {
if (schemaKeys.indexOf(key) < 0) {
warn(`${key}: not-in-schema property.`);
}
});
schemaKeys
.filter((v) => v !== `__meta`)
.forEach((field_name) => {
const args = {
object,
field_name,
schema: schema[field_name],
strict,
allowIncomplete,
prefix,
results,
};
if (args.schema.__meta.array) {
testArray(args);
} else {
testField(args);
}
});
return results;
}
/**
* Test an array of values for individual element conformance to the indicated schema.
* @param {*} object
* @param {*} field_name
* @param {*} schema
* @param {*} strict
* @param {*} allowIncomplete
* @param {*} prefix
* @param {*} results
* @ignore
*/
function testArray({
object,
field_name,
schema,
strict,
allowIncomplete,
prefix,
results,
}) {
const { required } = schema.__meta;
const { warn, error } = results;
const field = `${prefix ? `${prefix}.` : ``}${field_name}`;
const value = object[field_name];
// if this field is required, it can't be undefined,
// but even if it's defined, it can't be an empty array.
if (required) {
if (value === undefined) {
return error(`${field}: required array field missing.`);
}
if (value instanceof Array && value.length === 0) {
return error(`${field}: empty required array field found.`);
}
return error(`${field}: required field must be an array.`);
}
// if this is not a required field, and it's not defined,
// then we're done: there is nothing to test.
if (value === undefined) return;
// If it *is* defined, we want to make sure that this is an array.
if (value !== undefined && !(value instanceof Array)) {
return error(`${field}: must be an array`);
}
value.forEach((_, position) => {
testField({
object: value,
field_name: position,
schema,
strict,
allowIncomplete,
prefix: `${field}[]`,
results,
});
});
}
/**
* Test a value to see if it conforms to the associated schema
* @param {*} object
* @param {*} field_name
* @param {*} schema
* @param {*} strict
* @param {*} allowIncomplete
* @param {*} prefix
* @param {*} results
* @ignore
*/
function testField({
object,
field_name,
schema,
strict,
allowIncomplete,
prefix,
results,
}) {
const { warn, error } = results;
const { type, choices, shape } = schema;
const { required, configurable } = schema.__meta;
const field = `${prefix ? `${prefix}.` : ``}${field_name}`;
const value = object[field_name];
if (value === undefined) {
if (required) {
if (schema.default === undefined) {
if (allowIncomplete && !configurable) {
warn(
`${field}: missing (required, permitted through ALLOW_INCOMPLETE)).`
);
} else {
error(`${field}: required field missing.`);
}
} else {
warn(`${field}: missing (required, but with default value specified).`);
}
} else {
warn(`${field}: missing (but not required).`);
}
return;
}
if (value instanceof Array) {
return error(
`${field}: arrays are not allowed as unmarked field values (add __meta.array:true).`
);
}
if (choices && TYPES.mixed(value, true, choices)) {
return;
}
if (choices && !TYPES.mixed(value, true, choices)) {
if (!strict && TYPES.mixed(value, false, choices)) {
object[field_name] = coerce(value, type, choices);
} else {
return error(
`${field}: value [${value}] is not in the list of permitted values [${choices.join(
`,`
)}]`
);
}
}
if (shape) {
return __conforms(shape, value, strict, allowIncomplete, results, field);
}
if (type && TYPES[type](value, true, choices)) {
return;
}
if (type && !TYPES[type](value, true, choices)) {
if (!strict && TYPES[type](value, false, choices)) {
object[field_name] = coerce(value, type);
} else {
return error(`${field}: value is not a valid ${type}.`);
}
}
}
// Force values to fit the type they need to be, if possible
function coerce(value, type, choices) {
if (type) {
if (type === `boolean`) {
if (typeof value === `number`) {
if (value === 0) return false;
if (value === 1) return true;
}
if (typeof value === `string`) {
const lc = value.toLocaleLowerCase();
if (lc === `true`) return true;
if (lc === `false`) return false;
}
}
if (type === `number`) {
if (typeof value === `string`) {
return parseFloat(value);
}
}
if (type === `string`) {
return `${value}`;
}
}
if (choices) {
return choices.find((e) => e == value);
}
}