From ab7591e45e7e31b4e2c0e2f81e224672145993fa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:01:21 -0300 Subject: 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. --- src/data/things/album.js | 157 +------- src/data/things/art-tag.js | 10 +- src/data/things/artist.js | 6 +- src/data/things/composite.js | 727 +------------------------------------ src/data/things/flash.js | 6 +- src/data/things/group.js | 6 +- src/data/things/homepage-layout.js | 16 +- src/data/things/language.js | 9 +- src/data/things/news-entry.js | 6 +- src/data/things/static-page.js | 6 +- src/data/things/thing.js | 713 +----------------------------------- src/data/things/track.js | 471 ++---------------------- src/data/things/wiki-info.js | 6 +- 13 files changed, 97 insertions(+), 2042 deletions(-) (limited to 'src/data/things') diff --git a/src/data/things/album.js b/src/data/things/album.js index fd8a71d3..e3ac1651 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,21 +1,12 @@ +import {input} from '#composite'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; -import {isDate, isTrackSectionList} from '#validators'; -import {filterMultipleArrays} from '#wiki-data'; +import {isDate} from '#validators'; + +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {exitWithoutContribs} from '#composite/wiki-data'; import { - exitWithoutDependency, - exitWithoutUpdateValue, - exposeDependency, - exposeUpdateValueOrContinue, - input, - fillMissingListItems, - withFlattenedList, - withPropertiesFromList, - withUnflattenedList, -} from '#composite'; - -import Thing, { additionalFiles, commentary, color, @@ -24,7 +15,6 @@ import Thing, { contributionList, dimensions, directory, - exitWithoutContribs, fileExtension, flag, name, @@ -33,8 +23,14 @@ import Thing, { simpleString, urls, wikiData, - withResolvedReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + withTracks, + withTrackSections, +} from '#composite/things/album'; + +import Thing from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -101,100 +97,8 @@ export class Album extends Thing { additionalFiles: additionalFiles(), trackSections: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutUpdateValue({ - mode: input.value('empty'), - value: input.value([]), - }), - - withPropertiesFromList({ - list: input.updateValue(), - prefix: input.value('#sections'), - properties: input.value([ - 'tracks', - 'dateOriginallyReleased', - 'isDefaultTrackSection', - 'color', - ]), - }), - - fillMissingListItems({ - list: '#sections.tracks', - fill: input.value([]), - }), - - fillMissingListItems({ - list: '#sections.isDefaultTrackSection', - fill: input.value(false), - }), - - fillMissingListItems({ - list: '#sections.color', - fill: input.dependency('color'), - }), - - withFlattenedList({ - list: '#sections.tracks', - }).outputs({ - ['#flattenedList']: '#trackRefs', - ['#flattenedIndices']: '#sections.startIndex', - }), - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - notFoundMode: input.value('null'), - find: input.value(find.track), - }).outputs({ - ['#resolvedReferenceList']: '#tracks', - }), - - withUnflattenedList({ - list: '#tracks', - indices: '#sections.startIndex', - }).outputs({ - ['#unflattenedList']: '#sections.tracks', - }), - - { - flags: {update: true, expose: true}, - - update: {validate: isTrackSectionList}, - - expose: { - dependencies: [ - '#sections.tracks', - '#sections.color', - '#sections.dateOriginallyReleased', - '#sections.isDefaultTrackSection', - '#sections.startIndex', - ], - - transform(trackSections, { - '#sections.tracks': tracks, - '#sections.color': color, - '#sections.dateOriginallyReleased': dateOriginallyReleased, - '#sections.isDefaultTrackSection': isDefaultTrackSection, - '#sections.startIndex': startIndex, - }) { - filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, - tracks => !empty(tracks)); - - return stitchArrays({ - tracks, - color, - dateOriginallyReleased, - isDefaultTrackSection, - startIndex, - }); - } - }, - }, + withTrackSections(), + exposeDependency({dependency: '#trackSections'}), ], artistContribs: contributionList(), @@ -231,33 +135,8 @@ export class Album extends Thing { hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutDependency({ - dependency: 'trackSections', - mode: input.value('empty'), - value: input.value([]), - }), - - { - dependencies: ['trackSections'], - compute: (continuation, {trackSections}) => - continuation({ - '#trackRefs': trackSections - .flatMap(section => section.tracks ?? []), - }), - }, - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - find: input.value(find.track), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), + withTracks(), + exposeDependency({dependency: '#tracks'}), ], }); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index ba3cbd0d..1266a4e0 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,14 +1,18 @@ -import {exposeUpdateValueOrContinue, input} from '#composite'; +import {input} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; import {isName} from '#validators'; -import Thing, { +import {exposeUpdateValueOrContinue} from '#composite/control-flow'; + +import { color, directory, flag, name, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 085e5663..ff9f8aee 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -2,7 +2,7 @@ import {input} from '#composite'; import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing, { +import { directory, fileExtension, flag, @@ -11,7 +11,9 @@ import Thing, { singleReference, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; 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, - }); - }, - }, - ], -}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index c3f90260..8fb1edfa 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,7 +9,7 @@ import { oneOf, } from '#validators'; -import Thing, { +import { color, contributionList, fileExtension, @@ -19,7 +19,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; diff --git a/src/data/things/group.js b/src/data/things/group.js index 0b117801..d5ae03e7 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,7 +1,7 @@ import {input} from '#composite'; import find from '#find'; -import Thing, { +import { color, directory, name, @@ -9,7 +9,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bcf99e80..de9d0e50 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,10 +1,6 @@ +import {input} from '#composite'; import find from '#find'; -import { - exposeDependency, - input, -} from '#composite'; - import { is, isCountingNumber, @@ -16,14 +12,18 @@ import { validateReference, } from '#validators'; -import Thing, { +import {exposeDependency} from '#composite/control-flow'; +import {withResolvedReference} from '#composite/wiki-data'; + +import { color, name, referenceList, simpleString, wikiData, - withResolvedReference, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ diff --git a/src/data/things/language.js b/src/data/things/language.js index a325d6a6..fe74f7bf 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,13 +1,14 @@ import {Tag} from '#html'; import {isLanguageCode} from '#validators'; -import CacheableObject from './cacheable-object.js'; - -import Thing, { +import { externalFunction, flag, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 6984874e..ba065c25 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,9 +1,11 @@ -import Thing, { +import { directory, name, simpleDate, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 0133e0b6..f03e4405 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,10 +1,12 @@ import {isName} from '#validators'; -import Thing, { +import { directory, name, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; 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>/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)), - }), - }, - ], -}); diff --git a/src/data/things/track.js b/src/data/things/track.js index c77bf889..193ad891 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,35 +1,28 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {input} from '#composite'; import find from '#find'; -import {empty} from '#sugar'; import { - exitWithoutDependency, - excludeFromList, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - exposeUpdateValueOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withPropertyFromObject, -} from '#composite'; - -import { - is, - isBoolean, isColor, isContributionList, isDate, isFileExtension, - validateWikiData, } from '#validators'; -import CacheableObject from './cacheable-object.js'; +import {withPropertyFromObject} from '#composite/data'; +import {withResolvedContribs} from '#composite/wiki-data'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; -import Thing, { +import { additionalFiles, commentary, commentatorArtists, @@ -45,10 +38,22 @@ import Thing, { simpleString, urls, wikiData, - withResolvedContribs, - withResolvedReference, - withReverseReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + exitWithoutUniqueCoverArt, + inheritFromOriginalRelease, + trackReverseReferenceList, + withAlbum, + withAlwaysReferenceByDirectory, + withContainingTrackSection, + withHasUniqueCoverArt, + withOtherReleases, + withPropertyFromAlbum, +} from '#composite/things/track'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Track extends Thing { static [Thing.referenceType] = 'track'; @@ -84,39 +89,9 @@ export class Track extends Thing { exposeDependency({dependency: '#album.color'}), ], - // Controls how find.track works - it'll never be matched by a reference - // just to the track's name, which means you don't have to always reference - // some *other* (much more commonly referenced) track by directory instead - // of more naturally by name. alwaysReferenceByDirectory: [ - exposeUpdateValueOrContinue({ - validate: input.value(isBoolean), - }), - - excludeFromList({ - list: 'trackData', - item: input.myself(), - }), - - withOriginalRelease({ - data: '#trackData', - }), - - exitWithoutDependency({ - dependency: '#originalRelease', - value: input.value(false), - }), - - withPropertyFromObject({ - object: '#originalRelease', - property: input.value('name'), - }), - - { - dependencies: ['name', '#originalRelease.name'], - compute: ({name, '#originalRelease.name': originalName}) => - name === originalName, - }, + withAlwaysReferenceByDirectory(), + exposeDependency({dependency: '#alwaysReferenceByDirectory'}), ], // Disables presenting the track as though it has its own unique artwork. @@ -298,61 +273,20 @@ export class Track extends Thing { exposeDependency({dependency: '#album.date'}), ], - // Whether or not the track has "unique" cover artwork - a cover which is - // specifically associated with this track in particular, rather than with - // the track's album as a whole. This is typically used to select between - // displaying the track artwork and a fallback, such as the album artwork - // or a placeholder. (This property is named hasUniqueCoverArt instead of - // the usual hasCoverArt to emphasize that it does not inherit from the - // album.) hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), ], otherReleases: [ - exitWithoutDependency({ - dependency: 'trackData', - mode: input.value('empty'), - }), - - withOriginalRelease({ - selfIfOriginal: input.value(true), - }), - - { - flags: {expose: true}, - expose: { - dependencies: [input.myself(), '#originalRelease', 'trackData'], - compute: ({ - [input.myself()]: thisTrack, - ['#originalRelease']: originalRelease, - trackData, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, - }, + withOtherReleases(), + exposeDependency({dependency: '#otherReleases'}), ], - // Specifically exclude re-releases from this list - while it's useful to - // get from a re-release to the tracks it references, re-releases aren't - // generally relevant from the perspective of the tracks being referenced. - // Filtering them from data here hides them from the corresponding field - // on the site (obviously), and has the bonus of not counting them when - // counting the number of times a track has been referenced, for use in - // the "Tracks - by Times Referenced" listing page (or other data - // processing). referencedByTracks: trackReverseReferenceList({ list: input.value('referencedTracks'), }), - // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ list: input.value('sampledTracks'), }), @@ -386,344 +320,3 @@ export class Track extends Thing { return parts.join(''); } } - -// Early exits with a value inherited from the original release, if -// this track is a rerelease, and otherwise continues with no further -// dependencies provided. If allowOverride is true, then the continuation -// will also be called if the original release exposed the requested -// property as null. -export const inheritFromOriginalRelease = templateCompositeFrom({ - annotation: `Track.inheritFromOriginalRelease`, - - inputs: { - property: input({type: 'string'}), - allowOverride: input({type: 'boolean', defaultValue: false}), - }, - - steps: () => [ - withOriginalRelease(), - - { - dependencies: [ - '#originalRelease', - input('property'), - input('allowOverride'), - ], - - compute: (continuation, { - ['#originalRelease']: originalRelease, - [input('property')]: originalProperty, - [input('allowOverride')]: allowOverride, - }) => { - if (!originalRelease) return continuation(); - - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation(); - - return continuation.exit(value); - }, - }, - ], -}); - -// Gets the track's album. This will early exit if albumData is missing. -// By default, if there's no album whose list of tracks includes this track, -// the output dependency will be null; set {notFoundMode: 'exit'} to early -// exit instead. -export const withAlbum = templateCompositeFrom({ - annotation: `Track.withAlbum`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#album'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: [input.myself(), 'albumData'], - compute: (continuation, { - [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)), - }), - }, - - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), - }, - ], -}); - -// Gets a single property from this track's album, providing it as the same -// property name prefixed with '#album.' (by default). If the track's album -// isn't available, then by default, the property will be provided as null; -// set {notFoundMode: 'exit'} to early exit instead. -export const withPropertyFromAlbum = templateCompositeFrom({ - annotation: `withPropertyFromAlbum`, - - inputs: { - property: input.staticValue({type: 'string'}), - - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ({ - [input.staticValue('property')]: property, - }) => ['#album.' + property], - - steps: () => [ - withAlbum({ - notFoundMode: input('notFoundMode'), - }), - - withPropertyFromObject({ - object: '#album', - property: input('property'), - }), - - { - dependencies: ['#value', input.staticValue('property')], - compute: (continuation, { - ['#value']: value, - [input.staticValue('property')]: property, - }) => continuation({ - ['#album.' + property]: value, - }), - }, - ], -}); - -// Gets the track section containing this track from its album's track list. -// If notFoundMode is set to 'exit', this will early exit if the album can't be -// found or if none of its trackSections includes the track for some reason. -export const withContainingTrackSection = templateCompositeFrom({ - annotation: `withContainingTrackSection`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#trackSection'], - - steps: () => [ - withPropertyFromAlbum({ - property: input.value('trackSections'), - notFoundMode: input('notFoundMode'), - }), - - { - dependencies: [ - input.myself(), - input('notFoundMode'), - '#album.trackSections', - ], - - compute(continuation, { - [input.myself()]: track, - [input('notFoundMode')]: notFoundMode, - ['#album.trackSections']: trackSections, - }) { - if (!trackSections) { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raiseOutput({ - ['#trackSection']: trackSection, - }); - } else if (notFoundMode === 'exit') { - return continuation.exit(null); - } else { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - }, - }, - ], -}); - -// Just includes the original release of this track as a dependency. -// If this track isn't a rerelease, then it'll provide null, unless the -// {selfIfOriginal} option is set, in which case it'll provide this track -// itself. Note that this will early exit if the original release is -// specified by reference and that reference doesn't resolve to anything. -// Outputs to '#originalRelease' by default. -export const withOriginalRelease = templateCompositeFrom({ - annotation: `withOriginalRelease`, - - inputs: { - selfIfOriginal: input({type: 'boolean', defaultValue: false}), - - data: input({ - validate: validateWikiData({referenceType: 'track'}), - defaultDependency: 'trackData', - }), - }, - - outputs: ['#originalRelease'], - - steps: () => [ - withResolvedReference({ - ref: 'originalReleaseTrack', - data: input('data'), - find: input.value(find.track), - notFoundMode: input.value('exit'), - }).outputs({ - ['#resolvedReference']: '#originalRelease', - }), - - { - dependencies: [ - input.myself(), - input('selfIfOriginal'), - '#originalRelease', - ], - - compute: (continuation, { - [input.myself()]: track, - [input('selfIfOriginal')]: selfIfOriginal, - ['#originalRelease']: originalRelease, - }) => - continuation({ - ['#originalRelease']: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, - ], -}); - -// The algorithm for checking if a track has unique cover art is used in a -// couple places, so it's defined in full as a compositional step. -export const withHasUniqueCoverArt = templateCompositeFrom({ - annotation: 'withHasUniqueCoverArt', - - outputs: ['#hasUniqueCoverArt'], - - steps: () => [ - { - dependencies: ['disableUniqueCoverArt'], - compute: (continuation, {disableUniqueCoverArt}) => - (disableUniqueCoverArt - ? continuation.raiseOutput({ - ['#hasUniqueCoverArt']: false, - }) - : continuation()), - }, - - withResolvedContribs({from: 'coverArtistContribs'}), - - { - dependencies: ['#resolvedContribs'], - compute: (continuation, { - ['#resolvedContribs']: contribsFromTrack, - }) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raiseOutput({ - ['#hasUniqueCoverArt']: true, - })), - }, - - withPropertyFromAlbum({ - property: input.value('trackCoverArtistContribs'), - }), - - { - dependencies: ['#album.trackCoverArtistContribs'], - compute: (continuation, { - ['#album.trackCoverArtistContribs']: contribsFromAlbum, - }) => - continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - !empty(contribsFromAlbum), - }), - }, - ], -}); - -// Shorthand for checking if the track has unique cover art and exposing a -// fallback value if it isn't. -export const exitWithoutUniqueCoverArt = templateCompositeFrom({ - annotation: `exitWithoutUniqueCoverArt`, - - inputs: { - value: input({defaultValue: null}), - }, - - steps: () => [ - withHasUniqueCoverArt(), - - exitWithoutDependency({ - dependency: '#hasUniqueCoverArt', - mode: input.value('falsy'), - value: input('value'), - }), - ], -}); - -export const trackReverseReferenceList = templateCompositeFrom({ - annotation: `trackReverseReferenceList`, - - compose: false, - - inputs: { - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: 'trackData', - list: input('list'), - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({ - ['#reverseReferenceList']: reverseReferenceList, - }) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ], -}); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index c764b528..0460f272 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -2,14 +2,16 @@ import {input} from '#composite'; import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing, { +import { color, flag, name, referenceList, simpleString, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ -- cgit 1.3.0-6-gf8a5