|
|
|
|
|
|
|
|
|
|
|
|
|
|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { MergeStrategy } = require("./merge-strategy"); |
|
|
const { ValidationStrategy } = require("./validation-strategy"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const strategies = Symbol("strategies"); |
|
|
const requiredKeys = Symbol("requiredKeys"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function validateDefinition(name, strategy) { |
|
|
|
|
|
let hasSchema = false; |
|
|
if (strategy.schema) { |
|
|
if (typeof strategy.schema === "object") { |
|
|
hasSchema = true; |
|
|
} else { |
|
|
throw new TypeError("Schema must be an object."); |
|
|
} |
|
|
} |
|
|
|
|
|
if (typeof strategy.merge === "string") { |
|
|
if (!(strategy.merge in MergeStrategy)) { |
|
|
throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`); |
|
|
} |
|
|
} else if (!hasSchema && typeof strategy.merge !== "function") { |
|
|
throw new TypeError(`Definition for key "${name}" must have a merge property.`); |
|
|
} |
|
|
|
|
|
if (typeof strategy.validate === "string") { |
|
|
if (!(strategy.validate in ValidationStrategy)) { |
|
|
throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`); |
|
|
} |
|
|
} else if (!hasSchema && typeof strategy.validate !== "function") { |
|
|
throw new TypeError(`Definition for key "${name}" must have a validate() method.`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnexpectedKeyError extends Error { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(key) { |
|
|
super(`Unexpected key "${key}" found.`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MissingKeyError extends Error { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(key) { |
|
|
super(`Missing required key "${key}".`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MissingDependentKeysError extends Error { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(key, requiredKeys) { |
|
|
super(`Key "${key}" requires keys "${requiredKeys.join("\", \"")}".`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WrapperError extends Error { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(key, source) { |
|
|
super(`Key "${key}": ${source.message}`, { cause: source }); |
|
|
|
|
|
|
|
|
for (const key of Object.keys(source)) { |
|
|
if (!(key in this)) { |
|
|
this[key] = source[key]; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ObjectSchema { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(definitions) { |
|
|
|
|
|
if (!definitions) { |
|
|
throw new Error("Schema definitions missing."); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this[strategies] = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this[requiredKeys] = new Map(); |
|
|
|
|
|
|
|
|
for (const key of Object.keys(definitions)) { |
|
|
validateDefinition(key, definitions[key]); |
|
|
|
|
|
|
|
|
if (typeof definitions[key].schema === "object") { |
|
|
const schema = new ObjectSchema(definitions[key].schema); |
|
|
definitions[key] = { |
|
|
...definitions[key], |
|
|
merge(first = {}, second = {}) { |
|
|
return schema.merge(first, second); |
|
|
}, |
|
|
validate(value) { |
|
|
ValidationStrategy.object(value); |
|
|
schema.validate(value); |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof definitions[key].merge === "string") { |
|
|
definitions[key] = { |
|
|
...definitions[key], |
|
|
merge: MergeStrategy[definitions[key].merge] |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
if (typeof definitions[key].validate === "string") { |
|
|
definitions[key] = { |
|
|
...definitions[key], |
|
|
validate: ValidationStrategy[definitions[key].validate] |
|
|
}; |
|
|
}; |
|
|
|
|
|
this[strategies].set(key, definitions[key]); |
|
|
|
|
|
if (definitions[key].required) { |
|
|
this[requiredKeys].set(key, definitions[key]); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hasKey(key) { |
|
|
return this[strategies].has(key); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
merge(...objects) { |
|
|
|
|
|
|
|
|
if (objects.length < 2) { |
|
|
throw new TypeError("merge() requires at least two arguments."); |
|
|
} |
|
|
|
|
|
if (objects.some(object => (object == null || typeof object !== "object"))) { |
|
|
throw new TypeError("All arguments must be objects."); |
|
|
} |
|
|
|
|
|
return objects.reduce((result, object) => { |
|
|
|
|
|
this.validate(object); |
|
|
|
|
|
for (const [key, strategy] of this[strategies]) { |
|
|
try { |
|
|
if (key in result || key in object) { |
|
|
const value = strategy.merge.call(this, result[key], object[key]); |
|
|
if (value !== undefined) { |
|
|
result[key] = value; |
|
|
} |
|
|
} |
|
|
} catch (ex) { |
|
|
throw new WrapperError(key, ex); |
|
|
} |
|
|
} |
|
|
return result; |
|
|
}, {}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validate(object) { |
|
|
|
|
|
|
|
|
for (const key of Object.keys(object)) { |
|
|
|
|
|
|
|
|
if (!this.hasKey(key)) { |
|
|
throw new UnexpectedKeyError(key); |
|
|
} |
|
|
|
|
|
|
|
|
const strategy = this[strategies].get(key); |
|
|
|
|
|
|
|
|
if (Array.isArray(strategy.requires)) { |
|
|
if (!strategy.requires.every(otherKey => otherKey in object)) { |
|
|
throw new MissingDependentKeysError(key, strategy.requires); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
strategy.validate.call(strategy, object[key]); |
|
|
} catch (ex) { |
|
|
throw new WrapperError(key, ex); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for (const [key] of this[requiredKeys]) { |
|
|
if (!(key in object)) { |
|
|
throw new MissingKeyError(key); |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
exports.ObjectSchema = ObjectSchema; |
|
|
|