diff options
Diffstat (limited to 'src/data/composite')
49 files changed, 1641 insertions, 135 deletions
diff --git a/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js b/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js new file mode 100644 index 00000000..a2fdd6b0 --- /dev/null +++ b/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js @@ -0,0 +1,42 @@ +// Exposes true if a dependency is available, and false otherwise, +// or the reverse if the `negate` input is set true. +// +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exposeWhetherDependencyAvailable`, + + compose: false, + + inputs: { + dependency: input({acceptsNull: true}), + + mode: inputAvailabilityCheckMode(), + + negate: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('negate')], + + compute: ({ + ['#availability']: availability, + [input('negate')]: negate, + }) => + (negate + ? !availability + : availability), + }, + ], +}); diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js index 7fad88b2..6148d465 100644 --- a/src/data/composite/control-flow/index.js +++ b/src/data/composite/control-flow/index.js @@ -9,6 +9,7 @@ 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 exposeWhetherDependencyAvailable} from './exposeWhetherDependencyAvailable.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/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js index a6942014..1d90b324 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -17,6 +17,7 @@ // - exitWithoutUpdateValue // - exposeDependencyOrContinue // - exposeUpdateValueOrContinue +// - exposeWhetherDependencyAvailable // - raiseOutputWithoutDependency // - raiseOutputWithoutUpdateValue // diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js index d798dcdc..2a3e818e 100644 --- a/src/data/composite/data/excludeFromList.js +++ b/src/data/composite/data/excludeFromList.js @@ -5,11 +5,6 @@ // See also: // - fillMissingListItems // -// More list utilities: -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; import {empty} from '#sugar'; diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js index 4f818a79..356b1119 100644 --- a/src/data/composite/data/fillMissingListItems.js +++ b/src/data/composite/data/fillMissingListItems.js @@ -4,11 +4,6 @@ // See also: // - excludeFromList // -// More list utilities: -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index 256c0490..c80bb350 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -3,15 +3,32 @@ // Entries here may depend on entries in #composite/control-flow. // +// Utilities which act on generic objects + +export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; +export {default as withPropertyFromObject} from './withPropertyFromObject.js'; + +// Utilities which act on generic lists + export {default as excludeFromList} from './excludeFromList.js'; + export {default as fillMissingListItems} from './fillMissingListItems.js'; +export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; + export {default as withFilteredList} from './withFilteredList.js'; -export {default as withFlattenedList} from './withFlattenedList.js'; export {default as withMappedList} from './withMappedList.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 withSortedList} from './withSortedList.js'; + +export {default as withPropertyFromList} from './withPropertyFromList.js'; +export {default as withPropertiesFromList} from './withPropertiesFromList.js'; + +export {default as withFlattenedList} from './withFlattenedList.js'; export {default as withUnflattenedList} from './withUnflattenedList.js'; -export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; + +export {default as withIndexInList} from './withIndexInList.js'; +export {default as withNearbyItemFromList} from './withNearbyItemFromList.js'; + +// Utilities which act on slightly more particular data forms +// (probably, containers of particular kinds of values) + +export {default as withSum} from './withSum.js'; diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js index 82e56903..60fe66f4 100644 --- a/src/data/composite/data/withFilteredList.js +++ b/src/data/composite/data/withFilteredList.js @@ -16,12 +16,6 @@ // - withMappedList // - withSortedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js index edfa3403..31b1a742 100644 --- a/src/data/composite/data/withFlattenedList.js +++ b/src/data/composite/data/withFlattenedList.js @@ -5,12 +5,6 @@ // See also: // - withUnflattenedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withIndexInList.js b/src/data/composite/data/withIndexInList.js new file mode 100644 index 00000000..b1af2033 --- /dev/null +++ b/src/data/composite/data/withIndexInList.js @@ -0,0 +1,38 @@ +// Gets the index of the provided item in the provided list. Note that this +// will output -1 if the item is not found, and this may be detected using +// any availability check with type: 'index'. If the list includes the item +// twice, the output index will be of the first match. +// +// Both the list and item must be provided. +// +// See also: +// - withNearbyItemFromList +// - exitWithoutDependency +// - raiseOutputWithoutDependency +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withIndexInList`, + + inputs: { + list: input({acceptsNull: false, type: 'array'}), + item: input({acceptsNull: false}), + }, + + outputs: ['#index'], + + steps: () => [ + { + dependencies: [input('list'), input('item')], + compute: (continuation, { + [input('list')]: list, + [input('item')]: item, + }) => continuation({ + ['#index']: + list.indexOf(item), + }), + }, + ], +}); diff --git a/src/data/composite/data/withMappedList.js b/src/data/composite/data/withMappedList.js index e0a700b2..0bc63a92 100644 --- a/src/data/composite/data/withMappedList.js +++ b/src/data/composite/data/withMappedList.js @@ -5,12 +5,6 @@ // - withFilteredList // - withSortedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withNearbyItemFromList.js b/src/data/composite/data/withNearbyItemFromList.js new file mode 100644 index 00000000..83a8cc21 --- /dev/null +++ b/src/data/composite/data/withNearbyItemFromList.js @@ -0,0 +1,73 @@ +// Gets a nearby (typically adjacent) item in a list, meaning the item which is +// placed at a particular offset compared to the provided item. This is null if +// the provided list doesn't include the provided item at all, and also if the +// offset would read past either end of the list - except if configured: +// +// - If the 'wrap' input is provided (as true), the offset will loop around +// and continue from the opposing end. +// +// - If the 'valuePastEdge' input is provided, that value will be output +// instead of null. +// +// Both the list and item must be provided. +// +// See also: +// - withIndexInList +// + +import {input, templateCompositeFrom} from '#composite'; +import {atOffset} from '#sugar'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withIndexInList from './withIndexInList.js'; + +export default templateCompositeFrom({ + annotation: `withNearbyItemFromList`, + + inputs: { + list: input({acceptsNull: false, type: 'array'}), + item: input({acceptsNull: false}), + + offset: input({type: 'number'}), + wrap: input({type: 'boolean', defaultValue: false}), + }, + + outputs: ['#nearbyItem'], + + steps: () => [ + withIndexInList({ + list: input('list'), + item: input('item'), + }), + + raiseOutputWithoutDependency({ + dependency: '#index', + mode: input.value('index'), + + output: input.value({ + ['#nearbyItem']: + null, + }), + }), + + { + dependencies: [ + input('list'), + input('offset'), + input('wrap'), + '#index', + ], + + compute: (continuation, { + [input('list')]: list, + [input('offset')]: offset, + [input('wrap')]: wrap, + ['#index']: index, + }) => continuation({ + ['#nearbyItem']: + atOffset(list, index, offset, {wrap}), + }), + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js index 08907bab..fb4134bc 100644 --- a/src/data/composite/data/withPropertiesFromList.js +++ b/src/data/composite/data/withPropertiesFromList.js @@ -8,12 +8,6 @@ // - withPropertiesFromObject // - withPropertyFromList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// import {input, templateCompositeFrom} from '#composite'; import {isString, validateArrayItems} from '#validators'; diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index a2c66d77..65ebf77b 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -9,12 +9,6 @@ // - withPropertiesFromList // - withPropertyFromObject // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js index b31bab15..4f240506 100644 --- a/src/data/composite/data/withPropertyFromObject.js +++ b/src/data/composite/data/withPropertyFromObject.js @@ -2,11 +2,15 @@ // If the object itself is null, or the object doesn't have the listed property, // the provided dependency will also be null. // +// If the `internal` input is true, this reads the CacheableObject update value +// of the object rather than its exposed value. +// // See also: // - withPropertiesFromObject // - withPropertyFromList // +import CacheableObject from '#cacheable-object'; import {input, templateCompositeFrom} from '#composite'; export default templateCompositeFrom({ @@ -15,6 +19,7 @@ export default templateCompositeFrom({ inputs: { object: input({type: 'object', acceptsNull: true}), property: input({type: 'string'}), + internal: input({type: 'boolean', defaultValue: false}), }, outputs: ({ @@ -49,20 +54,35 @@ export default templateCompositeFrom({ { dependencies: [ - '#output', input('object'), input('property'), + input('internal'), ], compute: (continuation, { - ['#output']: output, [input('object')]: object, [input('property')]: property, + [input('internal')]: internal, }) => continuation({ - [output]: + '#value': (object === null ? null - : object[property] ?? null), + : internal + ? CacheableObject.getUpdateValue(object, property) + ?? null + : object[property] + ?? null), + }), + }, + + { + dependencies: ['#output', '#value'], + + compute: (continuation, { + ['#output']: output, + ['#value']: value, + }) => continuation({ + [output]: value, }), }, ], diff --git a/src/data/composite/data/withSortedList.js b/src/data/composite/data/withSortedList.js index dd810786..a7d21768 100644 --- a/src/data/composite/data/withSortedList.js +++ b/src/data/composite/data/withSortedList.js @@ -27,12 +27,6 @@ // - withFilteredList // - withMappedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withSum.js b/src/data/composite/data/withSum.js new file mode 100644 index 00000000..484e9906 --- /dev/null +++ b/src/data/composite/data/withSum.js @@ -0,0 +1,33 @@ +// Gets the numeric total of adding all the values in a list together. +// Values that are false, null, or undefined are skipped over. + +import {input, templateCompositeFrom} from '#composite'; +import {isNumber, sparseArrayOf} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withSum`, + + inputs: { + values: input({ + validate: sparseArrayOf(isNumber), + }), + }, + + outputs: ['#sum'], + + steps: () => [ + { + dependencies: [input('values')], + compute: (continuation, { + [input('values')]: values, + }) => continuation({ + ['#sum']: + values + .filter(item => typeof item === 'number') + .reduce( + (accumulator, value) => accumulator + value, + 0), + }), + }, + ], +}); diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js index 39a666dc..820d628a 100644 --- a/src/data/composite/data/withUnflattenedList.js +++ b/src/data/composite/data/withUnflattenedList.js @@ -7,12 +7,6 @@ // See also: // - withFlattenedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; import {isWholeNumber, validateArrayItems} from '#validators'; diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js index 8b5098f0..0ef91b87 100644 --- a/src/data/composite/things/album/index.js +++ b/src/data/composite/things/album/index.js @@ -1 +1,2 @@ +export {default as withTrackSections} from './withTrackSections.js'; export {default as withTracks} from './withTracks.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..a56bda31 --- /dev/null +++ b/src/data/composite/things/album/withTrackSections.js @@ -0,0 +1,21 @@ +import {input, templateCompositeFrom} from '#composite'; + +import find from '#find'; + +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withTrackSections`, + + outputs: ['#trackSections'], + + steps: () => [ + withResolvedReferenceList({ + list: 'trackSections', + data: 'ownTrackSectionData', + find: input.value(find.unqualifiedTrackSection), + }).outputs({ + ['#resolvedReferenceList']: '#trackSections', + }), + ], +}); diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js index 3fe6dd2e..c8d27c4c 100644 --- a/src/data/composite/things/album/withTracks.js +++ b/src/data/composite/things/album/withTracks.js @@ -1,24 +1,17 @@ import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; - -import {exitWithoutDependency} from '#composite/control-flow'; import {withFlattenedList, withPropertyFromList} from '#composite/data'; import {withResolvedReferenceList} from '#composite/wiki-data'; +import withTrackSections from './withTrackSections.js'; + export default templateCompositeFrom({ annotation: `withTracks`, outputs: ['#tracks'], steps: () => [ - withResolvedReferenceList({ - list: 'trackSections', - data: 'ownTrackSectionData', - find: input.value(find.unqualifiedTrackSection), - }).outputs({ - ['#resolvedReferenceList']: '#trackSections', - }), + withTrackSections(), withPropertyFromList({ list: '#trackSections', diff --git a/src/data/composite/things/artist/artistTotalDuration.js b/src/data/composite/things/artist/artistTotalDuration.js new file mode 100644 index 00000000..ff709f28 --- /dev/null +++ b/src/data/composite/things/artist/artistTotalDuration.js @@ -0,0 +1,70 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {withFilteredList, withPropertyFromList} from '#composite/data'; +import {withContributionListSums, withReverseContributionList} + from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `artistTotalDuration`, + + compose: false, + + steps: () => [ + withReverseContributionList({ + data: 'trackData', + list: input.value('artistContribs'), + }).outputs({ + '#reverseContributionList': '#contributionsAsArtist', + }), + + withReverseContributionList({ + data: 'trackData', + list: input.value('contributorContribs'), + }).outputs({ + '#reverseContributionList': '#contributionsAsContributor', + }), + + { + dependencies: [ + '#contributionsAsArtist', + '#contributionsAsContributor', + ], + + compute: (continuation, { + ['#contributionsAsArtist']: artistContribs, + ['#contributionsAsContributor']: contributorContribs, + }) => continuation({ + ['#allContributions']: [ + ...artistContribs, + ...contributorContribs, + ], + }), + }, + + withPropertyFromList({ + list: '#allContributions', + property: input.value('thing'), + }), + + withPropertyFromList({ + list: '#allContributions.thing', + property: input.value('isOriginalRelease'), + }), + + withFilteredList({ + list: '#allContributions', + filter: '#allContributions.thing.isOriginalRelease', + }).outputs({ + '#filteredList': '#originalContributions', + }), + + withContributionListSums({ + list: '#originalContributions', + }), + + exposeDependency({ + dependency: '#contributionListDuration', + }), + ], +}); diff --git a/src/data/composite/things/artist/index.js b/src/data/composite/things/artist/index.js new file mode 100644 index 00000000..55514c71 --- /dev/null +++ b/src/data/composite/things/artist/index.js @@ -0,0 +1 @@ +export {default as artistTotalDuration} from './artistTotalDuration.js'; diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js new file mode 100644 index 00000000..9b22be2e --- /dev/null +++ b/src/data/composite/things/contribution/index.js @@ -0,0 +1,7 @@ +export {default as inheritFromContributionPresets} from './inheritFromContributionPresets.js'; +export {default as thingPropertyMatches} from './thingPropertyMatches.js'; +export {default as thingReferenceTypeMatches} from './thingReferenceTypeMatches.js'; +export {default as withContainingReverseContributionList} from './withContainingReverseContributionList.js'; +export {default as withContributionArtist} from './withContributionArtist.js'; +export {default as withContributionContext} from './withContributionContext.js'; +export {default as withMatchingContributionPresets} from './withMatchingContributionPresets.js'; diff --git a/src/data/composite/things/contribution/inheritFromContributionPresets.js b/src/data/composite/things/contribution/inheritFromContributionPresets.js new file mode 100644 index 00000000..82425b9c --- /dev/null +++ b/src/data/composite/things/contribution/inheritFromContributionPresets.js @@ -0,0 +1,61 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromList, withPropertyFromObject} from '#composite/data'; + +import withMatchingContributionPresets + from './withMatchingContributionPresets.js'; + +export default templateCompositeFrom({ + annotation: `inheritFromContributionPresets`, + + inputs: { + property: input({type: 'string'}), + }, + + steps: () => [ + withMatchingContributionPresets().outputs({ + '#matchingContributionPresets': '#presets', + }), + + raiseOutputWithoutDependency({ + dependency: '#presets', + mode: input.value('empty'), + }), + + withPropertyFromList({ + list: '#presets', + property: input('property'), + }), + + { + dependencies: ['#values'], + + compute: (continuation, { + ['#values']: values, + }) => continuation({ + ['#index']: + values.findIndex(value => + value !== undefined && + value !== null), + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#index', + mode: input.value('index'), + }), + + { + dependencies: ['#values', '#index'], + + compute: (continuation, { + ['#values']: values, + ['#index']: index, + }) => continuation({ + ['#value']: + values[index], + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js new file mode 100644 index 00000000..4a37f2cf --- /dev/null +++ b/src/data/composite/things/contribution/thingPropertyMatches.js @@ -0,0 +1,33 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `thingPropertyMatches`, + + compose: false, + + inputs: { + value: input({type: 'string'}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: 'thingProperty', + value: input.value(false), + }), + + { + dependencies: [ + 'thingProperty', + input('value'), + ], + + compute: ({ + ['thingProperty']: thingProperty, + [input('value')]: value, + }) => + thingProperty === value, + }, + ], +}); diff --git a/src/data/composite/things/contribution/thingReferenceTypeMatches.js b/src/data/composite/things/contribution/thingReferenceTypeMatches.js new file mode 100644 index 00000000..2ee811af --- /dev/null +++ b/src/data/composite/things/contribution/thingReferenceTypeMatches.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `thingReferenceTypeMatches`, + + compose: false, + + inputs: { + value: input({type: 'string'}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: 'thing', + value: input.value(false), + }), + + withPropertyFromObject({ + object: 'thing', + property: input.value('constructor'), + }), + + { + dependencies: [ + '#thing.constructor', + input('value'), + ], + + compute: ({ + ['#thing.constructor']: constructor, + [input('value')]: value, + }) => + constructor[Symbol.for('Thing.referenceType')] === value, + }, + ], +}); diff --git a/src/data/composite/things/contribution/withContainingReverseContributionList.js b/src/data/composite/things/contribution/withContainingReverseContributionList.js new file mode 100644 index 00000000..56704c8b --- /dev/null +++ b/src/data/composite/things/contribution/withContainingReverseContributionList.js @@ -0,0 +1,40 @@ +// Get the artist's contribution list containing this property. + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withContributionArtist from './withContributionArtist.js'; + +export default templateCompositeFrom({ + annotation: `withContainingReverseContributionList`, + + inputs: { + artistProperty: input({ + defaultDependency: 'artistProperty', + acceptsNull: true, + }), + }, + + outputs: ['#containingReverseContributionList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('artistProperty'), + output: input.value({ + ['#containingReverseContributionList']: + null, + }), + }), + + withContributionArtist(), + + withPropertyFromObject({ + object: '#artist', + property: input('artistProperty'), + }).outputs({ + ['#value']: '#containingReverseContributionList', + }), + ], +}); diff --git a/src/data/composite/things/contribution/withContributionArtist.js b/src/data/composite/things/contribution/withContributionArtist.js new file mode 100644 index 00000000..5a611c1a --- /dev/null +++ b/src/data/composite/things/contribution/withContributionArtist.js @@ -0,0 +1,34 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; + +import {withPropertyFromObject} from '#composite/data'; +import {withResolvedReference} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withContributionArtist`, + + inputs: { + ref: input({ + type: 'string', + defaultDependency: 'artist', + }), + }, + + outputs: ['#artist'], + + steps: () => [ + withPropertyFromObject({ + object: 'thing', + property: input.value('artistData'), + internal: input.value(true), + }), + + withResolvedReference({ + ref: input('ref'), + data: '#thing.artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReference': '#artist', + }), + ], +}); diff --git a/src/data/composite/things/contribution/withContributionContext.js b/src/data/composite/things/contribution/withContributionContext.js new file mode 100644 index 00000000..3c1c31c0 --- /dev/null +++ b/src/data/composite/things/contribution/withContributionContext.js @@ -0,0 +1,45 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withContributionContext`, + + outputs: [ + '#contributionTarget', + '#contributionProperty', + ], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'thing', + output: input.value({ + '#contributionTarget': null, + '#contributionProperty': null, + }), + }), + + raiseOutputWithoutDependency({ + dependency: 'thingProperty', + output: input.value({ + '#contributionTarget': null, + '#contributionProperty': null, + }), + }), + + { + dependencies: ['thing', 'thingProperty'], + + compute: (continuation, { + ['thing']: thing, + ['thingProperty']: thingProperty, + }) => continuation({ + ['#contributionTarget']: + thing.constructor[Symbol.for('Thing.referenceType')], + + ['#contributionProperty']: + thingProperty, + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/withMatchingContributionPresets.js b/src/data/composite/things/contribution/withMatchingContributionPresets.js new file mode 100644 index 00000000..09454164 --- /dev/null +++ b/src/data/composite/things/contribution/withMatchingContributionPresets.js @@ -0,0 +1,70 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withContributionContext from './withContributionContext.js'; + +export default templateCompositeFrom({ + annotation: `withMatchingContributionPresets`, + + outputs: ['#matchingContributionPresets'], + + steps: () => [ + withPropertyFromObject({ + object: 'thing', + property: input.value('wikiInfo'), + internal: input.value(true), + }), + + raiseOutputWithoutDependency({ + dependency: '#thing.wikiInfo', + output: input.value({ + '#matchingContributionPresets': null, + }), + }), + + withPropertyFromObject({ + object: '#thing.wikiInfo', + property: input.value('contributionPresets'), + }).outputs({ + '#thing.wikiInfo.contributionPresets': '#contributionPresets', + }), + + raiseOutputWithoutDependency({ + dependency: '#contributionPresets', + mode: input.value('empty'), + output: input.value({ + '#matchingContributionPresets': [], + }), + }), + + withContributionContext(), + + { + dependencies: [ + '#contributionPresets', + '#contributionTarget', + '#contributionProperty', + 'annotation', + ], + + compute: (continuation, { + ['#contributionPresets']: presets, + ['#contributionTarget']: target, + ['#contributionProperty']: property, + ['annotation']: annotation, + }) => continuation({ + ['#matchingContributionPresets']: + presets + .filter(preset => + preset.context[0] === target && + preset.context.slice(1).includes(property) && + // For now, only match if the annotation is a complete match. + // Partial matches (e.g. because the contribution includes "two" + // annotations, separated by commas) don't count. + preset.annotation === annotation), + }) + }, + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js index 8959de9f..714858a0 100644 --- a/src/data/composite/things/track/index.js +++ b/src/data/composite/things/track/index.js @@ -1,12 +1,16 @@ export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; export {default as inferredAdditionalNameList} from './inferredAdditionalNameList.js'; +export {default as inheritContributionListFromOriginalRelease} from './inheritContributionListFromOriginalRelease.js'; export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js'; export {default as sharedAdditionalNameList} from './sharedAdditionalNameList.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 withDate} from './withDate.js'; export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; +export {default as withOriginalRelease} from './withOriginalRelease.js'; export {default as withOtherReleases} from './withOtherReleases.js'; export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; export {default as withPropertyFromOriginalRelease} from './withPropertyFromOriginalRelease.js'; +export {default as withTrackArtDate} from './withTrackArtDate.js'; diff --git a/src/data/composite/things/track/inheritContributionListFromOriginalRelease.js b/src/data/composite/things/track/inheritContributionListFromOriginalRelease.js new file mode 100644 index 00000000..f4ae3ddb --- /dev/null +++ b/src/data/composite/things/track/inheritContributionListFromOriginalRelease.js @@ -0,0 +1,44 @@ +// Like inheritFromOriginalRelease, but tuned for contributions. +// Recontextualized contributions for this track. + +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withRecontextualizedContributionList, withRedatedContributionList} + from '#composite/wiki-data'; + +import withDate from './withDate.js'; +import withPropertyFromOriginalRelease + from './withPropertyFromOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `inheritContributionListFromOriginalRelease`, + + steps: () => [ + withPropertyFromOriginalRelease({ + property: input.thisProperty(), + notFoundValue: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: '#isRerelease', + mode: input.value('falsy'), + }), + + withRecontextualizedContributionList({ + list: '#originalValue', + }), + + withDate(), + + withRedatedContributionList({ + list: '#originalValue', + date: '#date', + }), + + exposeDependency({ + dependency: '#originalValue', + }), + ], +}); diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js index eaac14de..2c42709b 100644 --- a/src/data/composite/things/track/withContainingTrackSection.js +++ b/src/data/composite/things/track/withContainingTrackSection.js @@ -30,7 +30,6 @@ export default templateCompositeFrom({ compute: (continuation, { [input.myself()]: track, - [input('notFoundMode')]: notFoundMode, ['#album.trackSections']: trackSections, }) => continuation({ ['#trackSection']: diff --git a/src/data/composite/things/track/withDate.js b/src/data/composite/things/track/withDate.js new file mode 100644 index 00000000..b5a770e9 --- /dev/null +++ b/src/data/composite/things/track/withDate.js @@ -0,0 +1,34 @@ +// Gets the track's own date. This is either its dateFirstReleased property +// or, if unset, the album's date. + +import {input, templateCompositeFrom} from '#composite'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withDate`, + + outputs: ['#date'], + + steps: () => [ + { + dependencies: ['dateFirstReleased'], + compute: (continuation, {dateFirstReleased}) => + (dateFirstReleased + ? continuation.raiseOutput({'#date': dateFirstReleased}) + : continuation()), + }, + + withPropertyFromAlbum({ + property: input.value('date'), + }), + + { + dependencies: ['#album.date'], + compute: (continuation, {['#album.date']: albumDate}) => + (albumDate + ? continuation.raiseOutput({'#date': albumDate}) + : continuation.raiseOutput({'#date': null})), + }, + ], +}) diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js index 96078d5f..f7e65f25 100644 --- a/src/data/composite/things/track/withHasUniqueCoverArt.js +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -29,7 +29,10 @@ export default templateCompositeFrom({ : continuation()), }, - withResolvedContribs({from: 'coverArtistContribs'}), + withResolvedContribs({ + from: 'coverArtistContribs', + date: input.value(null), + }), { dependencies: ['#resolvedContribs'], diff --git a/src/data/composite/things/track/withTrackArtDate.js b/src/data/composite/things/track/withTrackArtDate.js new file mode 100644 index 00000000..e2c4d8bc --- /dev/null +++ b/src/data/composite/things/track/withTrackArtDate.js @@ -0,0 +1,80 @@ +// Gets the date of cover art release. This represents only the track's own +// unique cover artwork, if any. +// +// If the 'fallback' option is false (the default), this will only output +// the track's own coverArtDate or its album's trackArtDate. If 'fallback' +// is set, and neither of these is available, it'll output the track's own +// date instead. + +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withDate from './withDate.js'; +import withHasUniqueCoverArt from './withHasUniqueCoverArt.js'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withTrackArtDate`, + + inputs: { + from: input({ + validate: isDate, + defaultDependency: 'coverArtDate', + acceptsNull: true, + }), + + fallback: input({ + type: 'boolean', + defaultValue: false, + }), + }, + + outputs: ['#trackArtDate'], + + steps: () => [ + withHasUniqueCoverArt(), + + raiseOutputWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + output: input.value({'#trackArtDate': null}), + }), + + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from + ? continuation.raiseOutput({'#trackArtDate': from}) + : continuation()), + }, + + withPropertyFromAlbum({ + property: input.value('trackArtDate'), + }), + + { + dependencies: [ + '#album.trackArtDate', + input('fallback'), + ], + + compute: (continuation, { + ['#album.trackArtDate']: albumTrackArtDate, + [input('fallback')]: fallback, + }) => + (albumTrackArtDate + ? continuation.raiseOutput({'#trackArtDate': albumTrackArtDate}) + : fallback + ? continuation() + : continuation.raiseOutput({'#trackArtDate': null})), + }, + + withDate().outputs({ + '#date': '#trackArtDate', + }), + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js index 2c8219fc..cf52950d 100644 --- a/src/data/composite/wiki-data/exitWithoutContribs.js +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -24,6 +24,7 @@ export default templateCompositeFrom({ steps: () => [ withResolvedContribs({ from: input('contribs'), + date: input.value(null), }), // TODO: Fairly certain exitWithoutDependency would be sufficient here. diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index 15ebaffa..5f17ca3a 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -6,9 +6,14 @@ export {default as exitWithoutContribs} from './exitWithoutContribs.js'; export {default as inputWikiData} from './inputWikiData.js'; +export {default as withClonedThings} from './withClonedThings.js'; +export {default as withContributionListSums} from './withContributionListSums.js'; +export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withDirectory} from './withDirectory.js'; export {default as withDirectoryFromName} from './withDirectoryFromName.js'; export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; +export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js'; +export {default as withRedatedContributionList} from './withRedatedContributionList.js'; export {default as withResolvedContribs} from './withResolvedContribs.js'; export {default as withResolvedReference} from './withResolvedReference.js'; export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; diff --git a/src/data/composite/wiki-data/withClonedThings.js b/src/data/composite/wiki-data/withClonedThings.js new file mode 100644 index 00000000..9af6aa84 --- /dev/null +++ b/src/data/composite/wiki-data/withClonedThings.js @@ -0,0 +1,68 @@ +// Clones all the things in a list. If the 'assign' input is provided, +// all new things are assigned the same specified properties. If the +// 'assignEach' input is provided, each new thing is assigned the +// corresponding properties. + +import CacheableObject from '#cacheable-object'; +import {input, templateCompositeFrom} from '#composite'; +import {isObject, sparseArrayOf} from '#validators'; + +import {withMappedList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withClonedThings`, + + inputs: { + things: input({type: 'array'}), + + assign: input({ + type: 'object', + defaultValue: null, + }), + + assignEach: input({ + validate: sparseArrayOf(isObject), + defaultValue: null, + }), + }, + + outputs: ['#clonedThings'], + + steps: () => [ + { + dependencies: [input('assign'), input('assignEach')], + compute: (continuation, { + [input('assign')]: assign, + [input('assignEach')]: assignEach, + }) => continuation({ + ['#assignmentMap']: + (index) => + (assign && assignEach + ? {...assignEach[index] ?? {}, ...assign} + : assignEach + ? assignEach[index] ?? {} + : assign ?? {}), + }), + }, + + { + dependencies: ['#assignmentMap'], + compute: (continuation, { + ['#assignmentMap']: assignmentMap, + }) => continuation({ + ['#cloningMap']: + (thing, index) => + Object.assign( + CacheableObject.clone(thing), + assignmentMap(index)), + }), + }, + + withMappedList({ + list: input('things'), + map: '#cloningMap', + }).outputs({ + '#mappedList': '#clonedThings', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withContributionListSums.js b/src/data/composite/wiki-data/withContributionListSums.js new file mode 100644 index 00000000..b4f36361 --- /dev/null +++ b/src/data/composite/wiki-data/withContributionListSums.js @@ -0,0 +1,95 @@ +// Gets the total duration and contribution count from a list of contributions, +// respecting their `countInContributionTotals` and `countInDurationTotals` +// flags. + +import {input, templateCompositeFrom} from '#composite'; + +import { + withFilteredList, + withPropertiesFromList, + withPropertyFromList, + withSum, + withUniqueItemsOnly, +} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withContributionListSums`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: [ + '#contributionListCount', + '#contributionListDuration', + ], + + steps: () => [ + withPropertiesFromList({ + list: input('list'), + properties: input.value([ + 'countInContributionTotals', + 'countInDurationTotals', + ]), + }), + + withFilteredList({ + list: input('list'), + filter: '#list.countInContributionTotals', + }).outputs({ + '#filteredList': '#contributionsForCounting', + }), + + withFilteredList({ + list: input('list'), + filter: '#list.countInDurationTotals', + }).outputs({ + '#filteredList': '#contributionsForDuration', + }), + + { + dependencies: ['#contributionsForCounting'], + compute: (continuation, { + ['#contributionsForCounting']: contributionsForCounting, + }) => continuation({ + ['#count']: + contributionsForCounting.length, + }), + }, + + withPropertyFromList({ + list: '#contributionsForDuration', + property: input.value('thing'), + }), + + // Don't double-up the durations for a track where the artist has multiple + // contributions. + withUniqueItemsOnly({ + list: '#contributionsForDuration.thing', + }), + + withPropertyFromList({ + list: '#contributionsForDuration.thing', + property: input.value('duration'), + }).outputs({ + '#contributionsForDuration.thing.duration': '#durationValues', + }), + + withSum({ + values: '#durationValues', + }).outputs({ + '#sum': '#duration', + }), + + { + dependencies: ['#count', '#duration'], + compute: (continuation, { + ['#count']: count, + ['#duration']: duration, + }) => continuation({ + ['#contributionListCount']: count, + ['#contributionListDuration']: duration, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/wiki-data/withCoverArtDate.js new file mode 100644 index 00000000..0c644c77 --- /dev/null +++ b/src/data/composite/wiki-data/withCoverArtDate.js @@ -0,0 +1,70 @@ +// Gets the current thing's coverArtDate, or, if the 'fallback' option is set, +// the thing's date. This is always null if the thing doesn't actually have +// any coverArtistContribs. + +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `withCoverArtDate`, + + inputs: { + from: input({ + validate: isDate, + defaultDependency: 'coverArtDate', + acceptsNull: true, + }), + + fallback: input({ + type: 'boolean', + defaultValue: false, + }), + }, + + outputs: ['#coverArtDate'], + + steps: () => [ + withResolvedContribs({ + from: 'coverArtistContribs', + date: input.value(null), + }), + + raiseOutputWithoutDependency({ + dependency: '#resolvedContribs', + mode: input.value('empty'), + output: input.value({'#coverArtDate': null}), + }), + + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from + ? continuation.raiseOutput({'#coverArtDate': from}) + : continuation()), + }, + + { + dependencies: [input('fallback')], + compute: (continuation, { + [input('fallback')]: fallback, + }) => + (fallback + ? continuation() + : continuation.raiseOutput({'#coverArtDate': null})), + }, + + { + dependencies: ['date'], + compute: (continuation, {date}) => + (date + ? continuation.raiseOutput({'#coverArtDate': date}) + : continuation.raiseOutput({'#coverArtDate': null})), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withRecontextualizedContributionList.js b/src/data/composite/wiki-data/withRecontextualizedContributionList.js new file mode 100644 index 00000000..d2401eac --- /dev/null +++ b/src/data/composite/wiki-data/withRecontextualizedContributionList.js @@ -0,0 +1,101 @@ +// Clones all the contributions in a list, with thing and thingProperty both +// updated to match the current thing. Overwrites the provided dependency. +// Optionally updates artistProperty as well. Doesn't do anything if +// the provided dependency is null. +// +// See also: +// - withRedatedContributionList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isStringNonEmpty} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withClonedThings} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withRecontextualizedContributionList`, + + inputs: { + list: input.staticDependency({ + type: 'array', + acceptsNull: true, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list], + + steps: () => [ + // TODO: Is raiseOutputWithoutDependency workable here? + // Is it true that not specifying any output wouldn't overwrite + // the provided dependency? + { + dependencies: [ + input.staticDependency('list'), + input('list'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: dependency, + [input('list')]: list, + }) => + (list + ? continuation() + : continuation.raiseOutput({ + [dependency]: list, + })), + }, + + { + dependencies: [ + input.myself(), + input.thisProperty(), + input('artistProperty'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input.thisProperty()]: thisProperty, + [input('artistProperty')]: artistProperty, + }) => continuation({ + ['#assignment']: + Object.assign( + {thing: myself}, + {thingProperty: thisProperty}, + + (artistProperty + ? {artistProperty} + : {})), + }), + }, + + withClonedThings({ + things: input('list'), + assign: '#assignment', + }).outputs({ + '#clonedThings': '#newContributions', + }), + + { + dependencies: [ + input.staticDependency('list'), + '#newContributions', + ], + + compute: (continuation, { + [input.staticDependency('list')]: listDependency, + ['#newContributions']: newContributions, + }) => continuation({ + [listDependency]: + newContributions, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withRedatedContributionList.js b/src/data/composite/wiki-data/withRedatedContributionList.js new file mode 100644 index 00000000..12f3e16b --- /dev/null +++ b/src/data/composite/wiki-data/withRedatedContributionList.js @@ -0,0 +1,127 @@ +// Clones all the contributions in a list, with date updated to the provided +// value. Overwrites the provided dependency. Doesn't do anything if the +// provided dependency is null, or the provided date is null. +// +// If 'override' is true (the default), then so long as the provided date has +// a value at all, it's always written onto the (cloned) contributions. +// +// If 'override' is false, and any of the contributions were already dated, +// those will keep their existing dates. +// +// See also: +// - withRecontextualizedContributionList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {withMappedList, withPropertyFromList} from '#composite/data'; +import {withClonedThings} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withRedatedContributionList`, + + inputs: { + list: input.staticDependency({ + type: 'array', + acceptsNull: true, + }), + + date: input({ + validate: isDate, + acceptsNull: true, + }), + + override: input({ + type: 'boolean', + defaultValue: true, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list], + + steps: () => [ + // TODO: Is raiseOutputWithoutDependency workable here? + // Is it true that not specifying any output wouldn't overwrite + // the provided dependency? + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('date'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: dependency, + [input('list')]: list, + [input('date')]: date, + }) => + (list && date + ? continuation() + : continuation.raiseOutput({ + [dependency]: list, + })), + }, + + withPropertyFromList({ + list: input('list'), + property: input.value('date'), + }).outputs({ + '#list.date': '#existingDates', + }), + + { + dependencies: [ + input('date'), + input('override'), + '#existingDates', + ], + + compute: (continuation, { + [input('date')]: date, + [input('override')]: override, + '#existingDates': existingDates, + }) => continuation({ + ['#assignmentMap']: + // TODO: Should be mapping over withIndicesFromList + (_, index) => + (!override && existingDates[index] + ? {date: existingDates[index]} + : date + ? {date} + : {}), + }), + }, + + withMappedList({ + list: input('list'), + map: '#assignmentMap', + }).outputs({ + '#mappedList': '#assignment', + }), + + withClonedThings({ + things: input('list'), + assignEach: '#assignment', + }).outputs({ + '#clonedThings': '#newContributions', + }), + + { + dependencies: [ + input.staticDependency('list'), + '#newContributions', + ], + + compute: (continuation, { + [input.staticDependency('list')]: listDependency, + ['#newContributions']: newContributions, + }) => continuation({ + [listDependency]: + newContributions, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index 95266382..b5d7255b 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -7,17 +7,11 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; import {filterMultipleArrays, stitchArrays} from '#sugar'; -import {is, isContributionList} from '#validators'; +import thingConstructors from '#things'; +import {is, isContributionList, isDate, isStringNonEmpty} from '#validators'; -import { - raiseOutputWithoutDependency, -} from '#composite/control-flow'; - -import { - withPropertiesFromList, -} from '#composite/data'; - -import withResolvedReferenceList from './withResolvedReferenceList.js'; +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertiesFromList} from '#composite/data'; export default templateCompositeFrom({ annotation: `withResolvedContribs`, @@ -28,10 +22,25 @@ export default templateCompositeFrom({ acceptsNull: true, }), + date: input({ + validate: isDate, + acceptsNull: true, + }), + notFoundMode: input({ validate: is('exit', 'filter', 'null'), defaultValue: 'null', }), + + thingProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), }, outputs: ['#resolvedContribs'], @@ -45,34 +54,96 @@ export default templateCompositeFrom({ }), }), + { + dependencies: [ + input('thingProperty'), + input.staticDependency('from'), + ], + + compute: (continuation, { + [input('thingProperty')]: thingProperty, + [input.staticDependency('from')]: fromDependency, + }) => continuation({ + ['#thingProperty']: + (thingProperty + ? thingProperty + : !fromDependency?.startsWith('#') + ? fromDependency + : null), + }), + }, + withPropertiesFromList({ list: input('from'), properties: input.value(['artist', 'annotation']), prefix: input.value('#contribs'), }), - withResolvedReferenceList({ - list: '#contribs.artist', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input('notFoundMode'), - }).outputs({ - ['#resolvedReferenceList']: '#contribs.artist', - }), - { - dependencies: ['#contribs.artist', '#contribs.annotation'], + dependencies: [ + '#contribs.artist', + '#contribs.annotation', + input('date'), + ], compute(continuation, { ['#contribs.artist']: artist, ['#contribs.annotation']: annotation, + [input('date')]: date, }) { filterMultipleArrays(artist, annotation, (artist, _annotation) => artist); + return continuation({ - ['#resolvedContribs']: - stitchArrays({artist, annotation}), + ['#details']: + stitchArrays({artist, annotation}) + .map(details => ({ + ...details, + date: date ?? null, + })), }); }, }, + + { + dependencies: [ + '#details', + '#thingProperty', + input('artistProperty'), + input.myself(), + ], + + compute: (continuation, { + ['#details']: details, + ['#thingProperty']: thingProperty, + [input('artistProperty')]: artistProperty, + [input.myself()]: myself, + }) => continuation({ + ['#contributions']: + details.map(details => { + const contrib = new thingConstructors.Contribution(); + + Object.assign(contrib, { + ...details, + thing: myself, + thingProperty: thingProperty, + artistProperty: artistProperty, + }); + + return contrib; + }), + }), + }, + + { + dependencies: ['#contributions'], + + compute: (continuation, { + ['#contributions']: contributions, + }) => continuation({ + ['#resolvedContribs']: + contributions + .filter(contrib => contrib.artist), + }), + }, ], }); diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js index 91e125e4..63e712bb 100644 --- a/src/data/composite/wiki-data/withReverseContributionList.js +++ b/src/data/composite/wiki-data/withReverseContributionList.js @@ -1,6 +1,6 @@ // Analogous implementation for withReverseReferenceList, for contributions. -// This is all duplicate code and both should be ported to the same underlying -// data form later on. +// This is mostly duplicate code and both should be ported to the same +// underlying data form later on. // // This implementation uses a global cache (via WeakMap) to attempt to speed // up subsequent similar accesses. @@ -10,9 +10,11 @@ // is used, a fresh cache will always be created. import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; import {exitWithoutDependency, raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withFlattenedList, withMappedList} from '#composite/data'; import inputWikiData from './inputWikiData.js'; @@ -33,6 +35,8 @@ export default templateCompositeFrom({ outputs: ['#reverseContributionList'], steps: () => [ + // Common behavior -- + // Early exit with an empty array if the data list isn't available. exitWithoutDependency({ dependency: input('data'), @@ -46,46 +50,122 @@ export default templateCompositeFrom({ output: input.value({'#reverseContributionList': []}), }), + // Check for an existing cache record which corresponds to this + // input('list') and input('data'). If it exists, query it for the + // current thing, and raise that; if it doesn't, create it, put it + // where it needs to be, and provide it so the next steps can fill + // it in. { - dependencies: [input.myself(), input('data'), input('list')], + dependencies: [input('list'), input('data'), input.myself()], compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, [input('list')]: list, + [input('data')]: data, + [input.myself()]: myself, }) => { if (!caches.has(list)) { - caches.set(list, new WeakMap()); + const cache = new WeakMap(); + caches.set(list, cache); + + const cacheRecord = new WeakMap(); + cache.set(data, cacheRecord); + + return continuation({ + ['#cacheRecord']: cacheRecord, + }); } const cache = caches.get(list); if (!cache.has(data)) { const cacheRecord = new WeakMap(); + cache.set(data, cacheRecord); + + return continuation({ + ['#cacheRecord']: cacheRecord, + }); + } + + return continuation.raiseOutput({ + ['#reverseContributionList']: + cache.get(data).get(myself) ?? [], + }); + }, + }, + + // Unique behavior for contribution lists -- + + { + dependencies: [input('list')], + compute: (continuation, { + [input('list')]: list, + }) => continuation({ + ['#contributionListMap']: + thing => thing[list], + }), + }, + + withMappedList({ + list: input('data'), + map: '#contributionListMap', + }).outputs({ + '#mappedList': '#contributionLists', + }), - for (const referencingThing of data) { - const referenceList = referencingThing[list]; + withFlattenedList({ + list: '#contributionLists', + }).outputs({ + '#flattenedList': '#referencingThings', + }), - // Destructuring {artist} is the only unique part of the - // withReverseContributionList implementation, compared to - // withReverseReferneceList. - for (const {artist: referencedThing} of referenceList) { + withMappedList({ + list: '#referencingThings', + map: input.value(contrib => [contrib.artist]), + }).outputs({ + '#mappedList': '#referencedThings', + }), + + // Common behavior -- + + // Actually fill in the cache record. Since we're building up a *reverse* + // reference list, track connections in terms of the referenced thing. + // No newly-provided dependencies here since we're mutating the cache + // record, which is properly in store and will probably be reused in the + // future (and certainly in the next step). + { + dependencies: ['#cacheRecord', '#referencingThings', '#referencedThings'], + compute: (continuation, { + ['#cacheRecord']: cacheRecord, + ['#referencingThings']: referencingThings, + ['#referencedThings']: referencedThings, + }) => { + stitchArrays({ + referencingThing: referencingThings, + referencedThings: referencedThings, + }).forEach(({referencingThing, referencedThings}) => { + for (const referencedThing of referencedThings) { if (cacheRecord.has(referencedThing)) { cacheRecord.get(referencedThing).push(referencingThing); } else { cacheRecord.set(referencedThing, [referencingThing]); } } - } + }); - cache.set(data, cacheRecord); - } - - return continuation({ - ['#reverseContributionList']: - cache.get(data).get(myself) ?? [], - }); + return continuation(); }, }, + + // Then just pluck out the current object from the now-filled cache record! + { + dependencies: ['#cacheRecord', input.myself()], + compute: (continuation, { + ['#cacheRecord']: cacheRecord, + [input.myself()]: myself, + }) => continuation({ + ['#reverseContributionList']: + cacheRecord.get(myself) ?? [], + }), + }, ], }); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 8cd540a5..1f8c082f 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -12,9 +12,11 @@ // so any changes should be reflected there (until these are combined). import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; import {exitWithoutDependency, raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withMappedList} from '#composite/data'; import inputWikiData from './inputWikiData.js'; @@ -35,6 +37,8 @@ export default templateCompositeFrom({ outputs: ['#reverseReferenceList'], steps: () => [ + // Common behavior -- + // Early exit with an empty array if the data list isn't available. exitWithoutDependency({ dependency: input('data'), @@ -48,42 +52,119 @@ export default templateCompositeFrom({ output: input.value({'#reverseReferenceList': []}), }), + // Check for an existing cache record which corresponds to this + // input('list') and input('data'). If it exists, query it for the + // current thing, and raise that; if it doesn't, create it, put it + // where it needs to be, and provide it so the next steps can fill + // it in. { - dependencies: [input.myself(), input('data'), input('list')], + dependencies: [input('list'), input('data'), input.myself()], compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, [input('list')]: list, + [input('data')]: data, + [input.myself()]: myself, }) => { if (!caches.has(list)) { - caches.set(list, new WeakMap()); + const cache = new WeakMap(); + caches.set(list, cache); + + const cacheRecord = new WeakMap(); + cache.set(data, cacheRecord); + + return continuation({ + ['#cacheRecord']: cacheRecord, + }); } const cache = caches.get(list); if (!cache.has(data)) { const cacheRecord = new WeakMap(); + cache.set(data, cacheRecord); + + return continuation({ + ['#cacheRecord']: cacheRecord, + }); + } + + return continuation.raiseOutput({ + ['#reverseReferenceList']: + cache.get(data).get(myself) ?? [], + }); + }, + }, + + // Unique behavior for reference lists -- + + { + dependencies: [input('list')], + compute: (continuation, { + [input('list')]: list, + }) => continuation({ + ['#referenceMap']: + thing => thing[list], + }), + }, + + withMappedList({ + list: input('data'), + map: '#referenceMap', + }).outputs({ + '#mappedList': '#referencedThings', + }), + + { + dependencies: [input('data')], + compute: (continuation, { + [input('data')]: data, + }) => continuation({ + ['#referencingThings']: + data, + }), + }, + + // Common behavior -- - for (const referencingThing of data) { - const referenceList = referencingThing[list]; - for (const referencedThing of referenceList) { + // Actually fill in the cache record. Since we're building up a *reverse* + // reference list, track connections in terms of the referenced thing. + // No newly-provided dependencies here since we're mutating the cache + // record, which is properly in store and will probably be reused in the + // future (and certainly in the next step). + { + dependencies: ['#cacheRecord', '#referencingThings', '#referencedThings'], + compute: (continuation, { + ['#cacheRecord']: cacheRecord, + ['#referencingThings']: referencingThings, + ['#referencedThings']: referencedThings, + }) => { + stitchArrays({ + referencingThing: referencingThings, + referencedThings: referencedThings, + }).forEach(({referencingThing, referencedThings}) => { + for (const referencedThing of referencedThings) { if (cacheRecord.has(referencedThing)) { cacheRecord.get(referencedThing).push(referencingThing); } else { cacheRecord.set(referencedThing, [referencingThing]); } } - } + }); - cache.set(data, cacheRecord); - } - - return continuation({ - ['#reverseReferenceList']: - cache.get(data).get(myself) ?? [], - }); + return continuation(); }, }, + + // Then just pluck out the current object from the now-filled cache record! + { + dependencies: ['#cacheRecord', input.myself()], + compute: (continuation, { + ['#cacheRecord']: cacheRecord, + [input.myself()]: myself, + }) => continuation({ + ['#reverseReferenceList']: + cacheRecord.get(myself) ?? [], + }), + }, ], }); diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js index aad12a2d..d9a6b417 100644 --- a/src/data/composite/wiki-properties/contributionList.js +++ b/src/data/composite/wiki-properties/contributionList.js @@ -15,7 +15,7 @@ // import {input, templateCompositeFrom} from '#composite'; -import {isContributionList} from '#validators'; +import {isContributionList, isDate, isStringNonEmpty} from '#validators'; import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow'; import {withResolvedContribs} from '#composite/wiki-data'; @@ -25,11 +25,34 @@ export default templateCompositeFrom({ compose: false, + inputs: { + date: input({ + validate: isDate, + acceptsNull: true, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + }, + update: {validate: isContributionList}, steps: () => [ - withResolvedContribs({from: input.updateValue()}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: input.value([])}), + withResolvedContribs({ + from: input.updateValue(), + thingProperty: input.thisProperty(), + artistProperty: input('artistProperty'), + date: input('date'), + }), + + exposeDependencyOrContinue({ + dependency: '#resolvedContribs', + }), + + exposeConstant({ + value: input.value([]), + }), ], }); diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js index 89cb6838..5328d17e 100644 --- a/src/data/composite/wiki-properties/index.js +++ b/src/data/composite/wiki-properties/index.js @@ -24,5 +24,6 @@ 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 thing} from './thing.js'; export {default as urls} from './urls.js'; export {default as wikiData} from './wikiData.js'; diff --git a/src/data/composite/wiki-properties/thing.js b/src/data/composite/wiki-properties/thing.js new file mode 100644 index 00000000..5b5d77dd --- /dev/null +++ b/src/data/composite/wiki-properties/thing.js @@ -0,0 +1,31 @@ +// An individual Thing, provided directly rather than by reference. + +import {input, templateCompositeFrom} from '#composite'; +import {isThingClass, validateThing} from '#validators'; + +export default templateCompositeFrom({ + annotation: `wikiData`, + + compose: false, + + inputs: { + class: input.staticValue({ + validate: isThingClass, + defaultValue: null, + }), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => ({ + validate: + validateThing({ + referenceType: + (thingClass + ? thingClass[Symbol.for('Thing.referenceType')] + : ''), + }), + }), + + steps: () => [], +}); |