diff options
Diffstat (limited to 'src/data/composite/wiki-data')
31 files changed, 1852 insertions, 355 deletions
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/gobbleSoupyFind.js b/src/data/composite/wiki-data/gobbleSoupyFind.js new file mode 100644 index 00000000..aec3f5b1 --- /dev/null +++ b/src/data/composite/wiki-data/gobbleSoupyFind.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withPropertyFromObject} from '#composite/data'; + +import inputSoupyFind, {getSoupyFindInputKey} from './inputSoupyFind.js'; + +export default templateCompositeFrom({ + annotation: `gobbleSoupyFind`, + + inputs: { + find: inputSoupyFind(), + }, + + outputs: ['#find'], + + steps: () => [ + { + dependencies: [input('find')], + compute: (continuation, { + [input('find')]: find, + }) => + (typeof find === 'function' + ? continuation.raiseOutput({ + ['#find']: find, + }) + : continuation({ + ['#key']: + getSoupyFindInputKey(find), + })), + }, + + withPropertyFromObject({ + object: 'find', + property: '#key', + }).outputs({ + '#value': '#find', + }), + ], +}); diff --git a/src/data/composite/wiki-data/gobbleSoupyReverse.js b/src/data/composite/wiki-data/gobbleSoupyReverse.js new file mode 100644 index 00000000..86a1061c --- /dev/null +++ b/src/data/composite/wiki-data/gobbleSoupyReverse.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withPropertyFromObject} from '#composite/data'; + +import inputSoupyReverse, {getSoupyReverseInputKey} from './inputSoupyReverse.js'; + +export default templateCompositeFrom({ + annotation: `gobbleSoupyReverse`, + + inputs: { + reverse: inputSoupyReverse(), + }, + + outputs: ['#reverse'], + + steps: () => [ + { + dependencies: [input('reverse')], + compute: (continuation, { + [input('reverse')]: reverse, + }) => + (typeof reverse === 'function' + ? continuation.raiseOutput({ + ['#reverse']: reverse, + }) + : continuation({ + ['#key']: + getSoupyReverseInputKey(reverse), + })), + }, + + withPropertyFromObject({ + object: 'reverse', + property: '#key', + }).outputs({ + '#value': '#reverse', + }), + ], +}); diff --git a/src/data/composite/wiki-data/helpers/withDirectoryFromName.js b/src/data/composite/wiki-data/helpers/withDirectoryFromName.js new file mode 100644 index 00000000..f85dae16 --- /dev/null +++ b/src/data/composite/wiki-data/helpers/withDirectoryFromName.js @@ -0,0 +1,41 @@ +// Compute a directory from a name. + +import {input, templateCompositeFrom} from '#composite'; + +import {isName} from '#validators'; +import {getKebabCase} from '#wiki-data'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withDirectoryFromName`, + + inputs: { + name: input({ + validate: isName, + acceptsNull: true, + }), + }, + + outputs: ['#directory'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('name'), + mode: input.value('falsy'), + output: input.value({ + ['#directory']: null, + }), + }), + + { + dependencies: [input('name')], + compute: (continuation, { + [input('name')]: name, + }) => continuation({ + ['#directory']: + getKebabCase(name), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/helpers/withResolvedReverse.js b/src/data/composite/wiki-data/helpers/withResolvedReverse.js new file mode 100644 index 00000000..818f60b7 --- /dev/null +++ b/src/data/composite/wiki-data/helpers/withResolvedReverse.js @@ -0,0 +1,40 @@ +// Actually execute a reverse function. + +import {input, templateCompositeFrom} from '#composite'; + +import inputWikiData from '../inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + data: inputWikiData({allowMixedTypes: true}), + reverse: input({type: 'function'}), + options: input({type: 'object', defaultValue: null}), + }, + + outputs: ['#resolvedReverse'], + + steps: () => [ + { + dependencies: [ + input.myself(), + input('data'), + input('reverse'), + input('options'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input('data')]: data, + [input('reverse')]: reverseFunction, + [input('options')]: opts, + }) => continuation({ + ['#resolvedReverse']: + (data + ? reverseFunction(myself, data, opts) + : reverseFunction(myself, opts)), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/helpers/withSimpleDirectory.js b/src/data/composite/wiki-data/helpers/withSimpleDirectory.js new file mode 100644 index 00000000..08ca3bfc --- /dev/null +++ b/src/data/composite/wiki-data/helpers/withSimpleDirectory.js @@ -0,0 +1,52 @@ +// A "simple" directory, based only on the already-provided directory, if +// available, or the provided name. + +import {input, templateCompositeFrom} from '#composite'; + +import {isDirectory, isName} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withDirectoryFromName from './withDirectoryFromName.js'; + +export default templateCompositeFrom({ + annotation: `withSimpleDirectory`, + + inputs: { + directory: input({ + validate: isDirectory, + defaultDependency: 'directory', + acceptsNull: true, + }), + + name: input({ + validate: isName, + acceptsNull: true, + }), + }, + + outputs: ['#directory'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('directory'), + }), + + { + dependencies: ['#availability', input('directory')], + compute: (continuation, { + ['#availability']: availability, + [input('directory')]: directory, + }) => + (availability + ? continuation.raiseOutput({ + ['#directory']: directory + }) + : continuation()), + }, + + withDirectoryFromName({ + name: input('name'), + }), + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index b4cf6d13..1d94f74b 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -5,12 +5,28 @@ // export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as gobbleSoupyFind} from './gobbleSoupyFind.js'; +export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js'; +export {default as inputNotFoundMode} from './inputNotFoundMode.js'; +export {default as inputSoupyFind} from './inputSoupyFind.js'; +export {default as inputSoupyReverse} from './inputSoupyReverse.js'; export {default as inputWikiData} from './inputWikiData.js'; +export {default as processContentEntryDates} from './processContentEntryDates.js'; +export {default as withClonedThings} from './withClonedThings.js'; +export {default as withConstitutedArtwork} from './withConstitutedArtwork.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 withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; +export {default as withParsedContentEntries} from './withParsedContentEntries.js'; +export {default as withParsedLyricsEntries} from './withParsedLyricsEntries.js'; +export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js'; +export {default as withRedatedContributionList} from './withRedatedContributionList.js'; +export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.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 withReverseContributionList} from './withReverseContributionList.js'; +export {default as withResolvedSeriesList} from './withResolvedSeriesList.js'; export {default as withReverseReferenceList} from './withReverseReferenceList.js'; export {default as withThingsSortedAlphabetically} from './withThingsSortedAlphabetically.js'; export {default as withUniqueReferencingThing} from './withUniqueReferencingThing.js'; diff --git a/src/data/composite/wiki-data/inputNotFoundMode.js b/src/data/composite/wiki-data/inputNotFoundMode.js new file mode 100644 index 00000000..d16b2472 --- /dev/null +++ b/src/data/composite/wiki-data/inputNotFoundMode.js @@ -0,0 +1,9 @@ +import {input} from '#composite'; +import {is} from '#validators'; + +export default function inputNotFoundMode() { + return input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }); +} diff --git a/src/data/composite/wiki-data/inputSoupyFind.js b/src/data/composite/wiki-data/inputSoupyFind.js new file mode 100644 index 00000000..020f4990 --- /dev/null +++ b/src/data/composite/wiki-data/inputSoupyFind.js @@ -0,0 +1,28 @@ +import {input} from '#composite'; +import {anyOf, isFunction, isString} from '#validators'; + +function inputSoupyFind() { + return input({ + validate: + anyOf( + isFunction, + val => { + isString(val); + + if (!val.startsWith('_soupyFind:')) { + throw new Error(`Expected soupyFind.input() token`); + } + + return true; + }), + }); +} + +inputSoupyFind.input = key => + input.value('_soupyFind:' + key); + +export default inputSoupyFind; + +export function getSoupyFindInputKey(value) { + return value.slice('_soupyFind:'.length); +} diff --git a/src/data/composite/wiki-data/inputSoupyReverse.js b/src/data/composite/wiki-data/inputSoupyReverse.js new file mode 100644 index 00000000..0b0a23fe --- /dev/null +++ b/src/data/composite/wiki-data/inputSoupyReverse.js @@ -0,0 +1,32 @@ +import {input} from '#composite'; +import {anyOf, isFunction, isString} from '#validators'; + +function inputSoupyReverse() { + return input({ + validate: + anyOf( + isFunction, + val => { + isString(val); + + if (!val.startsWith('_soupyReverse:')) { + throw new Error(`Expected soupyReverse.input() token`); + } + + return true; + }), + }); +} + +inputSoupyReverse.input = key => + input.value('_soupyReverse:' + key); + +export default inputSoupyReverse; + +export function getSoupyReverseInputKey(value) { + return value.slice('_soupyReverse:'.length).replace(/\.unique$/, ''); +} + +export function doesSoupyReverseInputWantUnique(value) { + return value.endsWith('.unique'); +} diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js index cf7a7c2c..b9021986 100644 --- a/src/data/composite/wiki-data/inputWikiData.js +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -12,6 +12,6 @@ export default function inputWikiData({ } = {}) { return input({ validate: validateWikiData({referenceType, allowMixedTypes}), - acceptsNull: true, + defaultValue: null, }); } diff --git a/src/data/composite/wiki-data/processContentEntryDates.js b/src/data/composite/wiki-data/processContentEntryDates.js new file mode 100644 index 00000000..e418a121 --- /dev/null +++ b/src/data/composite/wiki-data/processContentEntryDates.js @@ -0,0 +1,181 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isContentString, isString, looseArrayOf} from '#validators'; + +import {fillMissingListItems} from '#composite/data'; + +// Important note: These two kinds of inputs have the exact same shape!! +// This isn't on purpose (besides that they *are* both supposed to be strings). +// They just don't have any more particular validation, yet. + +const inputDateList = defaultDependency => + input({ + validate: looseArrayOf(isString), + defaultDependency, + }); + +const inputKindList = defaultDependency => + input.staticDependency({ + validate: looseArrayOf(isString), + defaultDependency: defaultDependency, + }); + +export default templateCompositeFrom({ + annotation: `processContentEntryDates`, + + inputs: { + annotations: input({ + validate: looseArrayOf(isContentString), + defaultDependency: '#entries.annotation', + }), + + dates: inputDateList('#entries.date'), + secondDates: inputDateList('#entries.secondDate'), + accessDates: inputDateList('#entries.accessDate'), + + dateKinds: inputKindList('#entries.dateKind'), + accessKinds: inputKindList('#entries.accessKind'), + }, + + outputs: ({ + [input.staticDependency('dates')]: dates, + [input.staticDependency('secondDates')]: secondDates, + [input.staticDependency('accessDates')]: accessDates, + [input.staticDependency('dateKinds')]: dateKinds, + [input.staticDependency('accessKinds')]: accessKinds, + }) => [ + dates ?? '#processedContentEntryDates', + secondDates ?? '#processedContentEntrySecondDates', + accessDates ?? '#processedContentEntryAccessDates', + dateKinds ?? '#processedContentEntryDateKinds', + accessKinds ?? '#processedContentEntryAccessKinds', + ], + + steps: () => [ + { + dependencies: [input('annotations')], + compute: (continuation, { + [input('annotations')]: annotations, + }) => continuation({ + ['#webArchiveDates']: + annotations + .map(text => text?.match(/https?:\/\/web.archive.org\/web\/([0-9]{8,8})[0-9]*\//)) + .map(match => match?.[1]) + .map(dateText => + (dateText + ? dateText.slice(0, 4) + '/' + + dateText.slice(4, 6) + '/' + + dateText.slice(6, 8) + : null)), + }), + }, + + { + dependencies: [input('dates')], + compute: (continuation, { + [input('dates')]: dates, + }) => continuation({ + ['#processedContentEntryDates']: + dates + .map(date => date ? new Date(date) : null), + }), + }, + + { + dependencies: [input('secondDates')], + compute: (continuation, { + [input('secondDates')]: secondDates, + }) => continuation({ + ['#processedContentEntrySecondDates']: + secondDates + .map(date => date ? new Date(date) : null), + }), + }, + + fillMissingListItems({ + list: input('dateKinds'), + fill: input.value(null), + }).outputs({ + '#list': '#processedContentEntryDateKinds', + }), + + { + dependencies: [input('accessDates'), '#webArchiveDates'], + compute: (continuation, { + [input('accessDates')]: accessDates, + ['#webArchiveDates']: webArchiveDates, + }) => continuation({ + ['#processedContentEntryAccessDates']: + stitchArrays({ + accessDate: accessDates, + webArchiveDate: webArchiveDates + }).map(({accessDate, webArchiveDate}) => + accessDate ?? + webArchiveDate ?? + null) + .map(date => date ? new Date(date) : date), + }), + }, + + { + dependencies: [input('accessKinds'), '#webArchiveDates'], + compute: (continuation, { + [input('accessKinds')]: accessKinds, + ['#webArchiveDates']: webArchiveDates, + }) => continuation({ + ['#processedContentEntryAccessKinds']: + stitchArrays({ + accessKind: accessKinds, + webArchiveDate: webArchiveDates, + }).map(({accessKind, webArchiveDate}) => + accessKind ?? + (webArchiveDate && 'captured') ?? + null), + }), + }, + + // TODO: Annoying conversion step for outputs, would be nice to avoid. + { + dependencies: [ + '#processedContentEntryDates', + '#processedContentEntrySecondDates', + '#processedContentEntryAccessDates', + '#processedContentEntryDateKinds', + '#processedContentEntryAccessKinds', + input.staticDependency('dates'), + input.staticDependency('secondDates'), + input.staticDependency('accessDates'), + input.staticDependency('dateKinds'), + input.staticDependency('accessKinds'), + ], + + compute: (continuation, { + ['#processedContentEntryDates']: processedContentEntryDates, + ['#processedContentEntrySecondDates']: processedContentEntrySecondDates, + ['#processedContentEntryAccessDates']: processedContentEntryAccessDates, + ['#processedContentEntryDateKinds']: processedContentEntryDateKinds, + ['#processedContentEntryAccessKinds']: processedContentEntryAccessKinds, + [input.staticDependency('dates')]: dates, + [input.staticDependency('secondDates')]: secondDates, + [input.staticDependency('accessDates')]: accessDates, + [input.staticDependency('dateKinds')]: dateKinds, + [input.staticDependency('accessKinds')]: accessKinds, + }) => continuation({ + [dates ?? '#processedContentEntryDates']: + processedContentEntryDates, + + [secondDates ?? '#processedContentEntrySecondDates']: + processedContentEntrySecondDates, + + [accessDates ?? '#processedContentEntryAccessDates']: + processedContentEntryAccessDates, + + [dateKinds ?? '#processedContentEntryDateKinds']: + processedContentEntryDateKinds, + + [accessKinds ?? '#processedContentEntryAccessKinds']: + processedContentEntryAccessKinds, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/raiseResolvedReferenceList.js b/src/data/composite/wiki-data/raiseResolvedReferenceList.js new file mode 100644 index 00000000..613b002b --- /dev/null +++ b/src/data/composite/wiki-data/raiseResolvedReferenceList.js @@ -0,0 +1,96 @@ +// Concludes compositions like withResolvedReferenceList, which share behavior +// in processing the resolved results before continuing further. + +import {input, templateCompositeFrom} from '#composite'; + +import {withFilteredList} from '#composite/data'; + +import inputNotFoundMode from './inputNotFoundMode.js'; + +export default templateCompositeFrom({ + inputs: { + notFoundMode: inputNotFoundMode(), + + results: input({type: 'array'}), + filter: input({type: 'array'}), + + exitValue: input({defaultValue: []}), + + outputs: input.staticValue({type: 'string'}), + }, + + outputs: ({ + [input.staticValue('outputs')]: outputs, + }) => [outputs], + + steps: () => [ + { + dependencies: [ + input('results'), + input('filter'), + input('outputs'), + ], + + compute: (continuation, { + [input('results')]: results, + [input('filter')]: filter, + [input('outputs')]: outputs, + }) => + (filter.every(keep => keep) + ? continuation.raiseOutput({[outputs]: results}) + : continuation()), + }, + + { + dependencies: [ + input('notFoundMode'), + input('exitValue'), + ], + + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + [input('exitValue')]: exitValue, + }) => + (notFoundMode === 'exit' + ? continuation.exit(exitValue) + : continuation()), + }, + + { + dependencies: [ + input('results'), + input('notFoundMode'), + input('outputs'), + ], + + compute: (continuation, { + [input('results')]: results, + [input('notFoundMode')]: notFoundMode, + [input('outputs')]: outputs, + }) => + (notFoundMode === 'null' + ? continuation.raiseOutput({[outputs]: results}) + : continuation()), + }, + + withFilteredList({ + list: input('results'), + filter: input('filter'), + }), + + { + dependencies: [ + '#filteredList', + input('outputs'), + ], + + compute: (continuation, { + ['#filteredList']: filteredList, + [input('outputs')]: outputs, + }) => continuation({ + [outputs]: + filteredList, + }), + }, + ], +}); 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/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js new file mode 100644 index 00000000..9e260abf --- /dev/null +++ b/src/data/composite/wiki-data/withConstitutedArtwork.js @@ -0,0 +1,57 @@ +import {input, templateCompositeFrom} from '#composite'; +import thingConstructors from '#things'; +import {isContributionList} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withConstitutedArtwork`, + + inputs: { + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + outputs: ['#constitutedArtwork'], + + steps: () => [ + { + dependencies: [ + input.myself(), + input('dimensionsFromThingProperty'), + input('fileExtensionFromThingProperty'), + input('dateFromThingProperty'), + input('artistContribsFromThingProperty'), + input('artistContribsArtistProperty'), + input('artTagsFromThingProperty'), + input('referencedArtworksFromThingProperty'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input('dimensionsFromThingProperty')]: dimensionsFromThingProperty, + [input('fileExtensionFromThingProperty')]: fileExtensionFromThingProperty, + [input('dateFromThingProperty')]: dateFromThingProperty, + [input('artistContribsFromThingProperty')]: artistContribsFromThingProperty, + [input('artistContribsArtistProperty')]: artistContribsArtistProperty, + [input('artTagsFromThingProperty')]: artTagsFromThingProperty, + [input('referencedArtworksFromThingProperty')]: referencedArtworksFromThingProperty, + }) => continuation({ + ['#constitutedArtwork']: + Object.assign(new thingConstructors.Artwork, { + thing: myself, + dimensionsFromThingProperty, + fileExtensionFromThingProperty, + artistContribsFromThingProperty, + artistContribsArtistProperty, + artTagsFromThingProperty, + dateFromThingProperty, + referencedArtworksFromThingProperty, + }), + }), + }, + ], +}); 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..a114d5ff --- /dev/null +++ b/src/data/composite/wiki-data/withCoverArtDate.js @@ -0,0 +1,51 @@ +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, + }), + }, + + 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: ['date'], + compute: (continuation, {date}) => + (date + ? continuation({'#coverArtDate': date}) + : continuation({'#coverArtDate': null})), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withDirectory.js b/src/data/composite/wiki-data/withDirectory.js new file mode 100644 index 00000000..f3bedf2e --- /dev/null +++ b/src/data/composite/wiki-data/withDirectory.js @@ -0,0 +1,62 @@ +// Select a directory, either using a manually specified directory, or +// computing it from a name. By default these values are the current thing's +// 'directory' and 'name' properties, so it can be used without any options +// to get the current thing's effective directory (assuming no custom rules). + +import {input, templateCompositeFrom} from '#composite'; + +import {isDirectory, isName} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withSimpleDirectory from './helpers/withSimpleDirectory.js'; + +export default templateCompositeFrom({ + annotation: `withDirectory`, + + inputs: { + directory: input({ + validate: isDirectory, + defaultDependency: 'directory', + acceptsNull: true, + }), + + name: input({ + validate: isName, + defaultDependency: 'name', + acceptsNull: true, + }), + + suffix: input({ + validate: isDirectory, + defaultValue: null, + }), + }, + + outputs: ['#directory'], + + steps: () => [ + withSimpleDirectory({ + directory: input('directory'), + name: input('name'), + }), + + raiseOutputWithoutDependency({ + dependency: '#directory', + output: input.value({['#directory']: null}), + }), + + { + dependencies: ['#directory', input('suffix')], + compute: (continuation, { + ['#directory']: directory, + [input('suffix')]: suffix, + }) => continuation({ + ['#directory']: + (suffix + ? directory + '-' + suffix + : directory), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index f0404a5d..6794c479 100644 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -1,5 +1,4 @@ import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; import {stitchArrays} from '#sugar'; import {isCommentary} from '#validators'; import {commentaryRegexCaseSensitive} from '#wiki-data'; @@ -11,6 +10,9 @@ import { withUnflattenedList, } from '#composite/data'; +import inputSoupyFind from './inputSoupyFind.js'; +import processContentEntryDates from './processContentEntryDates.js'; +import withParsedContentEntries from './withParsedContentEntries.js'; import withResolvedReferenceList from './withResolvedReferenceList.js'; export default templateCompositeFrom({ @@ -23,78 +25,23 @@ export default templateCompositeFrom({ outputs: ['#parsedCommentaryEntries'], steps: () => [ - { - dependencies: [input('from')], - - compute: (continuation, { - [input('from')]: commentaryText, - }) => continuation({ - ['#rawMatches']: - Array.from(commentaryText.matchAll(commentaryRegexCaseSensitive)), - }), - }, - - withPropertiesFromList({ - list: '#rawMatches', - properties: input.value([ - '0', // The entire match as a string. - 'groups', - 'index', - ]), - }).outputs({ - '#rawMatches.0': '#rawMatches.text', - '#rawMatches.groups': '#rawMatches.groups', - '#rawMatches.index': '#rawMatches.startIndex', + withParsedContentEntries({ + from: input('from'), + caseSensitiveRegex: input.value(commentaryRegexCaseSensitive), }), - { - dependencies: [ - '#rawMatches.text', - '#rawMatches.startIndex', - ], - - compute: (continuation, { - ['#rawMatches.text']: text, - ['#rawMatches.startIndex']: startIndex, - }) => continuation({ - ['#rawMatches.endIndex']: - stitchArrays({text, startIndex}) - .map(({text, startIndex}) => startIndex + text.length), - }), - }, - - { - dependencies: [ - input('from'), - '#rawMatches.startIndex', - '#rawMatches.endIndex', - ], - - compute: (continuation, { - [input('from')]: commentaryText, - ['#rawMatches.startIndex']: startIndex, - ['#rawMatches.endIndex']: endIndex, - }) => continuation({ - ['#entries.body']: - stitchArrays({startIndex, endIndex}) - .map(({endIndex}, index, stitched) => - (index === stitched.length - 1 - ? commentaryText.slice(endIndex) - : commentaryText.slice( - endIndex, - stitched[index + 1].startIndex))) - .map(body => body.trim()), - }), - }, - withPropertiesFromList({ - list: '#rawMatches.groups', + list: '#parsedContentEntryHeadings', prefix: input.value('#entries'), properties: input.value([ 'artistReferences', 'artistDisplayText', 'annotation', 'date', + 'secondDate', + 'dateKind', + 'accessDate', + 'accessKind', ]), }), @@ -118,8 +65,7 @@ export default templateCompositeFrom({ withResolvedReferenceList({ list: '#flattenedList', - data: 'artistData', - find: input.value(find.artist), + find: inputSoupyFind.input('artist'), notFoundMode: input.value('null'), }), @@ -139,15 +85,7 @@ export default templateCompositeFrom({ fill: input.value(null), }), - { - dependencies: ['#entries.date'], - compute: (continuation, { - ['#entries.date']: date, - }) => continuation({ - ['#entries.date']: - date.map(date => date ? new Date(date) : null), - }), - }, + processContentEntryDates(), { dependencies: [ @@ -155,7 +93,11 @@ export default templateCompositeFrom({ '#entries.artistDisplayText', '#entries.annotation', '#entries.date', - '#entries.body', + '#entries.secondDate', + '#entries.dateKind', + '#entries.accessDate', + '#entries.accessKind', + '#parsedContentEntryBodies', ], compute: (continuation, { @@ -163,7 +105,11 @@ export default templateCompositeFrom({ ['#entries.artistDisplayText']: artistDisplayText, ['#entries.annotation']: annotation, ['#entries.date']: date, - ['#entries.body']: body, + ['#entries.secondDate']: secondDate, + ['#entries.dateKind']: dateKind, + ['#entries.accessDate']: accessDate, + ['#entries.accessKind']: accessKind, + ['#parsedContentEntryBodies']: body, }) => continuation({ ['#parsedCommentaryEntries']: stitchArrays({ @@ -171,6 +117,10 @@ export default templateCompositeFrom({ artistDisplayText, annotation, date, + secondDate, + dateKind, + accessDate, + accessKind, body, }), }), diff --git a/src/data/composite/wiki-data/withParsedContentEntries.js b/src/data/composite/wiki-data/withParsedContentEntries.js new file mode 100644 index 00000000..2a9b3f6a --- /dev/null +++ b/src/data/composite/wiki-data/withParsedContentEntries.js @@ -0,0 +1,111 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isContentString, validateInstanceOf} from '#validators'; + +import {withPropertiesFromList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withParsedContentEntries`, + + inputs: { + // TODO: Is there any way to validate this input based on the *other* + // inputs proivded, i.e. regexes? This kind of just assumes the string + // has already been validated according to the form the regex expects, + // which *is* always the case (as used), but it seems a bit awkward. + from: input({validate: isContentString}), + + caseSensitiveRegex: input({ + validate: validateInstanceOf(RegExp), + }), + }, + + outputs: [ + '#parsedContentEntryHeadings', + '#parsedContentEntryBodies', + ], + + steps: () => [ + { + dependencies: [ + input('from'), + input('caseSensitiveRegex'), + ], + + compute: (continuation, { + [input('from')]: commentaryText, + [input('caseSensitiveRegex')]: caseSensitiveRegex, + }) => continuation({ + ['#rawMatches']: + Array.from(commentaryText.matchAll(caseSensitiveRegex)), + }), + }, + + withPropertiesFromList({ + list: '#rawMatches', + properties: input.value([ + '0', // The entire match as a string. + 'groups', + 'index', + ]), + }).outputs({ + '#rawMatches.0': '#rawMatches.text', + '#rawMatches.groups': '#parsedContentEntryHeadings', + '#rawMatches.index': '#rawMatches.startIndex', + }), + + { + dependencies: [ + '#rawMatches.text', + '#rawMatches.startIndex', + ], + + compute: (continuation, { + ['#rawMatches.text']: text, + ['#rawMatches.startIndex']: startIndex, + }) => continuation({ + ['#rawMatches.endIndex']: + stitchArrays({text, startIndex}) + .map(({text, startIndex}) => startIndex + text.length), + }), + }, + + { + dependencies: [ + input('from'), + '#rawMatches.startIndex', + '#rawMatches.endIndex', + ], + + compute: (continuation, { + [input('from')]: commentaryText, + ['#rawMatches.startIndex']: startIndex, + ['#rawMatches.endIndex']: endIndex, + }) => continuation({ + ['#parsedContentEntryBodies']: + stitchArrays({startIndex, endIndex}) + .map(({endIndex}, index, stitched) => + (index === stitched.length - 1 + ? commentaryText.slice(endIndex) + : commentaryText.slice( + endIndex, + stitched[index + 1].startIndex))) + .map(body => body.trim()), + }), + }, + + { + dependencies: [ + '#parsedContentEntryHeadings', + '#parsedContentEntryBodies', + ], + + compute: (continuation, { + ['#parsedContentEntryHeadings']: parsedContentEntryHeadings, + ['#parsedContentEntryBodies']: parsedContentEntryBodies, + }) => continuation({ + ['#parsedContentEntryHeadings']: parsedContentEntryHeadings, + ['#parsedContentEntryBodies']: parsedContentEntryBodies, + }) + } + ], +}); diff --git a/src/data/composite/wiki-data/withParsedLyricsEntries.js b/src/data/composite/wiki-data/withParsedLyricsEntries.js new file mode 100644 index 00000000..d13bfbaa --- /dev/null +++ b/src/data/composite/wiki-data/withParsedLyricsEntries.js @@ -0,0 +1,157 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isLyrics} from '#validators'; +import {commentaryRegexCaseSensitive, oldStyleLyricsDetectionRegex} + from '#wiki-data'; + +import { + fillMissingListItems, + withFlattenedList, + withPropertiesFromList, + withUnflattenedList, +} from '#composite/data'; + +import inputSoupyFind from './inputSoupyFind.js'; +import processContentEntryDates from './processContentEntryDates.js'; +import withParsedContentEntries from './withParsedContentEntries.js'; +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +function constituteLyricsEntry(text) { + return { + artists: [], + artistDisplayText: null, + annotation: null, + date: null, + secondDate: null, + dateKind: null, + accessDate: null, + accessKind: null, + body: text, + }; +} + +export default templateCompositeFrom({ + annotation: `withParsedLyricsEntries`, + + inputs: { + from: input({validate: isLyrics}), + }, + + outputs: ['#parsedLyricsEntries'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: lyrics, + }) => + (oldStyleLyricsDetectionRegex.test(lyrics) + ? continuation() + : continuation.raiseOutput({ + ['#parsedLyricsEntries']: + [constituteLyricsEntry(lyrics)], + })), + }, + + withParsedContentEntries({ + from: input('from'), + caseSensitiveRegex: input.value(commentaryRegexCaseSensitive), + }), + + withPropertiesFromList({ + list: '#parsedContentEntryHeadings', + prefix: input.value('#entries'), + properties: input.value([ + 'artistReferences', + 'artistDisplayText', + 'annotation', + 'date', + 'secondDate', + 'dateKind', + 'accessDate', + 'accessKind', + ]), + }), + + // The artistReferences group will always have a value, since it's required + // for the line to match in the first place. + + { + dependencies: ['#entries.artistReferences'], + compute: (continuation, { + ['#entries.artistReferences']: artistReferenceTexts, + }) => continuation({ + ['#entries.artistReferences']: + artistReferenceTexts + .map(text => text.split(',').map(ref => ref.trim())), + }), + }, + + withFlattenedList({ + list: '#entries.artistReferences', + }), + + withResolvedReferenceList({ + list: '#flattenedList', + find: inputSoupyFind.input('artist'), + notFoundMode: input.value('null'), + }), + + withUnflattenedList({ + list: '#resolvedReferenceList', + }).outputs({ + '#unflattenedList': '#entries.artists', + }), + + fillMissingListItems({ + list: '#entries.artistDisplayText', + fill: input.value(null), + }), + + fillMissingListItems({ + list: '#entries.annotation', + fill: input.value(null), + }), + + processContentEntryDates(), + + { + dependencies: [ + '#entries.artists', + '#entries.artistDisplayText', + '#entries.annotation', + '#entries.date', + '#entries.secondDate', + '#entries.dateKind', + '#entries.accessDate', + '#entries.accessKind', + '#parsedContentEntryBodies', + ], + + compute: (continuation, { + ['#entries.artists']: artists, + ['#entries.artistDisplayText']: artistDisplayText, + ['#entries.annotation']: annotation, + ['#entries.date']: date, + ['#entries.secondDate']: secondDate, + ['#entries.dateKind']: dateKind, + ['#entries.accessDate']: accessDate, + ['#entries.accessKind']: accessKind, + ['#parsedContentEntryBodies']: body, + }) => continuation({ + ['#parsedLyricsEntries']: + stitchArrays({ + artists, + artistDisplayText, + annotation, + date, + secondDate, + dateKind, + accessDate, + accessKind, + body, + }), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withRecontextualizedContributionList.js b/src/data/composite/wiki-data/withRecontextualizedContributionList.js new file mode 100644 index 00000000..bcc6e486 --- /dev/null +++ b/src/data/composite/wiki-data/withRecontextualizedContributionList.js @@ -0,0 +1,100 @@ +// 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 {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/withResolvedAnnotatedReferenceList.js b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js new file mode 100644 index 00000000..9cc52f29 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js @@ -0,0 +1,100 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isObject, validateArrayItems} from '#validators'; + +import {withPropertyFromList} from '#composite/data'; + +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; + +import inputSoupyFind from './inputSoupyFind.js'; +import inputNotFoundMode from './inputNotFoundMode.js'; +import inputWikiData from './inputWikiData.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedAnnotatedReferenceList`, + + inputs: { + list: input({ + validate: validateArrayItems(isObject), + acceptsNull: true, + }), + + reference: input({type: 'string', defaultValue: 'reference'}), + annotation: input({type: 'string', defaultValue: 'annotation'}), + thing: input({type: 'string', defaultValue: 'thing'}), + + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), + + notFoundMode: inputNotFoundMode(), + }, + + outputs: ['#resolvedAnnotatedReferenceList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedAnnotatedReferenceList']: [], + }), + }), + + withPropertyFromList({ + list: input('list'), + property: input('reference'), + }).outputs({ + ['#values']: '#references', + }), + + withPropertyFromList({ + list: input('list'), + property: input('annotation'), + }).outputs({ + ['#values']: '#annotations', + }), + + withResolvedReferenceList({ + list: '#references', + data: input('data'), + find: input('find'), + notFoundMode: input.value('null'), + }), + + { + dependencies: [ + input('thing'), + input('annotation'), + '#resolvedReferenceList', + '#annotations', + ], + + compute: (continuation, { + [input('thing')]: thingProperty, + [input('annotation')]: annotationProperty, + ['#resolvedReferenceList']: things, + ['#annotations']: annotations, + }) => continuation({ + ['#matches']: + stitchArrays({ + [thingProperty]: things, + [annotationProperty]: annotations, + }), + }), + }, + + withAvailabilityFilter({ + from: '#resolvedReferenceList', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#matches', + filter: '#availabilityFilter', + outputs: input.value('#resolvedAnnotatedReferenceList'), + }), + ], +}) diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index 77b0f96d..838c991f 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -1,22 +1,20 @@ // 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. +// means mapping the artist reference of each contribution to an artist +// object, and filtering out those whose artist reference doesn't match +// any artist. import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; import {filterMultipleArrays, stitchArrays} from '#sugar'; -import {is, isContributionList} from '#validators'; +import thingConstructors from '#things'; +import {isContributionList, isDate, isStringNonEmpty} from '#validators'; -import { - raiseOutputWithoutDependency, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; +import {withPropertyFromList, withPropertiesFromList} from '#composite/data'; -import { - withPropertiesFromList, -} from '#composite/data'; - -import withResolvedReferenceList from './withResolvedReferenceList.js'; +import inputNotFoundMode from './inputNotFoundMode.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedContribs`, @@ -27,9 +25,21 @@ export default templateCompositeFrom({ acceptsNull: true, }), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', + date: input({ + validate: isDate, + acceptsNull: true, + }), + + notFoundMode: inputNotFoundMode(), + + thingProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, }), }, @@ -44,33 +54,103 @@ 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(['who', 'what']), + properties: input.value(['artist', 'annotation']), 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'], + dependencies: [ + '#contribs.artist', + '#contribs.annotation', + input('date'), + ], compute(continuation, { - ['#contribs.who']: who, - ['#contribs.what']: what, + ['#contribs.artist']: artist, + ['#contribs.annotation']: annotation, + [input('date')]: date, }) { - filterMultipleArrays(who, what, (who, _what) => who); + filterMultipleArrays(artist, annotation, (artist, _annotation) => artist); + return continuation({ - ['#resolvedContribs']: stitchArrays({who, what}), + ['#details']: + stitchArrays({artist, annotation}) + .map(details => ({ + ...details, + date: date ?? null, + })), }); }, }, + + { + dependencies: [ + '#details', + '#thingProperty', + input('artistProperty'), + input.myself(), + 'find', + ], + + compute: (continuation, { + ['#details']: details, + ['#thingProperty']: thingProperty, + [input('artistProperty')]: artistProperty, + [input.myself()]: myself, + ['find']: find, + }) => continuation({ + ['#contributions']: + details.map(details => { + const contrib = new thingConstructors.Contribution(); + + Object.assign(contrib, { + ...details, + thing: myself, + thingProperty: thingProperty, + artistProperty: artistProperty, + find: find, + }); + + return contrib; + }), + }), + }, + + withPropertyFromList({ + list: '#contributions', + property: input.value('artist'), + }), + + withAvailabilityFilter({ + from: '#contributions.artist', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#contributions', + filter: '#availabilityFilter', + outputs: input.value('#resolvedContribs'), + }), ], }); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js index ea71707e..6f422194 100644 --- a/src/data/composite/wiki-data/withResolvedReference.js +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -1,16 +1,14 @@ // 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. Otherwise, the data object is provided on the -// output dependency, or null, if the reference doesn't match anything or +// within the provided thingData dependency. 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 { - exitWithoutDependency, - raiseOutputWithoutDependency, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; export default templateCompositeFrom({ @@ -20,7 +18,7 @@ export default templateCompositeFrom({ ref: input({type: 'string', acceptsNull: true}), data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), + find: inputSoupyFind(), }, outputs: ['#resolvedReference'], @@ -33,24 +31,26 @@ export default templateCompositeFrom({ }), }), - exitWithoutDependency({ - dependency: input('data'), + gobbleSoupyFind({ + find: input('find'), }), { dependencies: [ input('ref'), input('data'), - input('find'), + '#find', ], compute: (continuation, { [input('ref')]: ref, [input('data')]: data, - [input('find')]: findFunction, + ['#find']: findFunction, }) => continuation({ ['#resolvedReference']: - findFunction(ref, data, {mode: 'quiet'}) ?? null, + (data + ? findFunction(ref, data, {mode: 'quiet'}) ?? null + : findFunction(ref, {mode: 'quiet'}) ?? null), }), }, ], diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js index 1d39e5b2..9dc960dd 100644 --- a/src/data/composite/wiki-data/withResolvedReferenceList.js +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -1,18 +1,20 @@ // 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'). +// data in the same way as withResolvedReference. 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 {isString, validateArrayItems} from '#validators'; -import { - exitWithoutDependency, - raiseOutputWithoutDependency, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; +import {withMappedList} from '#composite/data'; +import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputNotFoundMode from './inputNotFoundMode.js'; +import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedReferenceList`, @@ -23,23 +25,15 @@ export default templateCompositeFrom({ acceptsNull: true, }), - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), + notFoundMode: inputNotFoundMode(), }, outputs: ['#resolvedReferenceList'], steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - raiseOutputWithoutDependency({ dependency: input('list'), mode: input.value('empty'), @@ -48,54 +42,39 @@ export default templateCompositeFrom({ }), }), + gobbleSoupyFind({ + find: input('find'), + }), + { - dependencies: [input('list'), input('data'), input('find')], + dependencies: [input('data'), '#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()), + ['#find']: findFunction, + }) => continuation({ + ['#map']: + (data + ? ref => findFunction(ref, data, {mode: 'quiet'}) + : ref => findFunction(ref, {mode: 'quiet'})), + }), }, - { - 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), - }); + withMappedList({ + list: input('list'), + map: '#map', + }).outputs({ + '#mappedList': '#matches', + }), - case 'null': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.map(match => match ?? null), - }); + withAvailabilityFilter({ + from: '#matches', + }), - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#matches', + filter: '#availabilityFilter', + outputs: input.value('#resolvedReferenceList'), + }), ], }); diff --git a/src/data/composite/wiki-data/withResolvedSeriesList.js b/src/data/composite/wiki-data/withResolvedSeriesList.js new file mode 100644 index 00000000..deaab466 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedSeriesList.js @@ -0,0 +1,130 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isSeriesList, validateThing} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import { + fillMissingListItems, + withFlattenedList, + withUnflattenedList, + withPropertiesFromList, +} from '#composite/data'; + +import inputSoupyFind from './inputSoupyFind.js'; +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedSeriesList`, + + inputs: { + group: input({ + validate: validateThing({referenceType: 'group'}), + }), + + list: input({ + validate: isSeriesList, + acceptsNull: true, + }), + }, + + outputs: ['#resolvedSeriesList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedSeriesList']: [], + }), + }), + + withPropertiesFromList({ + list: input('list'), + prefix: input.value('#serieses'), + properties: input.value([ + 'name', + 'description', + 'albums', + + 'showAlbumArtists', + ]), + }), + + fillMissingListItems({ + list: '#serieses.albums', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#serieses.albums', + }), + + withResolvedReferenceList({ + list: '#flattenedList', + find: inputSoupyFind.input('album'), + notFoundMode: input.value('null'), + }), + + withUnflattenedList({ + list: '#resolvedReferenceList', + }).outputs({ + '#unflattenedList': '#serieses.albums', + }), + + fillMissingListItems({ + list: '#serieses.description', + fill: input.value(null), + }), + + fillMissingListItems({ + list: '#serieses.showAlbumArtists', + fill: input.value(null), + }), + + { + dependencies: [ + '#serieses.name', + '#serieses.description', + '#serieses.albums', + + '#serieses.showAlbumArtists', + ], + + compute: (continuation, { + ['#serieses.name']: name, + ['#serieses.description']: description, + ['#serieses.albums']: albums, + + ['#serieses.showAlbumArtists']: showAlbumArtists, + }) => continuation({ + ['#seriesProperties']: + stitchArrays({ + name, + description, + albums, + + showAlbumArtists, + }).map(properties => ({ + ...properties, + group: input + })) + }), + }, + + { + dependencies: ['#seriesProperties', input('group')], + compute: (continuation, { + ['#seriesProperties']: seriesProperties, + [input('group')]: group, + }) => continuation({ + ['#resolvedSeriesList']: + seriesProperties + .map(properties => ({ + ...properties, + group, + })), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js deleted file mode 100644 index eccb58b7..00000000 --- a/src/data/composite/wiki-data/withReverseContributionList.js +++ /dev/null @@ -1,83 +0,0 @@ -// 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 implementation uses a global cache (via WeakMap) to attempt to speed -// up subsequent similar accesses. -// -// This has absolutely not been rigorously tested with altering properties of -// data objects in a wiki data array which is reused. If a new wiki data array -// is used, a fresh cache will always be created. - -import {input, templateCompositeFrom} from '#composite'; - -import {exitWithoutDependency} from '#composite/control-flow'; - -import inputWikiData from './inputWikiData.js'; - -// Mapping of reference list property to WeakMap. -// Each WeakMap maps a wiki data array to another weak map, -// which in turn maps each referenced thing to an array of -// things referencing it. -const caches = new Map(); - -export default templateCompositeFrom({ - annotation: `withReverseContributionList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseContributionList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - mode: input.value('empty'), - }), - - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, - [input('list')]: list, - }) => { - if (!caches.has(list)) { - caches.set(list, new WeakMap()); - } - - const cache = caches.get(list); - - if (!cache.has(data)) { - const cacheRecord = new WeakMap(); - - for (const referencingThing of data) { - const referenceList = referencingThing[list]; - - // Destructuring {who} is the only unique part of the - // withReverseContributionList implementation, compared to - // withReverseReferneceList. - for (const {who: referencedThing} of referenceList) { - 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) ?? [], - }); - }, - }, - ], -}); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 2d7a421b..906f5bc5 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -1,81 +1,36 @@ // Check out the info on reverseReferenceList! // This is its composable form. -// -// This implementation uses a global cache (via WeakMap) to attempt to speed -// up subsequent similar accesses. -// -// This has absolutely not been rigorously tested with altering properties of -// data objects in a wiki data array which is reused. If a new wiki data array -// is used, a fresh cache will always be created. -// -// Note that this implementation is mirrored in withReverseContributionList, -// so any changes should be reflected there (until these are combined). import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency} from '#composite/control-flow'; - +import gobbleSoupyReverse from './gobbleSoupyReverse.js'; +import inputSoupyReverse from './inputSoupyReverse.js'; import inputWikiData from './inputWikiData.js'; -// Mapping of reference list property to WeakMap. -// Each WeakMap maps a wiki data array to another weak map, -// which in turn maps each referenced thing to an array of -// things referencing it. -const caches = new Map(); +import withResolvedReverse from './helpers/withResolvedReverse.js'; export default templateCompositeFrom({ annotation: `withReverseReferenceList`, inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), + data: inputWikiData({allowMixedTypes: true}), + reverse: inputSoupyReverse(), }, outputs: ['#reverseReferenceList'], steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - mode: input.value('empty'), + gobbleSoupyReverse({ + reverse: input('reverse'), }), - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, - [input('list')]: list, - }) => { - if (!caches.has(list)) { - caches.set(list, new WeakMap()); - } - - const cache = caches.get(list); - - if (!cache.has(data)) { - const cacheRecord = new WeakMap(); + // TODO: Check that the reverse spec returns a list. - for (const referencingThing of data) { - const referenceList = referencingThing[list]; - for (const referencedThing of referenceList) { - 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) ?? [], - }); - }, - }, + withResolvedReverse({ + data: input('data'), + reverse: '#reverse', + }).outputs({ + '#resolvedReverse': '#reverseReferenceList', + }), ], }); diff --git a/src/data/composite/wiki-data/withUniqueReferencingThing.js b/src/data/composite/wiki-data/withUniqueReferencingThing.js index ce04f838..7c267038 100644 --- a/src/data/composite/wiki-data/withUniqueReferencingThing.js +++ b/src/data/composite/wiki-data/withUniqueReferencingThing.js @@ -4,49 +4,33 @@ import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency, raiseOutputWithoutDependency} - from '#composite/control-flow'; - +import gobbleSoupyReverse from './gobbleSoupyReverse.js'; +import inputSoupyReverse from './inputSoupyReverse.js'; import inputWikiData from './inputWikiData.js'; -import withReverseReferenceList from './withReverseReferenceList.js'; + +import withResolvedReverse from './helpers/withResolvedReverse.js'; export default templateCompositeFrom({ annotation: `withUniqueReferencingThing`, inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), + data: inputWikiData({allowMixedTypes: true}), + reverse: inputSoupyReverse(), }, outputs: ['#uniqueReferencingThing'], steps: () => [ - // withReverseRefernceList does this check too, but it early exits with - // an empty array. That's no good here! - exitWithoutDependency({ - dependency: input('data'), - mode: input.value('empty'), + gobbleSoupyReverse({ + reverse: input('reverse'), }), - withReverseReferenceList({ + withResolvedReverse({ data: input('data'), - list: input('list'), + reverse: '#reverse', + options: input.value({unique: true}), + }).outputs({ + '#resolvedReverse': '#uniqueReferencingThing', }), - - raiseOutputWithoutDependency({ - dependency: '#reverseReferenceList', - mode: input.value('empty'), - output: input.value({'#uniqueReferencingThing': null}), - }), - - { - dependencies: ['#reverseReferenceList'], - compute: (continuation, { - ['#reverseReferenceList']: reverseReferenceList, - }) => continuation({ - ['#uniqueReferencingThing']: - reverseReferenceList[0], - }), - }, ], }); |