'use strict'; /** * @author Alan Rodas Bonjour */ /** * The base shape hierarchy. * * @private */ const baseShapesHierarchy = { // any type (nothing above any) any: undefined, // base types of typeof boolean: 'any', number: 'any', bigint: 'any', string: 'any', symbol: 'any', object: 'any', undefined: 'any', // number hierarchy int: 'number', float: 'number', nan: 'number', infinity: 'number', // object hierarchy array: 'object', regexp: 'object', buffer: 'object', function: 'object', promise: 'object', null: 'object', wrapper: 'object', instance: 'object' }; /** * Return the shape that can be used to unify the two given base shapes, or undefined * if the unification did not succeed. * * @param shapeA - The first shape to unify * @param shapeB - The second shape to unify * * @returns The shape that may be used to unify both given shapes. * * @private */ const unifyBaseShape = (shapeA, shapeB) => { const visitedShapesA = new Set(); let currentShapeA = shapeA; while (currentShapeA) { visitedShapesA.add(currentShapeA); currentShapeA = baseShapesHierarchy[currentShapeA]; } let currentShapeB = shapeB; while (currentShapeB) { if (visitedShapesA.has(currentShapeB)) { return currentShapeB; } currentShapeB = baseShapesHierarchy[currentShapeB]; } return undefined; }; /** * Return the shape that can be used to unify the two given array shapes, or undefined * if the unification did not succeed. * * @remarks * Two array shapes unify if their inner shape unifies. So the unified shape is * an array shape with the inner shape unified. * * @param shapeA - The first shape to unify * @param shapeB - The second shape to unify * * @returns The shape that may be used to unify both given shapes. * * @private */ const unifyArrays = (shapeA, shapeB) => { const unifiedElement = unify(shapeA[0], shapeB[0]); return unifiedElement ? [unifiedElement] : undefined; }; /** * Returns the shape that can be used to unify the two object shapes, or undefined * if the unification did not succeed. * * @remarks * Two object shapes unify if all their attributes also unify. So the returned * shape contains all the attributes that are present in both object, with * a value that is the unification of the shapes of such attribute values. * Note that the most general shape is the one used for unification. * * @param shapeA - The first shape to unify * @param shapeB - The second shape to unify * * @returns The shape that may be used to unify both given shapes. * * @private */ const unifyObjects = (shapeA, shapeB) => { // We know they are not arrays, as we discarded the case earlier const unifiedObject = {}; const keys = new Set([...Object.keys(shapeA), ...Object.keys(shapeB)]); for (const key of keys) { const shapeAValue = shapeA[key]; const shapeBValue = shapeB[key]; if (shapeAValue && shapeBValue) { const unifiedValue = unify(shapeAValue, shapeBValue); // Could not unify the attribute, they cannot be unified if (unifiedValue === undefined) return undefined; unifiedObject[key] = unifiedValue; } } return unifiedObject; }; /** * Unify two shapes, returning the common shape between the two, or undefined * if the unification did not succeed. * * @remarks * In the worst case scenario, 'any' can be used to unify any other shape, as * it's the common ancestor to all elements. So undefined is not rally ever returned. * * @param shapeA - The first shape to unify * @param shapeB - The second shape to unify * * @returns The shape that may be used to unify both given shapes. * * @private */ const unify = (shapeA, shapeB) => { // Base shape unification if (typeof shapeA === 'string' && typeof shapeB === 'string') { // It may be a union shape const shapesA = shapeA.split('|').map((e) => e.trim()); const shapesB = shapeB.split('|').map((e) => e.trim()); const shapes = [...shapesA, ...shapesB]; if (shapes.length === 2) { return unifyBaseShape(shapes[0], shapes[1]); } else { return unifyAll(shapes); } } // Array unification if (Array.isArray(shapeA) && shapeB === 'array') return 'array'; if (Array.isArray(shapeB) && shapeA === 'array') return 'array'; if (Array.isArray(shapeA) && Array.isArray(shapeB)) return unifyArrays(shapeA, shapeB); if (Array.isArray(shapeA) || Array.isArray(shapeB)) return undefined; // Object unification if (typeof shapeA === 'object' && shapeB === 'object') return 'object'; if (shapeA === 'object' && typeof shapeB === 'object') return 'object'; if (typeof shapeA === 'object' && typeof shapeB === 'object') return unifyObjects(shapeA, shapeB); // Could not unify, unify as any return 'any'; }; /** * Unify all the given shapes, returning the common shape between them, or undefined * if the unification did not succeed. * * @param shapes - The shapes to unify * * @returns The shape that may be used to unify all given shapes. * * @private */ const unifyAll = (shapes) => { if (shapes.length === 0) return undefined; return shapes.reduce((a, b) => unify(a, b)); }; /** * The default options to use when using {@link shapeOf} function. */ const defaultShapeOfOptions = { useFullObjectShapes: false, unwrapBaseShapes: true }; /** * Return the shape of the given number value. * * @remarks * The shape of a number is always a numeric shape. It may be any of 'int', 'nan', * 'infinity' or 'float'. 'number' is never returned, although all of this shapes * do unify with 'number'. * * @param value - The value to obtain the shape of * @param _options - The options to use when obtaining the shape * * @returns The shape of the given value */ const shapeOfNumber = (value, _options) => { if (Number.isInteger(value)) return 'int'; if (Number.isNaN(value)) return 'nan'; if (!Number.isFinite(value)) return 'infinity'; return 'float'; }; /** * Return the shape of the given array value. * * @remarks * Note that the shape of an array value is just 'array', when the option * attribute `useFullObjectShapes` is `false` (default) but it's an array shape * that it's calculated by unifying all the values in the array to a particular * shape if `useFullObjectShapes` is `true`. * In this regard, bear in mind that traversing all the elements in the array * to obtains and unify their shapes is a costly operation. You should do that * only when needed. * * @param value - The value to obtain the shape of * @param options - The options to use when obtaining the shape * * @returns The shape of the given value */ const shapeOfArray = (value, options) => { if (options.useFullObjectShapes) { if (value.length === 0) return ['any']; if (value.length === 1) return [shapeOf(value[0])]; const unified = unifyAll(value.map((e) => shapeOf(e, options))); return [unified]; } return 'array'; }; /** * Return the shape of the given object value. * * @remarks * Note that the shape of an object value (that is not an array) depends on * multiple factors. `null` will be the sole value having the 'null' shape. * * For well known objects, such as regular expressions, buffers and promises, * there are built-in object shapes (arrays are also included, but treated * separately from this function). * * If the value is an instance of a wrapper type * then the option `unwrapBaseShapes` is checked. If `true` (the default), then * the shape of the wrapped value is used, if `false` the shape 'wrapper' is used. * * For instances of other classes or constructor functions, the 'instance' shape * is used. * * If any of the above is matched, then the `useFullObjectShapes` is checked. * When `false` (default) the 'object' shape is used. When `true` then a complex * object shape is returned. This shape will contain all attributes of the given * value, and in each of them, the expected shape of the value in that attribute. * This implies traversing the object, and recursively traversing all attributes * of the object, which may be really costly. Also, bear in mind that circular * references will cause this process to hang. You should only ask for full * object shapes then it's really needed, and rely on basic shapes whenever possible. * * @param value - The value to obtain the shape of * @param options - The options to use when obtaining the shape * * @returns The shape of the given value */ const shapeOfObject = (value, options) => { // eslint-disable-next-line no-null/no-null if (value === null) return 'null'; if (Buffer.isBuffer(value)) return 'buffer'; if (value instanceof RegExp) return 'regexp'; if (options.unwrapBaseShapes) { // wrapped objects are treated as primitives if (value instanceof Number) return shapeOfNumber(value.valueOf()); if (value instanceof Boolean) return 'boolean'; if (value instanceof String) return 'string'; if (value instanceof Symbol) return 'symbol'; if (value instanceof BigInt) return 'bigint'; } if (value !== undefined && value.then !== undefined && typeof value.then === 'function' && value.then.length === 2) return 'promise'; if (value?.constructor?.name !== undefined && ['Number', 'Boolean', 'String', 'Symbol', 'BigInt'].includes(value.constructor.name)) return 'wrapper'; if (value?.constructor?.name !== undefined && !['Object', 'Array'].includes(value.constructor.name)) return 'instance'; if (options.useFullObjectShapes) { const shape = {}; for (const key of Object.keys(value)) { shape[key] = shapeOf(value[key], options); } return shape; } return 'object'; }; /** * Return the shape of the given value * * @remarks * The way in which a the shape of an object is calculated and returned may vary * according to different options. Read more about shapeOf and when to use it * in the README file. * * @param value - The value to obtain the shape of * @param options - The options to use when obtaining the shape * * @returns The shape of the given value */ // eslint-disable-next-line @typescript-eslint/no-explicit-any const shapeOf = (value, options) => { const opts = Object.assign(defaultShapeOfOptions, options); if (typeof value === 'number') return shapeOfNumber(value); if (Array.isArray(value)) return shapeOfArray(value, opts); if (typeof value === 'object') return shapeOfObject(value, opts); return typeof value; }; /** * @author Alan Rodas Bonjour */ /** * The default options to use when using {@link hasShape} function. */ const defaultHasShapeOptions = { unwrapBaseShapes: true, exactShape: false }; /** * Answers if the given value has the particular base shape. * * @remarks * Any value may have a base shape. The way to check if it has a base shape * is by obtaining the base shape of the value, and then attempting to check * if such shape unifies with the given one by traversing the base hierarchy * tree. * * @param value - The value to check if it has the base shape. * @param shape - The base shape to check for matching. * @param options - The options to use when attempting to match. * * @returns `true` if it has the shape, `false` otherwise. */ const hasBaseShape = (value, shape, options) => { const baseShapeOfObj = shapeOf(value, { useFullObjectShapes: false, unwrapBaseShapes: options.unwrapBaseShapes }); let currentShape = baseShapeOfObj; while (currentShape) { if (currentShape === shape) return true; currentShape = baseShapesHierarchy[currentShape]; } return false; }; /** * Answers if the given value has the particular array shape. * * @remarks * An array shape is an array containing a sole value, that is a shape. * Only array values may have an array shape, which happens if all values * in the array value have the array shape's inner shape. * * @param value - The value to check if it has the array shape. * @param shape - The array shape to check for matching. * @param options - The options to use when attempting to match. * * @returns `true` if it has the shape, `false` otherwise. */ const hasArrayShape = (value, shape, options) => { if (!Array.isArray(value)) return false; if (value.length === 0) return true; const innerShape = shape[0]; if (innerShape === 'any') return true; for (const each of value) { if (!hasShape(each, innerShape, options)) return false; } return true; }; /** * Answers if the given value has the particular object shape. * * @remarks * Only object may have an object shape. Objects must, by default, have * at least the given shape. * * If the options contains the `exactShape` attribute as `false`, then * we attempt to check if the value has at least the attributes in the * shape (although it may have more). If `true` is used, then exactly the * attributes in the shape, and no more are expected. * * @param value - The value to check if it has the object shape. * @param shape - The object shape to check for matching. * @param options - The options to use when attempting to match. * * @returns `true` if it has the shape, `false` otherwise. */ const hasObjectShape = (value, shape, options) => { // If the value is not an object, returns false if (typeof value !== 'object') return false; if (options.exactShape) { // If the exactShape used, then the same number of keys must be present if (Object.keys(value).length !== Object.keys(shape).length) return false; } else { // If the exactShape is false, then more or the same number of keys must be present if (Object.keys(value).length < Object.keys(shape).length) return false; } // Check that each key in the shape is present in the value for (const attrKey of Object.keys(shape)) { const attrValue = shape[attrKey]; if (!Object.prototype.hasOwnProperty.call(value, attrKey)) return false; if (attrValue === 'any') return true; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const objValue = value[attrKey]; if (!hasShape(objValue, attrValue, options)) { return false; } } // All keys matched return true; }; /** * Answers if the given value has any of the shapes given. * * @param value - The value to check if it has any of the given shapes. * @param shapes - The shapes to check for matching. * @param options - The options to use when attempting to match. * * @returns `true` if it has any of the shapes, `false` otherwise. */ const hasAnyShape = (value, shapes, options) => { for (const shape of shapes) { if (hasShape(value, shape, options)) { return true; } } return false; }; /** * Answer if the given value has the given shape. * * @remarks * The way in which an value is checked against the shape involves * multiple steps of following the unification process. Simple matching the * shape of the value with the given shape does not suffice. Read more * about hasShape and when to use it in the README file. * * @param value - The value to check if it has the given shapes. * @param shape - The shape to check for matching. * @param options - The options to use when attempting to match. * * @returns `true` if it has the given shape, `false` otherwise. */ const hasShape = (value, shape, options) => { // Use default options overwritten with the given ones const opts = Object.assign(defaultHasShapeOptions, options); if (typeof shape === 'string') { // Using a string as a shape if (shape.includes('|')) { // It contains a | character, so it represents a union shape const shapes = shape.split('|').map((e) => e.trim()); return hasAnyShape(value, shapes, opts); } else { // Using a base shape return hasBaseShape(value, shape, opts); } } if (Array.isArray(shape)) { // Using an array as a shape if (shape.length > 1) { // If the array has more than one value, then it represents a union shape return hasAnyShape(value, shape, opts); } else { // If not, it's a simple array return hasArrayShape(value, shape, opts); } } if (typeof shape === 'object') { // Using an object as a shape return hasObjectShape(value, shape, opts); } // The shape is not valid, returns false return false; }; exports.hasShape = hasShape; exports.shapeOf = shapeOf; //# sourceMappingURL=index.cjs.map