mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
this is a big slop that might backfire lol... new config system and UI!
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// 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,
|
||||
};
|
||||
Reference in New Issue
Block a user