diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-10-01 17:01:21 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-10-01 17:04:16 -0300 |
commit | ab7591e45e7e31b4e2c0e2f81e224672145993fa (patch) | |
tree | 11dcccc57e71424baa3b73a3eca58dabc56dca05 /src/data/things/thing.js | |
parent | dfcf911501211bbfc64b8ce6a964b70c6406447f (diff) |
data, test: refactor utilities into own file
Primarily for more precies test coverage mapping, but also to make navigation a bit easier and consolidate complex functions with lots of imports out of the same space as other, more simple or otherwise specialized files.
Diffstat (limited to 'src/data/things/thing.js')
-rw-r--r-- | src/data/things/thing.js | 713 |
1 files changed, 2 insertions, 711 deletions
diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1302e17..a47f8506 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1,48 +1,9 @@ -// Thing: base class for wiki data types, providing wiki-specific utility -// functions on top of essential CacheableObject behavior. +// Thing: base class for wiki data types, providing interfaces generally useful +// to all wiki data objects on top of foundational CacheableObject behavior. import {inspect} from 'node:util'; import {colors} from '#cli'; -import find from '#find'; -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, - 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'; @@ -77,673 +38,3 @@ export default class Thing extends CacheableObject { 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; - }, - }, - }; -} - -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. -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([])}), - ], - }); -} - -// 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 ?? [], - }, - }; -} - -const thingClassInput = { - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - 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'), - }), - - 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'), - }), - - 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, - }), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('contribs'), - mode: input.value('empty'), - }), - - 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'), - }), - - 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([]), - }), - - { - dependencies: ['commentary'], - compute: (continuation, {commentary}) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, - - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - find: input.value(find.artist), - }).outputs({ - '#resolvedReferenceList': '#artists', - }), - - { - flags: {expose: true}, - - expose: { - 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, - }), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedContribs'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('from'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedContribs']: [], - }), - }), - - withPropertiesFromList({ - list: input('from'), - properties: input.value(['who', 'what']), - prefix: input.value('#contribs'), - }), - - 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, - }), - - value: input({defaultValue: null}), - }, - - steps: () => [ - withResolvedContribs({ - from: input('contribs'), - }), - - withResultOfAvailabilityCheck({ - from: '#resolvedContribs', - mode: input.value('empty'), - }), - - { - 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, - }), - }), - - 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, - }), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), - }, - - outputs: ['#resolvedReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - raiseOutputWithoutDependency({ - dependency: input('list'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedReferenceList']: [], - }), - }), - - { - 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'})), - }), - }, - - { - 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`); - } - }, - }, - ], -}); - -// Check out the info on reverseReferenceList! -// This is its composable form. -export const withReverseReferenceList = templateCompositeFrom({ - annotation: `withReverseReferenceList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - { - 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)), - }), - }, - ], -}); |