structured-elements
Version:
A TypeScript package for modelling and validating data
382 lines • 17.7 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-explicit-any */
Object.defineProperty(exports, "__esModule", { value: true });
exports.StructuredElements = exports.Mirror = void 0;
var mirror_1 = require("./mirror");
Object.defineProperty(exports, "Mirror", { enumerable: true, get: function () { return mirror_1.Mirror; } });
const array_1 = require("./attemptSalvage/array");
const collection_1 = require("./attemptSalvage/collection");
const item_1 = require("./attemptSalvage/item");
const itemViaBlanking_1 = require("./attemptSalvage/itemViaBlanking");
const mirror_2 = require("./attemptSalvage/mirror");
const referenceValidators_1 = require("./build/referenceValidators");
const structuredResultCache_1 = require("./build/structuredResultCache");
const constants_1 = require("./constants");
const referencedValidator_1 = require("./ensure/referencedValidator");
const array_2 = require("./is/array");
// This library allows an application to easily validate data by
// defining a standard set of expectations for each known Type.
//
// The library is designed to be used with a Registry object that
// contains all the models that you want to validate. You supply this
// registry when you call the setup function that returns your api.
//
// Each model has an expectation that defines the rules for validating
// the model's data. You can define expectations as inline schemas,
// references to other models, or custom validation functions.
//
// The library caches the results of validating subjects against
// expectations, so that you don't have to validate the same subject
// multiple times. This is useful for performance and debugging.
//
// The library also provides a debug mode that logs information about
// the validation process when enabled.
//
// The library is designed to be used with TypeScript, so that you can
// define your models and expectations using the type system. This
// makes it easier to catch errors and refactor your code.
//
// To an extent, the library can detect when a model does not match its
// Type. This feature isn't perfect, but it can help you catch some
// mistakes. Unfortunately, the type errors in this situation aren't
// always as helpful as we would like them to be.
//
// You can validate a subject against an expectation by calling the
// validator function on your api object. This function returns a
// validator object that you can use to check if a subject is valid,
// get a list of validation failures, or attempt to salvage a subject
// that has failed validation.
//
// You can tell the library that a given expectation applies to data in
// a specific structure by using the reference function on your api
// object. This is useful for validating arrays, collections, and
// mirrors of data.
//
// That same reference function can be used to create a reference to
// another model, so that you specify when a model contains one or more
// instances of another model. This is useful for validating nested
// data structures.
// Example usage:
//
// (lib/models.ts)
//
// import { StructuredElements } from 'structured-elements';
// import { type Person, PersonModel } from '@/lib/person';
// import { type Thing, ThingModel } from '@/lib/thing';
//
// export type Registry = {
// Person: Person;
// Thing: Thing;
// }
//
// export type Model<ModelId extends keyof Registry> = StructuredElements.Model<
// Registry,
// ModelId
// >;
//
// export const Modelling = StructuredElements.setup<Registry>({
// debugEnabled: () => process.env.NODE_ENV === 'development',
// models: {
// Person: PersonModel,
// Thing: ThingModel,
// },
// });
//
// (lib/person.ts)
//
// import { Modelling, type Model } from '@/lib/models';
//
// export type Person = {
// inventory: Thing[];
// name: string;
// roleId?: number;
// }
//
// export const PersonModel: Model<'Person'> = {
// inventory: Modelling.reference('array', 'Thing'),
// name: 'string',
// roleId: ['number', undefined],
// };
//
// (lib/thing.ts)
//
// import { Modelling, type Model } from '@/lib/models';
//
// export type Thing = {
// id: string;
// name: string;
// parts: Record<string, Thing>;
// type: 'widget' | 'gadget';
// weight: number | null;
// }
//
// export const ThingModel: Model<'Thing'> = {
// id: 'string',
// name: 'string',
// parts: Modelling.reference('collection', 'Thing'),
// type: Modelling.equality('item', ['widget', 'gadget']),
// weight: ['number', null],
// },
//
// (lib/apiConsumer.ts)
//
// import { Modelling } from '@/lib/models';
//
// export const fetchPeople = async (ids: string) => {
// const response = await fetch(`/api/people?ids=${ids}`);
// const people = await response.json();
//
// const validator = Modelling.validator('array', 'Person');
//
// if (validator.isValid(people) {
// return people;
// }
//
// return validator.getSalvage(person, 'person');
// }
var StructuredElements;
(function (StructuredElements) {
// Call this function to set up the validation library.
// It returns an object that contains all the functions you need to interact with the library.
// You can use this object to define new models, arrays, collections, and mirrors.
// Make sure that you put the object in its own file and export it so that you can use it throughout your application.
// The Registry type that you pass in should contain all the models that you want to validate.
// The recommended approach is to match your modelId registry keys to the names of the types in your application.
// Do not create more than one instance of the API object unless you have a very good reason to.
StructuredElements.setup = ({ debugEnabled, logDebugMessage = console.log, models, }) => {
const api = {
// Supply this function when you initialize the API object.
// If it returns true, the library will log debug information.
debugEnabled,
equality: (structure, target) => {
if (api.internalCache.buildEqualityCheck) {
return api.internalCache.buildEqualityCheck(structure, target);
}
const buildEqualityCheck = APIMethods.curryBuildEqualityCheck();
api.internalCache.buildEqualityCheck = buildEqualityCheck;
return buildEqualityCheck(structure, target);
},
// This cache stores all the models that have been defined.
// The library uses it to look up models by their ID.
// It also stores the results of validating subjects, so that we don't have to validate the same subject multiple times.
// Interacting with the cache directly is not supported.
registeredModels: () => {
if (api.internalCache.modelRegistry) {
return api.internalCache.modelRegistry;
}
const registryBuilders = api.internalCache.prepareModelRegistry();
const newModelRegistry = new Map();
for (const modelId in registryBuilders) {
const entry = api.privateFunctions.buildRegistryEntry({
modelId,
expect: registryBuilders[modelId],
});
newModelRegistry.set(modelId, entry);
}
api.internalCache.modelRegistry = newModelRegistry;
return api.internalCache.modelRegistry;
},
reference: (structure, target) => {
if (api.internalCache.reference) {
return api.internalCache.reference(structure, target);
}
const reference = APIMethods.curryBuildReference();
api.internalCache.reference = reference;
return reference(structure, target);
},
results: new Map(),
validator: (expectation, structure) => {
if (api.internalCache.getValidator) {
return api.internalCache.getValidator(expectation, structure || `item`);
}
const getValidator = APIMethods.curryGetValidator(api);
api.internalCache.getValidator = getValidator;
return getValidator(expectation, structure);
},
// This object contains functions for attempting to salvage data in
// various structures. They are mostly used internally by the library,
// but exposed here because you might need to pass a specific
// attemptSalvage operation into a validator.
attemptSalvage: {
// Salvage an array by filtering out invalid elements.
array: array_1.attemptSalvageArray,
// Salvage a collection by filtering out invalid entries.
collection: collection_1.attemptSalvageCollection,
// By default, we do not salvage invalid items. This returns undefined.
item: item_1.attemptSalvageItem,
// Attempt to salvage an item by discarding optional invalid fields.
// This checks each invalid field against its expectation.
//
// If undefined is permitted, the field is omitted from the item.
//
// Otherwise, if null is permitted, the field's value is set to null.
//
// If the item has one or more invalid fields that cannot be blank,
// the entire item is considered invalid and this returns undefined.
itemViaBlanking: itemViaBlanking_1.attemptSalvageItemViaBlanking,
// Salvage a mirror by building a new one using only the valid elements
// from its collection. This process ignores the entire array, potentially
// discarding some valid array elements that are not in the collection,
// because we do not know what those elements would have used as keys.
mirror: mirror_2.attemptSalvageMirror,
},
// This object contains functions that are used internally by the library.
privateFunctions: {
// Validators call this function to cache the result of validating a subject against a given expectation or expectations.
cacheResult: ({ expectation, result, structure }) => {
if (api.internalCache.cacheResult) {
return api.internalCache.cacheResult({
expectation,
result,
structure,
});
}
const cacheResult = APIMethods.curryCacheResult(api);
api.internalCache.cacheResult = cacheResult;
return cacheResult({ expectation, result, structure });
},
debug: logDebugMessage,
// Validators call this function to check if a subject has already been validated against a given expectation.
// If it has, they usually return the cached result.
getCachedResult: ({ expectation, structure, subject }) => {
if (api.internalCache.getCachedResult) {
return api.internalCache.getCachedResult({
expectation,
structure,
subject,
});
}
const getCachedResult = APIMethods.curryGetCachedResult(api);
api.internalCache.getCachedResult = getCachedResult;
return getCachedResult({ expectation, structure, subject });
},
// This function is used to create each of the models in the registry.
buildRegistryEntry: ({ modelId, expect: expectation }) => {
if (api.internalCache.registerModel) {
return api.internalCache.registerModel({
modelId,
expect: expectation,
});
}
const registerModel = APIMethods.curryBuildRegistryEntry(api);
api.internalCache.registerModel = registerModel;
return registerModel({ modelId, expect: expectation });
},
},
// Interacting with this cache directly is not supported.
internalCache: {
buildEqualityCheck: undefined,
cacheResult: undefined,
getCachedResult: undefined,
getValidator: undefined,
modelRegistry: undefined,
prepareModelRegistry: models,
reference: undefined,
registerModel: undefined,
validators: new Map(),
},
};
return api;
};
// This namespace is used internally by the library to define the functions that are exposed to the application via its api object.
let APIMethods;
(function (APIMethods) {
APIMethods.curryCacheResult = (api) => {
return ({ expectation, result, structure, }) => {
var _a, _b;
if (StructuredElements.isCacheable(result.subject)) {
const subject = result.subject;
if (!api.results.has(expectation)) {
const structuredResultCache = (0, structuredResultCache_1.buildStructuredResultCache)();
api.results.set(expectation, structuredResultCache);
}
;
(_b = (_a = api.results
.get(expectation)) === null || _a === void 0 ? void 0 : _a.get(structure)) === null || _b === void 0 ? void 0 : _b.set(subject, result);
}
return result;
};
};
APIMethods.curryGetCachedResult = (api) => {
return ({ expectation, structure, subject, }) => {
var _a, _b;
if (StructuredElements.isCacheable(subject)) {
const cachedResult = (_b = (_a = api.results
.get(expectation)) === null || _a === void 0 ? void 0 : _a.get(structure)) === null || _b === void 0 ? void 0 : _b.get(subject);
return cachedResult;
}
};
};
APIMethods.curryGetValidator = (api) => {
const getValidatorFn = (expectation, structure) => {
return (0, referencedValidator_1.ensureReferencedValidator)({
api,
container: api.reference(structure || `item`, expectation),
});
};
return getValidatorFn;
};
APIMethods.curryBuildEqualityCheck = () => {
return (structure, expectation) => {
const equalityFunction = (subject) => {
const allowedValues = (0, array_2.isArray)(expectation)
? expectation
: [expectation];
return allowedValues.some((allowedValue) => {
return subject === allowedValue;
});
};
return {
[constants_1.referenceToken]: {
structure,
target: equalityFunction,
},
};
};
};
APIMethods.curryBuildReference = () => {
return (structure, target) => {
return {
[constants_1.referenceToken]: {
structure,
target,
},
};
};
};
APIMethods.curryBuildRegistryEntry = (api) => {
return ({ modelId, expect, }) => {
const registryEntry = {
modelId,
expect,
validators: () => {
const expectation = registryEntry.expect();
const cached = api.internalCache.validators.get(expectation);
if (cached) {
return cached;
}
const validators = (0, referenceValidators_1.buildReferenceValidators)({
api,
expectation,
});
api.internalCache.validators.set(expectation, validators);
return validators;
},
};
return registryEntry;
};
};
})(APIMethods = StructuredElements.APIMethods || (StructuredElements.APIMethods = {}));
// We use the WeakMap structure because it automatically cleans up
// after itself when the subject is garbage collected. It stores the
// results against the object's memory reference.
//
// We can't cache the results of validating primitive values, as they
// don't have individual memory references and can be shared across
// the application. WeakMap will throw an error if you try to use a
// primitive value as a key.
StructuredElements.isCacheable = (subject) => {
return typeof subject === `object` && subject !== null;
};
})(StructuredElements = exports.StructuredElements || (exports.StructuredElements = {}));
//# sourceMappingURL=index.js.map