diff options
Diffstat (limited to 'src/data/things/composite.js')
-rw-r--r-- | src/data/things/composite.js | 727 |
1 files changed, 1 insertions, 726 deletions
diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c03f8833..7e068dce 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2,14 +2,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; - -import { - a, - is, - isString, - isWholeNumber, - validateArrayItems, -} from '#validators'; +import {a} from '#validators'; import { decorateErrorWithIndex, @@ -1639,721 +1632,3 @@ export function debugComposite(fn) { compositeFrom.debug = false; return value; } - -// Exposes a dependency exactly as it is; this is typically the base of a -// composition which was created to serve as one property's descriptor. -// -// Please note that this *doesn't* verify that the dependency exists, so -// if you provide the wrong name or it hasn't been set by a previous -// compositional step, the property will be exposed as undefined instead -// of null. -// -export const exposeDependency = templateCompositeFrom({ - annotation: `exposeDependency`, - - compose: false, - - inputs: { - dependency: input.staticDependency({acceptsNull: true}), - }, - - steps: () => [ - { - dependencies: [input('dependency')], - compute: ({ - [input('dependency')]: dependency - }) => dependency, - }, - ], -}); - -// Exposes a constant value exactly as it is; like exposeDependency, this -// is typically the base of a composition serving as a particular property -// descriptor. It generally follows steps which will conditionally early -// exit with some other value, with the exposeConstant base serving as the -// fallback default value. -export const exposeConstant = templateCompositeFrom({ - annotation: `exposeConstant`, - - compose: false, - - inputs: { - value: input.staticValue(), - }, - - steps: () => [ - { - dependencies: [input('value')], - compute: ({ - [input('value')]: value, - }) => value, - }, - ], -}); - -// Checks the availability of a dependency and provides the result to later -// steps under '#availability' (by default). This is mainly intended for use -// by the more specific utilities, which you should consider using instead. -// Customize {mode} to select one of these modes, or default to 'null': -// -// * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null, undefined, nor an empty -// array. -// * 'falsy': Check that the value isn't false when treated as a boolean -// (nor an empty array). Keep in mind this will also be false -// for values like zero and the empty string! -// - -const inputAvailabilityCheckMode = () => input({ - validate: is('null', 'empty', 'falsy'), - defaultValue: 'null', -}); - -export const withResultOfAvailabilityCheck = templateCompositeFrom({ - annotation: `withResultOfAvailabilityCheck`, - - inputs: { - from: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - outputs: ['#availability'], - - steps: () => [ - { - dependencies: [input('from'), input('mode')], - - compute: (continuation, { - [input('from')]: value, - [input('mode')]: mode, - }) => { - let availability; - - switch (mode) { - case 'null': - availability = value !== undefined && value !== null; - break; - - case 'empty': - availability = value !== undefined && !empty(value); - break; - - case 'falsy': - availability = !!value && (!Array.isArray(value) || !empty(value)); - break; - } - - return continuation({'#availability': availability}); - }, - }, - ], -}); - -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('dependency')], - compute: (continuation, { - ['#availability']: availability, - [input('dependency')]: dependency, - }) => - (availability - ? continuation.exit(dependency) - : continuation()), - }, - ], -}); - -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! Also provide {validate} here to conveniently -// set a custom validation check for this property's update value. -export const exposeUpdateValueOrContinue = templateCompositeFrom({ - annotation: `exposeUpdateValueOrContinue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - - validate: input({ - type: 'function', - defaultValue: null, - }), - }, - - update: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - - steps: () => [ - exposeDependencyOrContinue({ - dependency: input.updateValue(), - mode: input('mode'), - }), - ], -}); - -// Early exits if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutDependency = templateCompositeFrom({ - annotation: `exitWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutUpdateValue = templateCompositeFrom({ - annotation: `exitWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue(), - mode: input('mode'), - value: input('value'), - }), - ], -}); - -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutDependency = templateCompositeFrom({ - annotation: `raiseOutputWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Raises if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ - annotation: `raiseOutputWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input.updateValue(), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, or the object doesn't have the listed property, -// the provided dependency will also be null. -export const withPropertyFromObject = templateCompositeFrom({ - annotation: `withPropertyFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - property: input({type: 'string'}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), - - steps: () => [ - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), - }, - - { - dependencies: [ - '#output', - input('object'), - input('property'), - ], - - compute: (continuation, { - ['#output']: output, - [input('object')]: object, - [input('property')]: property, - }) => continuation({ - [output]: - (object === null - ? null - : object[property] ?? null), - }), - }, - ], -}); - -// Gets the listed properties from some object, providing each property's value -// as a dependency prefixed with the same name as the object (by default). -// If the object itself is null, all provided dependencies will be null; -// if it's missing only select properties, those will be provided as null. -export const withPropertiesFromObject = templateCompositeFrom({ - annotation: `withPropertiesFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - - properties: input({ - type: 'array', - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`)) - : ['#object']), - - steps: () => [ - { - dependencies: [input('object'), input('properties')], - compute: (continuation, { - [input('object')]: object, - [input('properties')]: properties, - }) => continuation({ - ['#entries']: - (object === null - ? properties.map(property => [property, null]) - : properties.map(property => [property, object[property]])), - }), - }, - - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#entries', - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#entries']: entries, - }) => - (properties - ? continuation( - Object.fromEntries( - entries.map(([property, value]) => [ - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`), - value ?? null, - ]))) - : continuation({ - ['#object']: - Object.fromEntries(entries), - })), - }, - ], -}); - -// Gets a property from each of a list of objects (in a dependency) and -// provides the results. This doesn't alter any list indices, so positions -// which were null in the original list are kept null here. Objects which don't -// have the specified property are retained in-place as null. -export function withPropertyFromList({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; -} - -// Gets the listed properties from each of a list of objects, providing lists -// of property values each into a dependency prefixed with the same name as the -// list (by default). Like withPropertyFromList, this doesn't alter indices. -export const withPropertiesFromList = templateCompositeFrom({ - annotation: `withPropertiesFromList`, - - inputs: { - list: input({type: 'array'}), - - properties: input({ - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`)) - : ['#lists']), - - steps: () => [ - { - dependencies: [input('list'), input('properties')], - compute: (continuation, { - [input('list')]: list, - [input('properties')]: properties, - }) => continuation({ - ['#lists']: - Object.fromEntries( - properties.map(property => [ - property, - list.map(item => item[property] ?? null), - ])), - }), - }, - - { - dependencies: [ - input.staticDependency('list'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#lists', - ], - - compute: (continuation, { - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#lists']: lists, - }) => - (properties - ? continuation( - Object.fromEntries( - properties.map(property => [ - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`), - lists[property], - ]))) - : continuation({'#lists': lists})), - }, - ], -}); - -// Replaces items of a list, which are null or undefined, with some fallback -// value. By default, this replaces the passed dependency. -export const fillMissingListItems = templateCompositeFrom({ - annotation: `fillMissingListItems`, - - inputs: { - list: input({type: 'array'}), - fill: input({acceptsNull: true}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [input('list'), input('fill')], - compute: (continuation, { - [input('list')]: list, - [input('fill')]: fill, - }) => continuation({ - ['#filled']: - list.map(item => item ?? fill), - }), - }, - - { - dependencies: [input.staticDependency('list'), '#filled'], - compute: (continuation, { - [input.staticDependency('list')]: list, - ['#filled']: filled, - }) => continuation({ - [list ?? '#list']: - filled, - }), - }, - ], -}); - -// Filters particular values out of a list. Note that this will always -// completely skip over null, but can be used to filter out any other -// primitive or object value. -export const excludeFromList = templateCompositeFrom({ - annotation: `excludeFromList`, - - inputs: { - list: input(), - - item: input({defaultValue: null}), - items: input({type: 'array', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [ - input.staticDependency('list'), - input('list'), - input('item'), - input('items'), - ], - - compute: (continuation, { - [input.staticDependency('list')]: listName, - [input('list')]: listContents, - [input('item')]: excludeItem, - [input('items')]: excludeItems, - }) => continuation({ - [listName ?? '#list']: - listContents.filter(item => { - if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && excludeItems.includes(item)) return false; - return true; - }), - }), - }, - ], -}); - -// Flattens an array with one level of nested arrays, providing as dependencies -// both the flattened array as well as the original starting indices of each -// successive source array. -export const withFlattenedList = templateCompositeFrom({ - annotation: `withFlattenedList`, - - inputs: { - list: input({type: 'array'}), - }, - - outputs: ['#flattenedList', '#flattenedIndices'], - - steps: () => [ - { - dependencies: [input('list')], - compute(continuation, { - [input('list')]: sourceList, - }) { - const flattenedList = sourceList.flat(); - const indices = []; - let lastEndIndex = 0; - for (const {length} of sourceList) { - indices.push(lastEndIndex); - lastEndIndex += length; - } - - return continuation({ - ['#flattenedList']: flattenedList, - ['#flattenedIndices']: indices, - }); - }, - }, - ], -}); - -// After mapping the contents of a flattened array in-place (being careful to -// retain the original indices by replacing unmatched results with null instead -// of filtering them out), this function allows for recombining them. It will -// filter out null and undefined items by default (pass {filter: false} to -// disable this). -export const withUnflattenedList = templateCompositeFrom({ - annotation: `withUnflattenedList`, - - inputs: { - list: input({ - type: 'array', - defaultDependency: '#flattenedList', - }), - - indices: input({ - validate: validateArrayItems(isWholeNumber), - defaultDependency: '#flattenedIndices', - }), - - filter: input({ - type: 'boolean', - defaultValue: true, - }), - }, - - outputs: ['#unflattenedList'], - - steps: () => [ - { - dependencies: [input('list'), input('indices'), input('filter')], - compute(continuation, { - [input('list')]: list, - [input('indices')]: indices, - [input('filter')]: filter, - }) { - const unflattenedList = []; - - for (let i = 0; i < indices.length; i++) { - const startIndex = indices[i]; - const endIndex = - (i === indices.length - 1 - ? list.length - : indices[i + 1]); - - const values = list.slice(startIndex, endIndex); - unflattenedList.push( - (filter - ? values.filter(value => value !== null && value !== undefined) - : values)); - } - - return continuation({ - ['#unflattenedList']: unflattenedList, - }); - }, - }, - ], -}); |