mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
// Configuration Validation
|
|
// Purpose: Validates and normalizes the one hierarchical configuration document.
|
|
// Scope: Owns reusable validation behavior for the complete schema assembled from service definitions.
|
|
const Ajv = require('ajv');
|
|
const addFormats = require('ajv-formats');
|
|
const { defaultConfig, rootSchema, secretPaths } = require('./definition');
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function mergeDefaults(defaultValue, suppliedValue) {
|
|
/*
|
|
Arrays are complete ordered values and must never be merged item-by-item.
|
|
Plain objects recurse so a stored document can omit a newly introduced
|
|
field and receive its safe default without discarding neighboring values.
|
|
Unknown supplied keys are retained here so strict schema validation can
|
|
report them instead of silently deleting operator input.
|
|
*/
|
|
if (Array.isArray(suppliedValue)) return clone(suppliedValue);
|
|
if (!suppliedValue || typeof suppliedValue !== 'object' || Array.isArray(defaultValue)) {
|
|
return suppliedValue === undefined ? clone(defaultValue) : suppliedValue;
|
|
}
|
|
|
|
const result = clone(defaultValue);
|
|
for (const [key, value] of Object.entries(suppliedValue)) {
|
|
const fallback = defaultValue && typeof defaultValue === 'object' ? defaultValue[key] : undefined;
|
|
result[key] = mergeDefaults(fallback, value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const ajv = new Ajv({ allErrors: true, strict: true });
|
|
addFormats(ajv);
|
|
const validate = ajv.compile(rootSchema);
|
|
|
|
function formatValidationErrors(errors = []) {
|
|
return errors.map((error) => ({
|
|
/*
|
|
Ajv uses JSON Pointer instance paths. Prefixing an additional-property
|
|
name makes the error point at the actual rejected field rather than only
|
|
its containing object, which is more useful in the hierarchical form.
|
|
*/
|
|
path: error.keyword === 'additionalProperties'
|
|
? `${error.instancePath}/${error.params.additionalProperty}`
|
|
: error.instancePath || '/',
|
|
message: error.message || 'Invalid value',
|
|
keyword: error.keyword,
|
|
}));
|
|
}
|
|
|
|
function normalizeConfig(input = {}) {
|
|
return mergeDefaults(defaultConfig, input);
|
|
}
|
|
|
|
function assertValidConfig(input) {
|
|
if (validate(input)) return input;
|
|
const error = new Error('Configuration validation failed.');
|
|
error.code = 'CONFIG_VALIDATION_FAILED';
|
|
error.validationErrors = formatValidationErrors(validate.errors);
|
|
throw error;
|
|
}
|
|
|
|
/*
|
|
Defaults are executable configuration, not documentation. Validate them at
|
|
module load so a definition edit cannot make first boot fail later in an
|
|
unrelated service require chain.
|
|
*/
|
|
assertValidConfig(defaultConfig);
|
|
|
|
module.exports = {
|
|
defaultConfig,
|
|
secretPaths,
|
|
rootSchema,
|
|
clone,
|
|
normalizeConfig,
|
|
assertValidConfig,
|
|
};
|