diff options
Diffstat (limited to 'src/data/things/thing.js')
-rw-r--r-- | src/data/things/thing.js | 994 |
1 files changed, 661 insertions, 333 deletions
diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5705ee7e..f1302e17 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -3,419 +3,747 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; -import {getKebabCase} from '#wiki-data'; +import {stitchArrays, unique} from '#sugar'; +import {filterMultipleArrays, getKebabCase} from '#wiki-data'; +import {is} from '#validators'; + +import { + compositeFrom, + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, + withResultOfAvailabilityCheck, + withPropertiesFromList, +} from '#composite'; import { isAdditionalFileList, isBoolean, - isCommentary, isColor, + isCommentary, isContributionList, isDate, + isDimensions, isDirectory, + isDuration, isFileExtension, isName, isString, + isType, isURL, validateArrayItems, validateInstanceOf, validateReference, validateReferenceList, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); + static referenceType = Symbol.for('Thing.referenceType'); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); - // Regularly reused property descriptors, for ease of access and generally - // duplicating less code across wiki data types. These are specialized utility - // functions, so check each for how its own arguments behave! - static common = { - name: (defaultName) => ({ - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }), + // Default custom inspect function, which may be overridden by Thing + // subclasses. This will be used when displaying aggregate errors and other + // command-line logging - it's the place to provide information useful in + // identifying the Thing being presented. + [inspect.custom]() { + const cname = this.constructor.name; - color: () => ({ - flags: {update: true, expose: true}, - update: {validate: isColor}, - }), + return ( + (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) + + (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '') + ); + } - directory: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, + static getReference(thing) { + if (!thing.constructor[Thing.referenceType]) { + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + } + + if (!thing.directory) { + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + } + + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; + } +} + +// Property descriptor templates +// +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! + +export function name(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} + +export function color() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} + +export function directory() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; }, - }), + }, + }; +} - urls: () => ({ - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }), +export function urls() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: (value) => value ?? []}, + }; +} - // A file extension! Or the default, if provided when calling this. - fileExtension: (defaultFileExtension = null) => ({ - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }), +// A file extension! Or the default, if provided when calling this. +export function fileExtension(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} + +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. +export function dimensions() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} + +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. +export function duration() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} + +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! +export function flag(defaultValue = false) { + // TODO: ^ Are you actually kidding me + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } + + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} + +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. +export function simpleDate() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} + +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. +export function simpleString() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} + +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. +export function externalFunction() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} + +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// +export function contributionList() { + return compositeFrom({ + annotation: `contributionList`, + + compose: false, + + update: {validate: isContributionList}, + + steps: [ + withResolvedContribs({from: input.updateValue()}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: input.value([])}), + ], + }); +} - // Straightforward flag descriptor for a variety of property purposes. - // Provide a default value, true or false! - flag: (defaultValue = false) => { - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; +// Artist commentary! Generally present on tracks and albums. +export function commentary() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} + +// This is a somewhat more involved data structure - it's for additional +// or "bonus" files associated with albums or tracks (or anything else). +// It's got this form: +// +// [ +// {title: 'Booklet', files: ['Booklet.pdf']}, +// { +// title: 'Wallpaper', +// description: 'Cool Wallpaper!', +// files: ['1440x900.png', '1920x1080.png'] +// }, +// {title: 'Alternate Covers', description: null, files: [...]}, +// ... +// ] +// +export function additionalFiles() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], }, + }; +} - // General date type, used as the descriptor for a bunch of properties. - // This isn't dynamic though - it won't inherit from a date stored on - // another object, for example. - simpleDate: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDate}, - }), +const thingClassInput = { + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } - // General string type. This should probably generally be avoided in favor - // of more specific validation, but using it makes it easy to find where we - // might want to improve later, and it's a useful shorthand meanwhile. - simpleString: () => ({ - flags: {update: true, expose: true}, - update: {validate: isString}, + return true; + }, +}; + +// A reference list! Keep in mind this is for general references to wiki +// objects of (usually) other Thing subclasses, not specifically leitmotif +// references in tracks (although that property uses referenceList too!). +// +// The underlying function validateReferenceList expects a string like +// 'artist' or 'track', but this utility keeps from having to hard-code the +// string in multiple places by referencing the value saved on the class +// instead. +export const referenceList = templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: input.staticValue(thingClassInput), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + + steps: () => [ + withResolvedReferenceList({ + list: input.updateValue(), + data: input('data'), + find: input('find'), }), - // External function. These should only be used as dependencies for other - // properties, so they're left unexposed. - externalFunction: ({expose = false} = {}) => ({ - flags: {update: true, expose}, - update: {validate: (t) => typeof t === 'function'}, + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}); + +// Corresponding function for a single reference. +export const singleReference = templateCompositeFrom({ + annotation: `singleReference`, + + compose: false, + + inputs: { + class: input(thingClassInput), + find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: false}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; + }, + + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('find'), }), - // Super simple "contributions by reference" list, used for a variety of - // properties (Artists, Cover Artists, etc). This is the property which is - // externally provided, in the form: - // - // [ - // {who: 'Artist Name', what: 'Viola'}, - // {who: 'artist:john-cena', what: null}, - // ... - // ] - // - // ...processed from YAML, spreadsheet, or any other kind of input. - contribsByRef: () => ({ - flags: {update: true, expose: true}, - update: {validate: isContributionList}, + exposeDependency({dependency: '#resolvedReference'}), + ], +}); + +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. +export const contribsPresent = templateCompositeFrom({ + annotation: `contribsPresent`, + + compose: false, + + inputs: { + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, }), + }, - // Artist commentary! Generally present on tracks and albums. - commentary: () => ({ - flags: {update: true, expose: true}, - update: {validate: isCommentary}, + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('contribs'), + mode: input.value('empty'), }), - // This is a somewhat more involved data structure - it's for additional - // or "bonus" files associated with albums or tracks (or anything else). - // It's got this form: - // - // [ - // {title: 'Booklet', files: ['Booklet.pdf']}, - // { - // title: 'Wallpaper', - // description: 'Cool Wallpaper!', - // files: ['1440x900.png', '1920x1080.png'] - // }, - // {title: 'Alternate Covers', description: null, files: [...]}, - // ... - // ] - // - additionalFiles: () => ({ - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, + exposeDependency({dependency: '#availability'}), + ], +}); + +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. +export const reverseReferenceList = templateCompositeFrom({ + annotation: `reverseReferenceList`, + + compose: false, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), }), - // A reference list! Keep in mind this is for general references to wiki - // objects of (usually) other Thing subclasses, not specifically leitmotif - // references in tracks (although that property uses referenceList too!). - // - // The underlying function validateReferenceList expects a string like - // 'artist' or 'track', but this utility keeps from having to hard-code the - // string in multiple places by referencing the value saved on the class - // instead. - referenceList: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); + +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. +export function wikiData(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), }, + }; +} + +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. +export const commentatorArtists = templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), - // Corresponding function for a single reference. - singleReference: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - // Corresponding dynamic property to referenceList, which takes the values - // in the provided property and searches the specified wiki data for - // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList: ( - referenceListProperty, - thingDataProperty, - findFn - ) => ({ + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { flags: {expose: true}, expose: { - dependencies: [referenceListProperty, thingDataProperty], - compute: ({ - [referenceListProperty]: refs, - [thingDataProperty]: thingData, - }) => - refs && thingData - ? refs - .map((ref) => findFn(ref, thingData, {mode: 'quiet'})) - .filter(Boolean) - : [], + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), }, + }, + ], +}); + +// Compositional utilities + +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData({referenceType, allowMixedTypes}), + acceptsNull: true, + }); +} + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export const withResolvedContribs = templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + from: input({ + validate: isContributionList, + acceptsNull: true, }), - // Corresponding function for a single reference. - dynamicThingFromSingleReference: ( - singleReferenceProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, - expose: { - dependencies: [singleReferenceProperty, thingDataProperty], - compute: ({ - [singleReferenceProperty]: ref, - [thingDataProperty]: thingData, - }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null), - }, + outputs: ['#resolvedContribs'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedContribs']: [], + }), }), - // Corresponding dynamic property to contribsByRef, which takes the values - // in the provided property and searches the object's artistData for - // matching actual Artist objects. The computed structure has the same form - // as contribsByRef, but with Artist objects instead of string references: - // - // [ - // {who: (an Artist), what: 'Viola'}, - // {who: (an Artist), what: null}, - // ... - // ] - // - // Contributions whose "who" values don't match anything in artistData are - // filtered out. (So if the list is all empty, chances are that either the - // reference list is somehow messed up, or artistData isn't being provided - // properly.) - dynamicContribs: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - contribsByRef && artistData - ? contribsByRef - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who) - : [], - }, + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), }), - // Dynamically inherit a contribution list from some other object, if it - // hasn't been overridden on this object. This is handy for solo albums - // where all tracks have the same artist, for example. - dynamicInheritContribs: ( - // If this property is explicitly false, the contribution list returned - // will always be empty. - nullerProperty, - - // Property holding contributions on the current object. - contribsByRefProperty, - - // Property holding corresponding "default" contributions on the parent - // object, which will fallen back to if the object doesn't have its own - // contribs. - parentContribsByRefProperty, - - // Data array to search in and "find" function to locate parent object - // (which will be passed the child object and the wiki data array). - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - expose: { - dependencies: [ - contribsByRefProperty, - thingDataProperty, - nullerProperty, - 'artistData', - ].filter(Boolean), - - compute({ - [Thing.instance]: thing, - [nullerProperty]: nuller, - [contribsByRefProperty]: contribsByRef, - [thingDataProperty]: thingData, - artistData, - }) { - if (!artistData) return []; - if (nuller === false) return []; - const refs = - contribsByRef ?? - findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty]; - if (!refs) return []; - return refs - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who); - }, + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + find: input.value(find.artist), + notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + ['#resolvedContribs']: stitchArrays({who, what}), + }); }, + }, + ], +}); + +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. +export const exitWithoutContribs = templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + contribs: input({ + validate: isContributionList, + acceptsNull: true, }), - // Nice 'n simple shorthand for an exposed-only flag which is true when any - // contributions are present in the specified property. - contribsPresent: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + value: input({defaultValue: null}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), }), - // Neat little shortcut for "reversing" the reference lists stored on other - // things - for example, tracks specify a "referenced tracks" property, and - // you would use this to compute a corresponding "referenced *by* tracks" - // property. Naturally, the passed ref list property is of the things in the - // wiki data provided, not the requesting Thing itself. - reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), - expose: { - dependencies: [thingDataProperty], + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. +export const withResolvedReference = templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + ref: input({type: 'string', acceptsNull: true}), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedReference'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({ + ['#resolvedReference']: null, + }), + }), - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => - thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute(continuation, { + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raiseOutput({ + ['#resolvedReference']: match ?? null, + }); }, + }, + ], +}); + +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export const withResolvedReferenceList = templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, }), - // Corresponding function for single references. Note that the return value - // is still a list - this is for matching all the objects whose single - // reference (in the given property) matches this Thing. - reverseSingleReference: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), - expose: { - dependencies: [thingDataProperty], + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => - thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], - }, + outputs: ['#resolvedReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), }), - // General purpose wiki data constructor, for properties like artistData, - // trackData, etc. - wikiData: (thingClass) => ({ - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedReferenceList']: [], + }), }), - // This one's kinda tricky: it parses artist "references" from the - // commentary content, and finds the matching artist for each reference. - // This is mostly useful for credits and listings on artist pages. - commentatorArtists: () => ({ - flags: {expose: true}, + { + dependencies: [input('list'), input('data'), input('find')], + compute: (continuation, { + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], + { + dependencies: ['#matches'], + compute: (continuation, {'#matches': matches}) => + (matches.every(match => match) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) + : continuation()), + }, + + { + dependencies: ['#matches', input('notFoundMode')], + compute(continuation, { + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); + + case 'null': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } }, - }), - }; + }, + ], +}); - // Default custom inspect function, which may be overridden by Thing - // subclasses. This will be used when displaying aggregate errors and other - // command-line logging - it's the place to provide information useful in - // identifying the Thing being presented. - [inspect.custom]() { - const cname = this.constructor.name; +// Check out the info on reverseReferenceList! +// This is its composable form. +export const withReverseReferenceList = templateCompositeFrom({ + annotation: `withReverseReferenceList`, - return ( - (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) + - (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '') - ); - } + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); - } + outputs: ['#reverseReferenceList'], - if (!thing.directory) { - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - } + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } -} + { + dependencies: [input.myself(), input('data'), input('list')], + + compute: (continuation, { + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, + }) => + continuation({ + ['#reverseReferenceList']: + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], +}); |