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. --- .../control-flow/exitWithoutDependency.js | 35 ++++++ .../control-flow/exitWithoutUpdateValue.js | 24 +++++ src/data/composite/control-flow/exposeConstant.js | 26 +++++ .../composite/control-flow/exposeDependency.js | 28 +++++ .../control-flow/exposeDependencyOrContinue.js | 34 ++++++ .../control-flow/exposeUpdateValueOrContinue.js | 40 +++++++ src/data/composite/control-flow/index.js | 9 ++ .../control-flow/inputAvailabilityCheckMode.js | 9 ++ .../control-flow/raiseOutputWithoutDependency.js | 39 +++++++ .../control-flow/raiseOutputWithoutUpdateValue.js | 47 ++++++++ .../control-flow/withResultOfAvailabilityCheck.js | 66 ++++++++++++ src/data/composite/data/excludeFromList.js | 56 ++++++++++ src/data/composite/data/fillMissingListItems.js | 51 +++++++++ src/data/composite/data/index.js | 8 ++ src/data/composite/data/withFlattenedList.js | 47 ++++++++ src/data/composite/data/withPropertiesFromList.js | 92 ++++++++++++++++ .../composite/data/withPropertiesFromObject.js | 87 +++++++++++++++ src/data/composite/data/withPropertyFromList.js | 56 ++++++++++ src/data/composite/data/withPropertyFromObject.js | 69 ++++++++++++ src/data/composite/data/withUnflattenedList.js | 62 +++++++++++ src/data/composite/things/album/index.js | 2 + .../composite/things/album/withTrackSections.js | 119 +++++++++++++++++++++ src/data/composite/things/album/withTracks.js | 51 +++++++++ .../things/track/exitWithoutUniqueCoverArt.js | 26 +++++ src/data/composite/things/track/index.js | 9 ++ .../things/track/inheritFromOriginalRelease.js | 43 ++++++++ .../things/track/trackReverseReferenceList.js | 38 +++++++ src/data/composite/things/track/withAlbum.js | 57 ++++++++++ .../things/track/withAlwaysReferenceByDirectory.js | 52 +++++++++ .../things/track/withContainingTrackSection.js | 63 +++++++++++ .../things/track/withHasUniqueCoverArt.js | 61 +++++++++++ .../composite/things/track/withOriginalRelease.js | 59 ++++++++++ .../composite/things/track/withOtherReleases.js | 40 +++++++ .../things/track/withPropertyFromAlbum.js | 49 +++++++++ .../composite/wiki-data/exitWithoutContribs.js | 47 ++++++++ src/data/composite/wiki-data/index.js | 7 ++ src/data/composite/wiki-data/inputThingClass.js | 23 ++++ src/data/composite/wiki-data/inputWikiData.js | 17 +++ .../composite/wiki-data/withResolvedContribs.js | 77 +++++++++++++ .../composite/wiki-data/withResolvedReference.js | 73 +++++++++++++ .../wiki-data/withResolvedReferenceList.js | 101 +++++++++++++++++ .../wiki-data/withReverseReferenceList.js | 40 +++++++ .../composite/wiki-properties/additionalFiles.js | 30 ++++++ src/data/composite/wiki-properties/color.js | 12 +++ src/data/composite/wiki-properties/commentary.js | 12 +++ .../wiki-properties/commentatorArtists.js | 55 ++++++++++ .../composite/wiki-properties/contribsPresent.js | 30 ++++++ .../composite/wiki-properties/contributionList.js | 35 ++++++ src/data/composite/wiki-properties/dimensions.js | 13 +++ src/data/composite/wiki-properties/directory.js | 23 ++++ src/data/composite/wiki-properties/duration.js | 13 +++ .../composite/wiki-properties/externalFunction.js | 11 ++ .../composite/wiki-properties/fileExtension.js | 13 +++ src/data/composite/wiki-properties/flag.js | 19 ++++ src/data/composite/wiki-properties/index.js | 20 ++++ src/data/composite/wiki-properties/name.js | 11 ++ .../composite/wiki-properties/referenceList.js | 47 ++++++++ .../wiki-properties/reverseReferenceList.js | 30 ++++++ src/data/composite/wiki-properties/simpleDate.js | 14 +++ src/data/composite/wiki-properties/simpleString.js | 14 +++ .../composite/wiki-properties/singleReference.js | 47 ++++++++ src/data/composite/wiki-properties/urls.js | 14 +++ src/data/composite/wiki-properties/wikiData.js | 17 +++ 63 files changed, 2419 insertions(+) create mode 100644 src/data/composite/control-flow/exitWithoutDependency.js create mode 100644 src/data/composite/control-flow/exitWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/exposeConstant.js create mode 100644 src/data/composite/control-flow/exposeDependency.js create mode 100644 src/data/composite/control-flow/exposeDependencyOrContinue.js create mode 100644 src/data/composite/control-flow/exposeUpdateValueOrContinue.js create mode 100644 src/data/composite/control-flow/index.js create mode 100644 src/data/composite/control-flow/inputAvailabilityCheckMode.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutDependency.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/withResultOfAvailabilityCheck.js create mode 100644 src/data/composite/data/excludeFromList.js create mode 100644 src/data/composite/data/fillMissingListItems.js create mode 100644 src/data/composite/data/index.js create mode 100644 src/data/composite/data/withFlattenedList.js create mode 100644 src/data/composite/data/withPropertiesFromList.js create mode 100644 src/data/composite/data/withPropertiesFromObject.js create mode 100644 src/data/composite/data/withPropertyFromList.js create mode 100644 src/data/composite/data/withPropertyFromObject.js create mode 100644 src/data/composite/data/withUnflattenedList.js create mode 100644 src/data/composite/things/album/index.js create mode 100644 src/data/composite/things/album/withTrackSections.js create mode 100644 src/data/composite/things/album/withTracks.js create mode 100644 src/data/composite/things/track/exitWithoutUniqueCoverArt.js create mode 100644 src/data/composite/things/track/index.js create mode 100644 src/data/composite/things/track/inheritFromOriginalRelease.js create mode 100644 src/data/composite/things/track/trackReverseReferenceList.js create mode 100644 src/data/composite/things/track/withAlbum.js create mode 100644 src/data/composite/things/track/withAlwaysReferenceByDirectory.js create mode 100644 src/data/composite/things/track/withContainingTrackSection.js create mode 100644 src/data/composite/things/track/withHasUniqueCoverArt.js create mode 100644 src/data/composite/things/track/withOriginalRelease.js create mode 100644 src/data/composite/things/track/withOtherReleases.js create mode 100644 src/data/composite/things/track/withPropertyFromAlbum.js create mode 100644 src/data/composite/wiki-data/exitWithoutContribs.js create mode 100644 src/data/composite/wiki-data/index.js create mode 100644 src/data/composite/wiki-data/inputThingClass.js create mode 100644 src/data/composite/wiki-data/inputWikiData.js create mode 100644 src/data/composite/wiki-data/withResolvedContribs.js create mode 100644 src/data/composite/wiki-data/withResolvedReference.js create mode 100644 src/data/composite/wiki-data/withResolvedReferenceList.js create mode 100644 src/data/composite/wiki-data/withReverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/additionalFiles.js create mode 100644 src/data/composite/wiki-properties/color.js create mode 100644 src/data/composite/wiki-properties/commentary.js create mode 100644 src/data/composite/wiki-properties/commentatorArtists.js create mode 100644 src/data/composite/wiki-properties/contribsPresent.js create mode 100644 src/data/composite/wiki-properties/contributionList.js create mode 100644 src/data/composite/wiki-properties/dimensions.js create mode 100644 src/data/composite/wiki-properties/directory.js create mode 100644 src/data/composite/wiki-properties/duration.js create mode 100644 src/data/composite/wiki-properties/externalFunction.js create mode 100644 src/data/composite/wiki-properties/fileExtension.js create mode 100644 src/data/composite/wiki-properties/flag.js create mode 100644 src/data/composite/wiki-properties/index.js create mode 100644 src/data/composite/wiki-properties/name.js create mode 100644 src/data/composite/wiki-properties/referenceList.js create mode 100644 src/data/composite/wiki-properties/reverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/simpleDate.js create mode 100644 src/data/composite/wiki-properties/simpleString.js create mode 100644 src/data/composite/wiki-properties/singleReference.js create mode 100644 src/data/composite/wiki-properties/urls.js create mode 100644 src/data/composite/wiki-properties/wikiData.js (limited to 'src/data/composite') diff --git a/src/data/composite/control-flow/exitWithoutDependency.js b/src/data/composite/control-flow/exitWithoutDependency.js new file mode 100644 index 00000000..c660a7ef --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutDependency.js @@ -0,0 +1,35 @@ +// Early exits if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default 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)), + }, + ], +}); diff --git a/src/data/composite/control-flow/exitWithoutUpdateValue.js b/src/data/composite/control-flow/exitWithoutUpdateValue.js new file mode 100644 index 00000000..244b3233 --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutUpdateValue.js @@ -0,0 +1,24 @@ +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import exitWithoutDependency from './exitWithoutDependency.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/control-flow/exposeConstant.js b/src/data/composite/control-flow/exposeConstant.js new file mode 100644 index 00000000..e0435478 --- /dev/null +++ b/src/data/composite/control-flow/exposeConstant.js @@ -0,0 +1,26 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeConstant`, + + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependency.js b/src/data/composite/control-flow/exposeDependency.js new file mode 100644 index 00000000..3aa3d03a --- /dev/null +++ b/src/data/composite/control-flow/exposeDependency.js @@ -0,0 +1,28 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeDependency`, + + compose: false, + + inputs: { + dependency: input.staticDependency({acceptsNull: true}), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependencyOrContinue.js b/src/data/composite/control-flow/exposeDependencyOrContinue.js new file mode 100644 index 00000000..0f7f223e --- /dev/null +++ b/src/data/composite/control-flow/exposeDependencyOrContinue.js @@ -0,0 +1,34 @@ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default 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()), + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeUpdateValueOrContinue.js b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js new file mode 100644 index 00000000..1f94b332 --- /dev/null +++ b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js @@ -0,0 +1,40 @@ +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. +// +// See withResultOfAvailabilityCheck for {mode} options. +// +// Provide {validate} here to conveniently set a custom validation check +// for this property's update value. +// + +import {input, templateCompositeFrom} from '#composite'; + +import exposeDependencyOrContinue from './exposeDependencyOrContinue.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default 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'), + }), + ], +}); diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js new file mode 100644 index 00000000..dfc53db7 --- /dev/null +++ b/src/data/composite/control-flow/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutDependency} from './exitWithoutDependency.js'; +export {default as exitWithoutUpdateValue} from './exitWithoutUpdateValue.js'; +export {default as exposeConstant} from './exposeConstant.js'; +export {default as exposeDependency} from './exposeDependency.js'; +export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js'; +export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js'; +export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js'; +export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js'; +export {default as withResultOfAvailabilityCheck} from './withResultOfAvailabilityCheck.js'; diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js new file mode 100644 index 00000000..d74a1149 --- /dev/null +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -0,0 +1,9 @@ +import {input} from '#composite'; +import {is} from '#validators'; + +export default function inputAvailabilityCheckMode() { + return input({ + validate: is('null', 'empty', 'falsy'), + defaultValue: 'null', + }); +} diff --git a/src/data/composite/control-flow/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js new file mode 100644 index 00000000..03d8036a --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js @@ -0,0 +1,39 @@ +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default 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)), + }, + ], +}); diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js new file mode 100644 index 00000000..3c39f5ba --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js @@ -0,0 +1,47 @@ +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default 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'), + }), + + // TODO: A bit of a kludge, below. Other "do something with the update + // value" type functions can get by pretty much just passing that value + // as an input (input.updateValue()) into the corresponding "do something + // with a dependency/arbitrary value" function. But we can't do that here, + // because the special behavior, raiseOutputAbove(), only works to raise + // output above the composition it's *directly* nested in. Other languages + // have a throw/catch system that might serve as inspiration for something + // better here. + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), + }, + ], +}); diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js new file mode 100644 index 00000000..bcbd0b37 --- /dev/null +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -0,0 +1,66 @@ +// 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! +// +// See also: +// - exitWithoutDependency +// - exitWithoutUpdateValue +// - exposeDependencyOrContinue +// - exposeUpdateValueOrContinue +// - raiseOutputWithoutDependency +// - raiseOutputWithoutUpdateValue +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default 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}); + }, + }, + ], +}); diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js new file mode 100644 index 00000000..718f2294 --- /dev/null +++ b/src/data/composite/data/excludeFromList.js @@ -0,0 +1,56 @@ +// 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. +// +// See also: +// - fillMissingListItems +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +export default 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; + }), + }), + }, + ], +}); diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js new file mode 100644 index 00000000..c06eceda --- /dev/null +++ b/src/data/composite/data/fillMissingListItems.js @@ -0,0 +1,51 @@ +// Replaces items of a list, which are null or undefined, with some fallback +// value. By default, this replaces the passed dependency. +// +// See also: +// - excludeFromList +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default 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, + }), + }, + ], +}); diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js new file mode 100644 index 00000000..ecd05129 --- /dev/null +++ b/src/data/composite/data/index.js @@ -0,0 +1,8 @@ +export {default as excludeFromList} from './excludeFromList.js'; +export {default as fillMissingListItems} from './fillMissingListItems.js'; +export {default as withFlattenedList} from './withFlattenedList.js'; +export {default as withPropertiesFromList} from './withPropertiesFromList.js'; +export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; +export {default as withPropertyFromList} from './withPropertyFromList.js'; +export {default as withPropertyFromObject} from './withPropertyFromObject.js'; +export {default as withUnflattenedList} from './withUnflattenedList.js'; diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js new file mode 100644 index 00000000..b08edb4e --- /dev/null +++ b/src/data/composite/data/withFlattenedList.js @@ -0,0 +1,47 @@ +// 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. +// +// See also: +// - withFlattenedList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withPropertyFromList +// - withPropertiesFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default 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, + }); + }, + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js new file mode 100644 index 00000000..76ba696c --- /dev/null +++ b/src/data/composite/data/withPropertiesFromList.js @@ -0,0 +1,92 @@ +// 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. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default 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})), + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromObject.js b/src/data/composite/data/withPropertiesFromObject.js new file mode 100644 index 00000000..21726b58 --- /dev/null +++ b/src/data/composite/data/withPropertiesFromObject.js @@ -0,0 +1,87 @@ +// 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. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default 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), + })), + }, + ], +}); diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js new file mode 100644 index 00000000..3ce05fdf --- /dev/null +++ b/src/data/composite/data/withPropertyFromList.js @@ -0,0 +1,56 @@ +// 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. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {empty} from '#sugar'; + +// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL +export default function({ + 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)), + }); + }, + }, + }; +} diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js new file mode 100644 index 00000000..b31bab15 --- /dev/null +++ b/src/data/composite/data/withPropertyFromObject.js @@ -0,0 +1,69 @@ +// 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. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default 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), + }), + }, + ], +}); diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js new file mode 100644 index 00000000..3cfc247b --- /dev/null +++ b/src/data/composite/data/withUnflattenedList.js @@ -0,0 +1,62 @@ +// 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). + +import {input, templateCompositeFrom} from '#composite'; +import {isWholeNumber, validateArrayItems} from '#validators'; + +export default 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/composite/things/album/index.js b/src/data/composite/things/album/index.js new file mode 100644 index 00000000..8139f10e --- /dev/null +++ b/src/data/composite/things/album/index.js @@ -0,0 +1,2 @@ +export {default as withTracks} from './withTracks.js'; +export {default as withTrackSections} from './withTrackSections.js'; diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js new file mode 100644 index 00000000..c99b94d2 --- /dev/null +++ b/src/data/composite/things/album/withTrackSections.js @@ -0,0 +1,119 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {empty, stitchArrays} from '#sugar'; +import {isTrackSectionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import {exitWithoutDependency, exitWithoutUpdateValue} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +import { + fillMissingListItems, + withFlattenedList, + withPropertiesFromList, + withUnflattenedList, +} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withTrackSections`, + + outputs: ['#trackSections'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutUpdateValue({ + mode: input.value('empty'), + value: input.value([]), + }), + + // TODO: input.updateValue description down here is a kludge. + withPropertiesFromList({ + list: input.updateValue({ + validate: isTrackSectionList, + }), + 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', + }), + + { + dependencies: [ + '#sections.tracks', + '#sections.color', + '#sections.dateOriginallyReleased', + '#sections.isDefaultTrackSection', + '#sections.startIndex', + ], + + compute: (continuation, { + '#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 continuation({ + ['#trackSections']: + stitchArrays({ + tracks, + color, + dateOriginallyReleased, + isDefaultTrackSection, + startIndex, + }), + }); + }, + }, + ], +}); diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js new file mode 100644 index 00000000..dcea6593 --- /dev/null +++ b/src/data/composite/things/album/withTracks.js @@ -0,0 +1,51 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; + +import {exitWithoutDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withTracks`, + + outputs: ['#tracks'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: 'trackSections', + mode: input.value('empty'), + output: input.value({ + ['#tracks']: [], + }), + }), + + { + dependencies: ['trackSections'], + compute: (continuation, {trackSections}) => + continuation({ + '#trackRefs': trackSections + .flatMap(section => section.tracks ?? []), + }), + }, + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + find: input.value(find.track), + }), + + { + dependencies: ['#resolvedReferenceList'], + compute: (continuation, { + ['#resolvedReferenceList']: resolvedReferenceList, + }) => continuation({ + ['#tracks']: resolvedReferenceList, + }) + }, + ], +}); diff --git a/src/data/composite/things/track/exitWithoutUniqueCoverArt.js b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js new file mode 100644 index 00000000..f47086d9 --- /dev/null +++ b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js @@ -0,0 +1,26 @@ +// Shorthand for checking if the track has unique cover art and exposing a +// fallback value if it isn't. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withHasUniqueCoverArt from './withHasUniqueCoverArt.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUniqueCoverArt`, + + inputs: { + value: input({defaultValue: null}), + }, + + steps: () => [ + withHasUniqueCoverArt(), + + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js new file mode 100644 index 00000000..3354b1c4 --- /dev/null +++ b/src/data/composite/things/track/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; +export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js'; +export {default as trackReverseReferenceList} from './trackReverseReferenceList.js'; +export {default as withAlbum} from './withAlbum.js'; +export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js'; +export {default as withContainingTrackSection} from './withContainingTrackSection.js'; +export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; +export {default as withOtherReleases} from './withOtherReleases.js'; +export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js new file mode 100644 index 00000000..a9d57f86 --- /dev/null +++ b/src/data/composite/things/track/inheritFromOriginalRelease.js @@ -0,0 +1,43 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `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); + }, + }, + ], +}); diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js new file mode 100644 index 00000000..e7bfedf3 --- /dev/null +++ b/src/data/composite/things/track/trackReverseReferenceList.js @@ -0,0 +1,38 @@ +// Like a normal reverse reference list ("objects which reference this object +// under a specified property"), only excluding re-releases from the possible +// outputs. While it's useful to travel from a re-release to the tracks it +// references, re-releases aren't generally relevant from the perspective of +// the tracks *being* referenced. Apart from hiding re-releases from lists on +// the site, it also excludes keeps them from relational data processing, such +// as on the "Tracks - by Times Referenced" listing page. + +import {input, templateCompositeFrom} from '#composite'; +import {withReverseReferenceList} from '#composite/wiki-data'; + +export default 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/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js new file mode 100644 index 00000000..34845ab0 --- /dev/null +++ b/src/data/composite/things/track/withAlbum.js @@ -0,0 +1,57 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `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}), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js new file mode 100644 index 00000000..0aeac788 --- /dev/null +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -0,0 +1,52 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {isBoolean} from '#validators'; + +import {exitWithoutDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {excludeFromList, withPropertyFromObject} from '#composite/data'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withAlwaysReferenceByDirectory`, + + steps: () => [ + 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: (continuation, { + name, + ['#originalRelease.name']: originalName, + }) => continuation({ + ['#alwaysReferenceByDirectory']: name === originalName, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js new file mode 100644 index 00000000..b2e5f2b3 --- /dev/null +++ b/src/data/composite/things/track/withContainingTrackSection.js @@ -0,0 +1,63 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default 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, + }); + } + }, + }, + ], +}); diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js new file mode 100644 index 00000000..96078d5f --- /dev/null +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -0,0 +1,61 @@ +// 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.) + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import {withResolvedContribs} from '#composite/wiki-data'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default 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), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withOriginalRelease.js new file mode 100644 index 00000000..d2ee39df --- /dev/null +++ b/src/data/composite/things/track/withOriginalRelease.js @@ -0,0 +1,59 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {validateWikiData} from '#validators'; + +import {withResolvedReference} from '#composite/wiki-data'; + +export default 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)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js new file mode 100644 index 00000000..84420cf8 --- /dev/null +++ b/src/data/composite/things/track/withOtherReleases.js @@ -0,0 +1,40 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withOtherReleases`, + + outputs: ['#otherReleases'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + }), + + withOriginalRelease({ + selfIfOriginal: input.value(true), + }), + + { + dependencies: [input.myself(), '#originalRelease', 'trackData'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['#originalRelease']: originalRelease, + trackData, + }) => continuation({ + ['#otherReleases']: + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js new file mode 100644 index 00000000..b236a6e8 --- /dev/null +++ b/src/data/composite/things/track/withPropertyFromAlbum.js @@ -0,0 +1,49 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {withPropertyFromObject} from '#composite/data'; + +import withAlbum from './withAlbum.js'; + +export default 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, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js new file mode 100644 index 00000000..2c8219fc --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -0,0 +1,47 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), + + value: input({defaultValue: null}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + // TODO: Fairly certain exitWithoutDependency would be sufficient here. + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js new file mode 100644 index 00000000..1d0400fc --- /dev/null +++ b/src/data/composite/wiki-data/index.js @@ -0,0 +1,7 @@ +export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as inputThingClass} from './inputThingClass.js'; +export {default as inputWikiData} from './inputWikiData.js'; +export {default as withResolvedContribs} from './withResolvedContribs.js'; +export {default as withResolvedReference} from './withResolvedReference.js'; +export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; +export {default as withReverseReferenceList} from './withReverseReferenceList.js'; diff --git a/src/data/composite/wiki-data/inputThingClass.js b/src/data/composite/wiki-data/inputThingClass.js new file mode 100644 index 00000000..d70480e6 --- /dev/null +++ b/src/data/composite/wiki-data/inputThingClass.js @@ -0,0 +1,23 @@ +// Please note that this input, used in a variety of #composite/wiki-data +// utilities, is basically always a kludge. Any usage of it depends on +// referencing Thing class values defined outside of the #composite folder. + +import {input} from '#composite'; +import {isType} from '#validators'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default function inputThingClass() { + return input.staticValue({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }); +} diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js new file mode 100644 index 00000000..cf7a7c2c --- /dev/null +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -0,0 +1,17 @@ +import {input} from '#composite'; +import {validateWikiData} from '#validators'; + +// 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 default function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData({referenceType, allowMixedTypes}), + acceptsNull: true, + }); +} diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js new file mode 100644 index 00000000..eda24160 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -0,0 +1,77 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {stitchArrays} from '#sugar'; +import {is, isContributionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import { + withPropertiesFromList, +} from '#composite/data'; + +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default 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}), + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js new file mode 100644 index 00000000..0fa5c554 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -0,0 +1,73 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default 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, + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js new file mode 100644 index 00000000..1d39e5b2 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -0,0 +1,101 @@ +// 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'). + +import {input, templateCompositeFrom} from '#composite'; +import {is, isString, validateArrayItems} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default 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`); + } + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js new file mode 100644 index 00000000..113a6c40 --- /dev/null +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -0,0 +1,40 @@ +// Check out the info on reverseReferenceList! +// This is its composable form. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default 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/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js new file mode 100644 index 00000000..6760527a --- /dev/null +++ b/src/data/composite/wiki-properties/additionalFiles.js @@ -0,0 +1,30 @@ +// 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: [...]}, +// ... +// ] +// + +import {isAdditionalFileList} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], + }, + }; +} diff --git a/src/data/composite/wiki-properties/color.js b/src/data/composite/wiki-properties/color.js new file mode 100644 index 00000000..1bc9888b --- /dev/null +++ b/src/data/composite/wiki-properties/color.js @@ -0,0 +1,12 @@ +// A color! This'll be some CSS-ready value. + +import {isColor} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js new file mode 100644 index 00000000..fbea9d5c --- /dev/null +++ b/src/data/composite/wiki-properties/commentary.js @@ -0,0 +1,12 @@ +// Artist commentary! Generally present on tracks and albums. + +import {isCommentary} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js new file mode 100644 index 00000000..52aeb868 --- /dev/null +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -0,0 +1,55 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {unique} from '#sugar'; + +import {exitWithoutDependency} from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default 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), + }, + }, + ], +}); diff --git a/src/data/composite/wiki-properties/contribsPresent.js b/src/data/composite/wiki-properties/contribsPresent.js new file mode 100644 index 00000000..24f302a5 --- /dev/null +++ b/src/data/composite/wiki-properties/contribsPresent.js @@ -0,0 +1,30 @@ +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; + +export default 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'}), + ], +}); diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js new file mode 100644 index 00000000..8fde2caa --- /dev/null +++ b/src/data/composite/wiki-properties/contributionList.js @@ -0,0 +1,35 @@ +// 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! +// + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow'; +import {withResolvedContribs} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `contributionList`, + + compose: false, + + update: {validate: isContributionList}, + + steps: () => [ + withResolvedContribs({from: input.updateValue()}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: input.value([])}), + ], +}); diff --git a/src/data/composite/wiki-properties/dimensions.js b/src/data/composite/wiki-properties/dimensions.js new file mode 100644 index 00000000..57a01279 --- /dev/null +++ b/src/data/composite/wiki-properties/dimensions.js @@ -0,0 +1,13 @@ +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. + +import {isDimensions} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js new file mode 100644 index 00000000..0b2181c9 --- /dev/null +++ b/src/data/composite/wiki-properties/directory.js @@ -0,0 +1,23 @@ +// The all-encompassing "directory" property, used as the unique identifier for +// almost any data object. Also corresponds to a part of the URL which pages of +// such objects are visited at. + +import {isDirectory} from '#validators'; +import {getKebabCase} from '#wiki-data'; + +// TODO: Not templateCompositeFrom. + +export default function() { + 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; + }, + }, + }; +} diff --git a/src/data/composite/wiki-properties/duration.js b/src/data/composite/wiki-properties/duration.js new file mode 100644 index 00000000..827f282d --- /dev/null +++ b/src/data/composite/wiki-properties/duration.js @@ -0,0 +1,13 @@ +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. + +import {isDuration} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} diff --git a/src/data/composite/wiki-properties/externalFunction.js b/src/data/composite/wiki-properties/externalFunction.js new file mode 100644 index 00000000..c388da6c --- /dev/null +++ b/src/data/composite/wiki-properties/externalFunction.js @@ -0,0 +1,11 @@ +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} diff --git a/src/data/composite/wiki-properties/fileExtension.js b/src/data/composite/wiki-properties/fileExtension.js new file mode 100644 index 00000000..c926fa8b --- /dev/null +++ b/src/data/composite/wiki-properties/fileExtension.js @@ -0,0 +1,13 @@ +// A file extension! Or the default, if provided when calling this. + +import {isFileExtension} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} diff --git a/src/data/composite/wiki-properties/flag.js b/src/data/composite/wiki-properties/flag.js new file mode 100644 index 00000000..076e663f --- /dev/null +++ b/src/data/composite/wiki-properties/flag.js @@ -0,0 +1,19 @@ +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! + +import {isBoolean} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: The description is a lie. This defaults to false. Bad. + +export default function(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}, + }; +} diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js new file mode 100644 index 00000000..2462b047 --- /dev/null +++ b/src/data/composite/wiki-properties/index.js @@ -0,0 +1,20 @@ +export {default as additionalFiles} from './additionalFiles.js'; +export {default as color} from './color.js'; +export {default as commentary} from './commentary.js'; +export {default as commentatorArtists} from './commentatorArtists.js'; +export {default as contribsPresent} from './contribsPresent.js'; +export {default as contributionList} from './contributionList.js'; +export {default as dimensions} from './dimensions.js'; +export {default as directory} from './directory.js'; +export {default as duration} from './duration.js'; +export {default as externalFunction} from './externalFunction.js'; +export {default as fileExtension} from './fileExtension.js'; +export {default as flag} from './flag.js'; +export {default as name} from './name.js'; +export {default as referenceList} from './referenceList.js'; +export {default as reverseReferenceList} from './reverseReferenceList.js'; +export {default as simpleDate} from './simpleDate.js'; +export {default as simpleString} from './simpleString.js'; +export {default as singleReference} from './singleReference.js'; +export {default as urls} from './urls.js'; +export {default as wikiData} from './wikiData.js'; diff --git a/src/data/composite/wiki-properties/name.js b/src/data/composite/wiki-properties/name.js new file mode 100644 index 00000000..5146488b --- /dev/null +++ b/src/data/composite/wiki-properties/name.js @@ -0,0 +1,11 @@ +// A wiki data object's name! Its directory (i.e. unique identifier) will be +// computed based on this value if not otherwise specified. + +import {isName} from '#validators'; + +export default function(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js new file mode 100644 index 00000000..f5b6c58e --- /dev/null +++ b/src/data/composite/wiki-properties/referenceList.js @@ -0,0 +1,47 @@ +// Stores and exposes a list of references to other data objects; all items +// must be references to the same type, which is specified on the class input. +// +// See also: +// - singleReference +// - withResolvedReferenceList +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReferenceList} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReferenceList} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: inputThingClass(), + + 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'}), + ], +}); diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js new file mode 100644 index 00000000..84ba67df --- /dev/null +++ b/src/data/composite/wiki-properties/reverseReferenceList.js @@ -0,0 +1,30 @@ +// 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. + +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data'; + +export default 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'}), + ], +}); diff --git a/src/data/composite/wiki-properties/simpleDate.js b/src/data/composite/wiki-properties/simpleDate.js new file mode 100644 index 00000000..f08d8323 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleDate.js @@ -0,0 +1,14 @@ +// 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. + +import {isDate} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} diff --git a/src/data/composite/wiki-properties/simpleString.js b/src/data/composite/wiki-properties/simpleString.js new file mode 100644 index 00000000..18d65146 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleString.js @@ -0,0 +1,14 @@ +// 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. + +import {isString} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js new file mode 100644 index 00000000..34bd2e6d --- /dev/null +++ b/src/data/composite/wiki-properties/singleReference.js @@ -0,0 +1,47 @@ +// Stores and exposes one connection, or reference, to another data object. +// The reference must be to a specific type, which is specified on the class +// input. +// +// See also: +// - referenceList +// - withResolvedReference +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReference} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReference} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `singleReference`, + + compose: false, + + inputs: { + class: inputThingClass(), + 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'}), + ], +}); diff --git a/src/data/composite/wiki-properties/urls.js b/src/data/composite/wiki-properties/urls.js new file mode 100644 index 00000000..3160a0bf --- /dev/null +++ b/src/data/composite/wiki-properties/urls.js @@ -0,0 +1,14 @@ +// A list of URLs! This will always be present on the data object, even if set +// to an empty array or null. + +import {isURL, validateArrayItems} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: value => value ?? []}, + }; +} diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js new file mode 100644 index 00000000..4ea47785 --- /dev/null +++ b/src/data/composite/wiki-properties/wikiData.js @@ -0,0 +1,17 @@ +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. + +import {validateArrayItems, validateInstanceOf} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: This should validate with validateWikiData. + +export default function(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; +} -- cgit 1.3.0-6-gf8a5 From a60c8906ed7580a21527c9f96cd0e6e277978263 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:58:56 -0300 Subject: data, test: expose track section names --- src/data/composite/things/album/withTrackSections.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'src/data/composite') diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js index c99b94d2..baa3cb4a 100644 --- a/src/data/composite/things/album/withTrackSections.js +++ b/src/data/composite/things/album/withTrackSections.js @@ -41,6 +41,7 @@ export default templateCompositeFrom({ 'tracks', 'dateOriginallyReleased', 'isDefaultTrackSection', + 'name', 'color', ]), }), @@ -55,6 +56,11 @@ export default templateCompositeFrom({ fill: input.value(false), }), + fillMissingListItems({ + list: '#sections.name', + fill: input.value('Unnamed Track Section'), + }), + fillMissingListItems({ list: '#sections.color', fill: input.dependency('color'), @@ -86,6 +92,7 @@ export default templateCompositeFrom({ { dependencies: [ '#sections.tracks', + '#sections.name', '#sections.color', '#sections.dateOriginallyReleased', '#sections.isDefaultTrackSection', @@ -94,19 +101,21 @@ export default templateCompositeFrom({ compute: (continuation, { '#sections.tracks': tracks, + '#sections.name': name, '#sections.color': color, '#sections.dateOriginallyReleased': dateOriginallyReleased, '#sections.isDefaultTrackSection': isDefaultTrackSection, '#sections.startIndex': startIndex, }) => { filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, tracks => !empty(tracks)); return continuation({ ['#trackSections']: stitchArrays({ tracks, + name, color, dateOriginallyReleased, isDefaultTrackSection, -- cgit 1.3.0-6-gf8a5 From 963e04f124f98464a986487208ee4f9edd893984 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:59:25 -0300 Subject: data: misc. composite fixes --- src/data/composite/things/track/withAlwaysReferenceByDirectory.js | 2 ++ src/data/composite/wiki-data/withReverseReferenceList.js | 1 + 2 files changed, 3 insertions(+) (limited to 'src/data/composite') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 0aeac788..7c59393c 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -15,6 +15,8 @@ import withOriginalRelease from './withOriginalRelease.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, + outputs: ['#alwaysReferenceByDirectory'], + steps: () => [ exposeUpdateValueOrContinue({ validate: input.value(isBoolean), diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 113a6c40..a025b5ed 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -21,6 +21,7 @@ export default templateCompositeFrom({ exitWithoutDependency({ dependency: input('data'), value: input.value([]), + mode: input.value('empty'), }), { -- cgit 1.3.0-6-gf8a5 From 72cc4d62a32e40c1dcb75e868c51991075cc03e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 2 Oct 2023 10:40:27 -0300 Subject: data: withAlwaysReferenceByDirectory: kludge to avoid infinite recursion --- .../things/track/withAlwaysReferenceByDirectory.js | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'src/data/composite') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 7c59393c..d27f7b23 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -2,15 +2,22 @@ // 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. +// +// See the implementation for an important caveat about matching the original +// track against other tracks, which uses a custom implementation pulling (and +// duplicating) details from #find instead of using withOriginalRelease and the +// usual withResolvedReference / find.track() utilities. +// import {input, templateCompositeFrom} from '#composite'; import {isBoolean} from '#validators'; import {exitWithoutDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; -import {excludeFromList, withPropertyFromObject} from '#composite/data'; +import {withPropertyFromObject} from '#composite/data'; -import withOriginalRelease from './withOriginalRelease.js'; +// TODO: Kludge. (The usage of this, not so much the import.) +import CacheableObject from '../../../things/cacheable-object.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, @@ -22,15 +29,45 @@ export default templateCompositeFrom({ validate: input.value(isBoolean), }), - excludeFromList({ - list: 'trackData', - item: input.myself(), + // Remaining code is for defaulting to true if this track is a rerelease of + // another with the same name, so everything further depends on access to + // trackData as well as originalReleaseTrack. + + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + value: input.value(false), }), - withOriginalRelease({ - data: '#trackData', + exitWithoutDependency({ + dependency: 'originalReleaseTrack', + value: input.value(false), }), + // "Slow" / uncached, manual search from trackData (with this track + // excluded). Otherwise there end up being pretty bad recursion issues + // (track1.alwaysReferencedByDirectory depends on searching through data + // including track2, which depends on evaluating track2.alwaysReferenced- + // ByDirectory, which depends on searcing through data including track1...) + // That said, this is 100% a kludge, since it involves duplicating find + // logic on a completely unrelated context. + { + dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['trackData']: trackData, + ['originalReleaseTrack']: ref, + }) => continuation({ + ['#originalRelease']: + (ref.startsWith('track:') + ? trackData.find(track => track.directory === ref.slice('track:'.length)) + : trackData.find(track => + track !== thisTrack && + !CacheableObject.getUpdateValue(track, 'originalReleaseTrack') && + track.name.toLowerCase() === ref.toLowerCase())), + }) + }, + exitWithoutDependency({ dependency: '#originalRelease', value: input.value(false), -- cgit 1.3.0-6-gf8a5 From c7e21005beb8807216aac6ed3ae54029575007a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 18:03:24 -0300 Subject: data: Track.withAlbum: bulkily match documented early exit behavior --- src/data/composite/things/track/withAlbum.js | 45 +++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) (limited to 'src/data/composite') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 34845ab0..0e85cee9 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,10 +4,9 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; import {is} from '#validators'; -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - export default templateCompositeFrom({ annotation: `withAlbum`, @@ -21,13 +20,20 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), + { + dependencies: [input('notFoundMode'), 'albumData'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['albumData']: albumData, + }) => + (albumData === null + ? continuation.exit(null) + : empty(albumData) + ? (notFoundMode === 'exit' + ? continuation.exit(null) + : continuation.raiseOutput({'#album': null})) + : continuation()), + }, { dependencies: [input.myself(), 'albumData'], @@ -37,21 +43,20 @@ export default templateCompositeFrom({ }) => continuation({ ['#album']: - albumData.find(album => album.tracks.includes(track)), + albumData.find(album => album.tracks.includes(track)) + ?? null, }), }, - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), + dependencies: [input('notFoundMode'), '#album'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['#album']: album, + }) => + ((album === null && notFoundMode === 'exit') + ? continuation.exit(null) + : continuation.raiseOutput({'#album': album})), }, ], }); -- cgit 1.3.0-6-gf8a5 From ecac8276182436897dffff02aaf1a7d268738cba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 18:47:08 -0300 Subject: data: update withPropertyFromList --- src/data/composite/data/withPropertyFromList.js | 100 +++++++++++++++--------- 1 file changed, 63 insertions(+), 37 deletions(-) (limited to 'src/data/composite') diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index 3ce05fdf..1983ebbc 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -16,41 +16,67 @@ // - withUnflattenedList // -import {empty} from '#sugar'; - -// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL -export default function({ - 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)), - }); - }, - }, - }; +import {input, templateCompositeFrom} from '#composite'; + +function getOutputName({list, property, prefix}) { + if (!property) return `#values`; + if (prefix) return `${prefix}.${property}`; + if (list) return `${list}.${property}`; + return `#list.${property}`; } + +export default templateCompositeFrom({ + annotation: `withPropertyFromList`, + + inputs: { + list: input({type: 'array'}), + property: input({type: 'string'}), + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => + [getOutputName({list, property, prefix})], + + steps: () => [ + { + dependencies: [input('list'), input('property')], + compute: (continuation, { + [input('list')]: list, + [input('property')]: property, + }) => continuation({ + ['#values']: + list.map(item => item[property] ?? null), + }), + }, + + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('property'), + input.staticValue('prefix'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => continuation({ + ['#outputName']: + getOutputName({list, property, prefix}), + }), + }, + + { + dependencies: ['#values', '#outputName'], + compute: (continuation, { + ['#values']: values, + ['#outputName']: outputName, + }) => + continuation.raiseOutput({[outputName]: values}), + }, + ], +}); -- cgit 1.3.0-6-gf8a5 From 5f740046562c85ab4e00063037ddf3af5a545279 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:05:39 -0300 Subject: data, test: withResultOfAvailabilityCheck: index mode --- src/data/composite/control-flow/inputAvailabilityCheckMode.js | 2 +- src/data/composite/control-flow/withResultOfAvailabilityCheck.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src/data/composite') diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js index d74a1149..8008fdeb 100644 --- a/src/data/composite/control-flow/inputAvailabilityCheckMode.js +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -3,7 +3,7 @@ import {is} from '#validators'; export default function inputAvailabilityCheckMode() { return input({ - validate: is('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy', 'index'), defaultValue: 'null', }); } diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js index bcbd0b37..a6942014 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -10,6 +10,7 @@ // * '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! +// * 'index': Check that the value is a number, and is at least zero. // // See also: // - exitWithoutDependency @@ -57,6 +58,10 @@ export default templateCompositeFrom({ case 'falsy': availability = !!value && (!Array.isArray(value) || !empty(value)); break; + + case 'index': + availability = typeof value === 'number' && value >= 0; + break; } return continuation({'#availability': availability}); -- cgit 1.3.0-6-gf8a5 From bb646f28853399a43d52a056c86d04f6a4343932 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:09:22 -0300 Subject: data: Track.withAlbum: refactor for clarity Utilizes availability checks instead of manual null comparisons and empty() calls, extracts track lists using withPropertyFromList, operates on index instead of unique album object where possible (including found / not found check). --- src/data/composite/things/track/withAlbum.js | 98 ++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 27 deletions(-) (limited to 'src/data/composite') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 0e85cee9..9c974cd1 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,9 +4,12 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; import {is} from '#validators'; +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + export default templateCompositeFrom({ annotation: `withAlbum`, @@ -20,43 +23,84 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ + // null albumData is always an early exit. + + exitWithoutDependency({ + dependency: 'albumData', + mode: input.value('null'), + }), + + // empty albumData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'albumData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#albumDataAvailability', + }), + { - dependencies: [input('notFoundMode'), 'albumData'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumDataAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['albumData']: albumData, - }) => - (albumData === null - ? continuation.exit(null) - : empty(albumData) - ? (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raiseOutput({'#album': null})) - : continuation()), + ['#albumDataAvailability']: albumDataIsAvailable, + }) { + if (albumDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, }, + withPropertyFromList({ + list: 'albumData', + property: input.value('tracks'), + }), + { - dependencies: [input.myself(), 'albumData'], + dependencies: [input.myself(), '#albumData.tracks'], compute: (continuation, { [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)) - ?? null, - }), + ['#albumData.tracks']: trackLists, + }) => continuation({ + ['#albumIndex']: + trackLists.findIndex(tracks => tracks.includes(track)), + }), }, + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#albumIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#albumAvailability', + }), + { - dependencies: [input('notFoundMode'), '#album'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['#album']: album, - }) => - ((album === null && notFoundMode === 'exit') - ? continuation.exit(null) - : continuation.raiseOutput({'#album': album})), + ['#albumAvailability']: albumIsAvailable, + }) { + if (albumIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, + }, + + { + dependencies: ['albumData', '#albumIndex'], + compute: (continuation, { + ['albumData']: albumData, + ['#albumIndex']: albumIndex, + }) => continuation.raiseOutput({ + ['#album']: + albumData[albumIndex], + }), }, ], }); -- cgit 1.3.0-6-gf8a5 From 855dcdbc17831809cfb3c800d378c62186702740 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:30:40 -0300 Subject: data: Flash.withFlashAct --- src/data/composite/things/flash/index.js | 1 + src/data/composite/things/flash/withFlashAct.js | 108 ++++++++++++++++++++++++ src/data/composite/things/track/withAlbum.js | 2 + 3 files changed, 111 insertions(+) create mode 100644 src/data/composite/things/flash/index.js create mode 100644 src/data/composite/things/flash/withFlashAct.js (limited to 'src/data/composite') diff --git a/src/data/composite/things/flash/index.js b/src/data/composite/things/flash/index.js new file mode 100644 index 00000000..63ac13da --- /dev/null +++ b/src/data/composite/things/flash/index.js @@ -0,0 +1 @@ +export {default as withFlashAct} from './withFlashAct.js'; diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js new file mode 100644 index 00000000..ada2dcfe --- /dev/null +++ b/src/data/composite/things/flash/withFlashAct.js @@ -0,0 +1,108 @@ +// Gets the flash's act. This will early exit if flashActData is missing. +// By default, if there's no flash whose list of flashes includes this flash, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. +// +// This step models with Flash.withAlbum. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withFlashAct`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#flashAct'], + + steps: () => [ + // null flashActData is always an early exit. + + exitWithoutDependency({ + dependency: 'flashActData', + mode: input.value('null'), + }), + + // empty flashActData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'flashActData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#flashActDataAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActDataAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActDataAvailability']: flashActDataIsAvailable, + }) { + if (flashActDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + withPropertyFromList({ + list: 'flashActData', + property: input.value('flashes'), + }), + + { + dependencies: [input.myself(), '#flashActData.flashes'], + compute: (continuation, { + [input.myself()]: track, + ['#flashActData.flashes']: flashLists, + }) => continuation({ + ['#flashActIndex']: + flashLists.findIndex(flashes => flashes.includes(track)), + }), + }, + + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#flashActIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#flashActAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActAvailability']: flashActIsAvailable, + }) { + if (flashActIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + { + dependencies: ['flashActData', '#flashActIndex'], + compute: (continuation, { + ['flashActData']: flashActData, + ['#flashActIndex']: flashActIndex, + }) => continuation.raiseOutput({ + ['#flashAct']: + flashActData[flashActIndex], + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 9c974cd1..cbd16dcd 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -2,6 +2,8 @@ // 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. +// +// This step models with Flash.withFlashAct. import {input, templateCompositeFrom} from '#composite'; import {is} from '#validators'; -- cgit 1.3.0-6-gf8a5