From b90c2072f1ef8f55ef495bfa3920af4bb482f0cd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 22:13:08 -0300 Subject: data: valdiateArrayItems: use same index formatting as other errors Specifically, the same as decorateErrorWithIndex. --- src/data/things/validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index fc953c2a..5748eacf 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -174,7 +174,7 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${color.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; throw error; } }; -- cgit 1.3.0-6-gf8a5 From 218a99a3164e8ae6967335190b72fd36275d1892 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 10:58:55 -0300 Subject: data, test: track: inherit album props more declaratively --- src/data/things/track.js | 225 ++++++++++++++++++++--------------------------- src/data/yaml.js | 6 +- 2 files changed, 98 insertions(+), 133 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index e176acb4..39c2930f 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,59 +44,58 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - hasCoverArt: { + // Disables presenting the track as though it has its own unique artwork. + // This flag should only be used in select circumstances, i.e. to override + // an album's trackCoverArtists. This flag supercedes that property, as well + // as the track's own coverArtists. + disableUniqueCoverArt: Thing.common.flag(), + + // File extension for track's corresponding media file. This represents the + // track's unique cover artwork, if any, and does not inherit the cover's + // main artwork. (It does inherit `trackCoverArtFileExtension` if present + // on the album.) + coverArtFileExtension: { flags: {update: true, expose: true}, - update: { - validate(value) { - if (value !== false) { - throw new TypeError(`Expected false or null`); - } + update: {validate: isFileExtension}, - return true; - }, - }, + expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef'], - transform: (hasCoverArt, { - albumData, + transform(coverArtFileExtension, { coverArtistContribsByRef, - [Track.instance]: track, - }) => - Track.hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ), - }, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + }, + }), }, - coverArtFileExtension: { + // Date of cover art release. Like coverArtFileExtension, this represents + // only the track's own unique cover artwork, if any. This exposes only as + // the track's own coverArtDate or its album's trackArtDate, so if neither + // is specified, this value is null. + coverArtDate: { flags: {update: true, expose: true}, - update: {validate: isFileExtension}, + update: {validate: isDate}, - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef'], - transform: (coverArtFileExtension, { - albumData, + expose: Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtDate, { coverArtistContribsByRef, - hasCoverArt, - [Track.instance]: track, - }) => - coverArtFileExtension ?? - (Track.hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) - ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension - : Track.findAlbum(track, albumData)?.coverArtFileExtension) ?? - 'jpg', - }, + disableUniqueCoverArt, + album: {trackArtDate, trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtDate ?? trackArtDate; + }, + }), }, originalReleaseTrackByRef: Thing.common.singleReference(Track), @@ -170,53 +169,29 @@ export class Track extends Thing { }, }, - coverArtDate: { - flags: {update: true, expose: true}, - - update: {validate: isDate}, - - expose: { - dependencies: [ - 'albumData', - 'coverArtistContribsByRef', - 'dateFirstReleased', - 'hasCoverArt', - ], - transform: (coverArtDate, { - albumData, - coverArtistContribsByRef, - dateFirstReleased, - hasCoverArt, - [Track.instance]: track, - }) => - (Track.hasCoverArt(track, albumData, coverArtistContribsByRef, hasCoverArt) - ? coverArtDate ?? - dateFirstReleased ?? - Track.findAlbum(track, albumData)?.trackArtDate ?? - Track.findAlbum(track, albumData)?.date ?? - null - : null), - }, - }, - + // Whether or not the track has "unique" cover artwork - a cover which is + // specifically associated with this track in particular, rather than with + // the track's album as a whole. This is typically used to select between + // displaying the track artwork and a fallback, such as the album artwork + // or a placeholder. (This property is named hasUniqueCoverArt instead of + // the usual hasCoverArt to emphasize that it does not inherit from the + // album.) hasUniqueCoverArt: { flags: {expose: true}, - expose: { - dependencies: ['albumData', 'coverArtistContribsByRef', 'hasCoverArt'], - compute: ({ - albumData, + expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef'], { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + compute({ coverArtistContribsByRef, - hasCoverArt, - [Track.instance]: track, - }) => - Track.hasUniqueCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ), - }, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return false; + if (!empty(coverArtistContribsByRef)) true; + if (!empty(trackCoverArtistContribsByRef)) return true; + return false; + }, + }), }, originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( @@ -342,53 +317,6 @@ export class Track extends Thing { ), }); - // This is a quick utility function for now, since the same code is reused in - // several places. Ideally it wouldn't be - we'd just reuse the `album` - // property - but support for that hasn't been coded yet :P - static findAlbum = (track, albumData) => - albumData?.find((album) => album.tracks.includes(track)); - - // Another reused utility function. This one's logic is a bit more complicated. - static hasCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) { - if (!empty(coverArtistContribsByRef)) { - return true; - } - - const album = Track.findAlbum(track, albumData); - if (album && !empty(album.trackCoverArtistContribsByRef)) { - return true; - } - - return false; - } - - static hasUniqueCoverArt( - track, - albumData, - coverArtistContribsByRef, - hasCoverArt - ) { - if (!empty(coverArtistContribsByRef)) { - return true; - } - - if (hasCoverArt === false) { - return false; - } - - const album = Track.findAlbum(track, albumData); - if (album && !empty(album.trackCoverArtistContribsByRef)) { - return true; - } - - return false; - } - static inheritFromOriginalRelease( originalProperty, originalMissingValue, @@ -423,6 +351,39 @@ export class Track extends Thing { }; } + static withAlbumProperties(albumProperties, oldExpose) { + const applyAlbumDependency = dependencies => { + const track = dependencies[Track.instance]; + const album = + dependencies.albumData + ?.find((album) => album.tracks.includes(track)); + + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } + + return {...dependencies, album: filteredAlbum}; + }; + + const newExpose = {dependencies: [...oldExpose.dependencies, 'albumData']}; + + if (oldExpose.compute) { + newExpose.compute = dependencies => + oldExpose.compute(applyAlbumDependency(dependencies)); + } + + if (oldExpose.transform) { + newExpose.transform = (value, dependencies) => + oldExpose.transform(value, applyAlbumDependency(dependencies)); + } + + return newExpose; + } + [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); diff --git a/src/data/yaml.js b/src/data/yaml.js index 35943199..13412f17 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -316,6 +316,10 @@ export const processTrackDocument = makeProcessDocument(T.Track, { 'Date First Released': (value) => new Date(value), 'Cover Art Date': (value) => new Date(value), + 'Has Cover Art': (value) => + (value === true ? false : + value === false ? true : + value), 'Artists': parseContributors, 'Contributors': parseContributors, @@ -336,7 +340,7 @@ export const processTrackDocument = makeProcessDocument(T.Track, { dateFirstReleased: 'Date First Released', coverArtDate: 'Cover Art Date', coverArtFileExtension: 'Cover Art File Extension', - hasCoverArt: 'Has Cover Art', + disableCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. lyrics: 'Lyrics', commentary: 'Commentary', -- cgit 1.3.0-6-gf8a5 From 55e4afead38bc541cba4ae1cef183527c254f99a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 17:28:15 -0300 Subject: data: track: experimental Thing.compose.from() processing style --- src/data/things/thing.js | 142 ++++++++++++++++++++++- src/data/things/track.js | 290 ++++++++++++++++++++++++----------------------- 2 files changed, 289 insertions(+), 143 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..143c1515 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; +import {empty, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -418,4 +418,144 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } + + static findArtistsFromContribs(contribsByRef, artistData) { + return ( + contribsByRef + .map(({who, what}) => ({ + who: find.artist(who, artistData), + what, + })) + .filter(({who}) => who)); + } + + static composite = { + from(composition) { + const base = composition.at(-1); + const steps = composition.slice(0, -1); + + const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + + if (base.flags.compose) { + aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + } + + const exposeFunctionOrder = []; + const exposeDependencies = new Set(base.expose?.dependencies); + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const message = + (step.annotation + ? `Errors in step #${i + 1} (${step.annotation})` + : `Errors in step #${i + 1}`); + + aggregate.nest({message}, ({push}) => { + if (!step.flags.compose) { + push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); + } + + if (step.flags.update) { + push(new Error(`Steps which update aren't supported yet`)); + } + + if (step.flags.expose) expose: { + if (!step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); + break expose; + } + + if (step.expose.dependencies) { + for (const dependency of step.expose.dependencies) { + exposeDependencies.add(dependency); + } + } + + if (base.flags.update) { + if (step.expose.transform) { + exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + } else { + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } else { + if (step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } + }); + } + + aggregate.close(); + + const constructedDescriptor = {}; + + constructedDescriptor.flags = { + update: !!base.flags.update, + expose: !!base.flags.expose, + compose: false, + }; + + if (base.flags.update) { + constructedDescriptor.update = base.flags.update; + } + + if (base.flags.expose) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); + + const continuationSymbol = Symbol(); + + if (base.flags.update) { + expose.transform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + let valueSoFar = value; + + for (const {type, fn} of exposeFunctionOrder) { + const result = + (type === 'transform' + ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + valueSoFar = updatedValue; + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }) + : fn(dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + })); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.transform(valueSoFar, dependencies); + }; + } else { + expose.compute = (initialDependencies) => { + const dependencies = {...initialDependencies}; + + for (const {fn} of exposeFunctionOrder) { + const result = + fn(valueSoFar, dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.compute(dependencies); + }; + } + } + + return constructedDescriptor; + }, + }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 39c2930f..fe6af205 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -54,49 +54,53 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the cover's // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) - coverArtFileExtension: { - flags: {update: true, expose: true}, - - update: {validate: isFileExtension}, - - expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - transform(coverArtFileExtension, { - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + coverArtFileExtension: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + + { + flags: {update: true, expos: true}, + update: {validate: isFileExtension}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtFileExtension, { + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; + }, }, - }), - }, + }, + ]), // Date of cover art release. Like coverArtFileExtension, this represents // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: { - flags: {update: true, expose: true}, - - update: {validate: isDate}, - - expose: Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - transform(coverArtDate, { - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackArtDate, trackCoverArtistContribsByRef}, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtDate ?? trackArtDate; + coverArtDate: Thing.composite.from([ + Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + + { + flags: {update: true, expose: true}, + update: {validate: isDate}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + transform(coverArtDate, { + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackArtDate, trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return null; + if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; + return coverArtDate ?? trackArtDate; + }, }, - }), - }, + } + ]), originalReleaseTrackByRef: Thing.common.singleReference(Track), @@ -176,23 +180,26 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: { - flags: {expose: true}, - - expose: Track.withAlbumProperties(['trackCoverArtistContribsByRef'], { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - compute({ - coverArtistContribsByRef, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef}, - }) { - if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) true; - if (!empty(trackCoverArtistContribsByRef)) return true; - return false; + hasUniqueCoverArt: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + + { + flags: {expose: true}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + compute({ + coverArtistContribsByRef, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef}, + }) { + if (disableUniqueCoverArt) return false; + if (!empty(coverArtistContribsByRef)) true; + if (!empty(trackCoverArtistContribsByRef)) return true; + return false; + }, }, - }), - }, + }, + ]), originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( 'originalReleaseTrackByRef', @@ -228,43 +235,70 @@ export class Track extends Thing { }, }, - artistContribs: - Track.inheritFromOriginalRelease('artistContribs', [], - Thing.common.dynamicInheritContribs( - null, - 'artistContribsByRef', - 'artistContribsByRef', - 'albumData', - Track.findAlbum)), + artistContribs: Thing.composite.from([ + Track.inheritFromOriginalRelease('artistContribs'), + + { + flags: {expose: true}, + expose: { + dependencies: ['artistContribs'], + + compute({ + artistContribsByRef: contribsFromTrack, + album: {artistContribsByRef: contribsFromAlbum}, + }) { + let contribsByRef = contribsFromTrack; + if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; + if (empty(contribsByRef)) return null; - contributorContribs: - Track.inheritFromOriginalRelease('contributorContribs', [], - Thing.common.dynamicContribs('contributorContribsByRef')), + return Thing.findArtistsFromContribs(contribsByRef, artistData); + }, + }, + }, + ]), + + contributorContribs: Thing.composite.from([ + Track.inheritFromOriginalRelease('contributorContribs'), + Thing.common.dynamicContribs('contributorContribsByRef'), + ]), // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: - Thing.common.dynamicInheritContribs( - 'hasCoverArt', - 'coverArtistContribsByRef', - 'trackCoverArtistContribsByRef', - 'albumData', - Track.findAlbum), - - referencedTracks: - Track.inheritFromOriginalRelease('referencedTracks', [], - Thing.common.dynamicThingsFromReferenceList( - 'referencedTracksByRef', - 'trackData', - find.track)), - - sampledTracks: - Track.inheritFromOriginalRelease('sampledTracks', [], - Thing.common.dynamicThingsFromReferenceList( - 'sampledTracksByRef', - 'trackData', - find.track)), + coverArtistContribs: Thing.composite.from([ + Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + + { + flags: {expose: true}, + expose: { + dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + + compute({ + coverArtistContribsByRef: contribsFromTrack, + disableUniqueCoverArt, + album: {trackCoverArtistContribsByRef: contribsFromAlbum}, + }) { + if (disableUniqueCoverArt) return null; + + let contribsByRef = contribsFromTrack; + if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; + if (empty(contribsByRef)) return null; + + return Thing.findArtistsFromContribs(contribsByRef, artistData); + }, + }, + }, + ]), + + referencedTracks: Thing.composite.from([ + Track.inheritFromOriginalRelease('referencedTracks'), + Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), + ]), + + sampledTracks: Thing.composite.from([ + Track.inheritFromOriginalRelease('sampledTracks'), + Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), + ]), // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't @@ -317,72 +351,44 @@ export class Track extends Thing { ), }); - static inheritFromOriginalRelease( - originalProperty, - originalMissingValue, - ownPropertyDescriptor - ) { - return { - flags: {expose: true}, + static inheritFromOriginalRelease = originalProperty => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: [ - ...ownPropertyDescriptor.expose.dependencies, - 'originalReleaseTrackByRef', - 'trackData', - ], - - compute(dependencies) { - const { - originalReleaseTrackByRef, - trackData, - } = dependencies; - - if (originalReleaseTrackByRef) { - if (!trackData) return originalMissingValue; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return originalMissingValue; - return original[originalProperty]; - } + expose: { + dependencies: ['originalReleaseTrackByRef', 'trackData'], - return ownPropertyDescriptor.expose.compute(dependencies); - }, - }, - }; - } + compute({originalReleaseTrackByRef, trackData}, callback) { + if (!originalReleaseTrackByRef) return callback(); - static withAlbumProperties(albumProperties, oldExpose) { - const applyAlbumDependency = dependencies => { - const track = dependencies[Track.instance]; - const album = - dependencies.albumData - ?.find((album) => album.tracks.includes(track)); - - const filteredAlbum = Object.create(null); - for (const property of albumProperties) { - filteredAlbum[property] = - (album - ? album[property] - : null); - } + if (!trackData) return null; + const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); + if (!original) return null; + return original[originalProperty]; + }, + }, + }); - return {...dependencies, album: filteredAlbum}; - }; + static withAlbumProperties = albumProperties => ({ + flags: {expose: true, compose: true}, - const newExpose = {dependencies: [...oldExpose.dependencies, 'albumData']}; + expose: { + dependencies: ['albumData'], - if (oldExpose.compute) { - newExpose.compute = dependencies => - oldExpose.compute(applyAlbumDependency(dependencies)); - } + compute({albumData, [Track.instance]: track}, callback) { + const album = albumData?.find((album) => album.tracks.includes(track)); - if (oldExpose.transform) { - newExpose.transform = (value, dependencies) => - oldExpose.transform(value, applyAlbumDependency(dependencies)); - } + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } - return newExpose; - } + return callback({album: filteredAlbum}); + }, + }, + }); [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); -- cgit 1.3.0-6-gf8a5 From 9e23a5a9eff30af0d7c8e356520dec791aebd38f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:15:42 -0300 Subject: content, data: be more guarded about track contribs arrays --- src/data/things/artist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 522ca5f9..43628b6b 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -156,7 +156,7 @@ export class Artist extends Thing { }) => thingData?.filter(thing => thing[contribsProperty] - .some(contrib => contrib.who === artist)) ?? [], + ?.some(contrib => contrib.who === artist)) ?? [], }, }); } -- cgit 1.3.0-6-gf8a5 From 128c47001a639d1569bdfadf783ccede22116350 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:17:02 -0300 Subject: data: fix compute() bugs in Thing.composite.from() --- src/data/things/thing.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 143c1515..111de550 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -420,6 +420,8 @@ export default class Thing extends CacheableObject { } static findArtistsFromContribs(contribsByRef, artistData) { + if (empty(contribsByRef)) return null; + return ( contribsByRef .map(({who, what}) => ({ @@ -518,7 +520,7 @@ export default class Thing extends CacheableObject { const result = (type === 'transform' ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { - valueSoFar = updatedValue; + valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) @@ -532,7 +534,11 @@ export default class Thing extends CacheableObject { } } - return base.expose.transform(valueSoFar, dependencies); + if (base.expose.transform) { + return base.expose.transform(valueSoFar, dependencies); + } else { + return base.expose.compute(dependencies); + } }; } else { expose.compute = (initialDependencies) => { @@ -540,7 +546,7 @@ export default class Thing extends CacheableObject { for (const {fn} of exposeFunctionOrder) { const result = - fn(valueSoFar, dependencies, providedDependencies => { + fn(dependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }); -- cgit 1.3.0-6-gf8a5 From d4af649bfdd546bd87b1a440bdba8152d010937e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:17:38 -0300 Subject: yaml: fix disableCoverArt -> disableUniqueCoverArt --- src/data/yaml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index 13412f17..25eda3c5 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -340,7 +340,7 @@ export const processTrackDocument = makeProcessDocument(T.Track, { dateFirstReleased: 'Date First Released', coverArtDate: 'Cover Art Date', coverArtFileExtension: 'Cover Art File Extension', - disableCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. + disableUniqueCoverArt: 'Has Cover Art', // This gets transformed to flip true/false. lyrics: 'Lyrics', commentary: 'Commentary', -- cgit 1.3.0-6-gf8a5 From 0fd10f2997db8ddec95e3caff94343eafdd9dda1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:18:23 -0300 Subject: data: track: more composite shenanigans --- src/data/things/thing.js | 22 ++++-- src/data/things/track.js | 186 ++++++++++++++++++++++++++--------------------- 2 files changed, 116 insertions(+), 92 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 111de550..5d14b296 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -250,14 +250,7 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefProperty], compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - contribsByRef && artistData - ? contribsByRef - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who) - : [], + Thing.findArtistsFromContribs(contribsByRef, artistData), }, }), @@ -563,5 +556,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + + withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + flags: {expose: true, compose: true}, + + expose: { + dependencies: ['artistData', contribsByRefProperty], + compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + callback({ + [dependencyName]: + Thing.findArtistsFromContribs(contribsByRef, artistData), + }), + }, + }), }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index fe6af205..8aa7ba26 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,6 +44,26 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), + color: Thing.composite.from([ + { + flags: {expose: true, compose: true}, + expose: { + transform: (color, {}, continuation) => + color ?? continuation(), + }, + }, + + Track.composite.withAlbumProperties(['color']), + + { + flags: {update: true, expose: true}, + update: {validate: isColor}, + expose: { + compute: ({album: {color}}) => color, + }, + }, + ]), + // Disables presenting the track as though it has its own unique artwork. // This flag should only be used in select circumstances, i.e. to override // an album's trackCoverArtists. This flag supercedes that property, as well @@ -55,7 +75,7 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), { flags: {update: true, expos: true}, @@ -81,7 +101,7 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), { flags: {update: true, expose: true}, @@ -147,31 +167,25 @@ export class Track extends Thing { find.album ), - date: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData', 'dateFirstReleased'], - compute: ({albumData, dateFirstReleased, [Track.instance]: track}) => - dateFirstReleased ?? Track.findAlbum(track, albumData)?.date ?? null, + date: Thing.composite.from([ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['dateFirstReleased'], + compute: ({dateFirstReleased}, continuation) => + dateFirstReleased ?? continuation(), + }, }, - }, - color: { - flags: {update: true, expose: true}, - - update: {validate: isColor}, + Track.composite.withAlbumProperties(['date']), - expose: { - dependencies: ['albumData'], - - transform: (color, {albumData, [Track.instance]: track}) => - color ?? - Track.findAlbum(track, albumData) - ?.trackSections.find(({tracks}) => tracks.includes(track)) - ?.color ?? null, + { + flags: {expose: true}, + expose: { + compute: ({album: {date}}) => date, + }, }, - }, + ]), // Whether or not the track has "unique" cover artwork - a cover which is // specifically associated with this track in particular, rather than with @@ -181,7 +195,7 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef']), { flags: {expose: true}, @@ -236,29 +250,27 @@ export class Track extends Thing { }, artistContribs: Thing.composite.from([ - Track.inheritFromOriginalRelease('artistContribs'), + Track.composite.inheritFromOriginalRelease('artistContribs'), + + Thing.composite.withDynamicContribs('artistContribsByRef', 'artistContribs'), + Track.composite.withAlbumProperties(['artistContribs']), { flags: {expose: true}, expose: { - dependencies: ['artistContribs'], - - compute({ - artistContribsByRef: contribsFromTrack, - album: {artistContribsByRef: contribsFromAlbum}, - }) { - let contribsByRef = contribsFromTrack; - if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; - if (empty(contribsByRef)) return null; - - return Thing.findArtistsFromContribs(contribsByRef, artistData); - }, + compute: ({ + artistContribs: contribsFromTrack, + album: {artistContribs: contribsFromAlbum}, + }) => + (empty(contribsFromTrack) + ? contribsFromAlbum + : contribsFromTrack), }, }, ]), contributorContribs: Thing.composite.from([ - Track.inheritFromOriginalRelease('contributorContribs'), + Track.composite.inheritFromOriginalRelease('contributorContribs'), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -266,37 +278,41 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: Thing.composite.from([ - Track.withAlbumProperties(['trackCoverArtistContribsByRef']), - { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], - - compute({ - coverArtistContribsByRef: contribsFromTrack, - disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef: contribsFromAlbum}, - }) { - if (disableUniqueCoverArt) return null; + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? null + : continuation()), + }, + }, - let contribsByRef = contribsFromTrack; - if (empty(contribsByRef)) contribsByRef = contribsFromAlbum; - if (empty(contribsByRef)) return null; + Track.composite.withAlbumProperties(['trackCoverArtistContribs']), + Thing.composite.withDynamicContribs('coverArtistContribsByRef', 'coverArtistContribs'), - return Thing.findArtistsFromContribs(contribsByRef, artistData); - }, + { + flags: {expose: true}, + expose: { + compute: ({ + coverArtistContribs: contribsFromTrack, + album: {trackCoverArtistContribs: contribsFromAlbum}, + }) => + (empty(contribsFromTrack) + ? contribsFromAlbum + : contribsFromTrack), }, }, ]), referencedTracks: Thing.composite.from([ - Track.inheritFromOriginalRelease('referencedTracks'), + Track.composite.inheritFromOriginalRelease('referencedTracks'), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), sampledTracks: Thing.composite.from([ - Track.inheritFromOriginalRelease('sampledTracks'), + Track.composite.inheritFromOriginalRelease('sampledTracks'), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), @@ -351,44 +367,46 @@ export class Track extends Thing { ), }); - static inheritFromOriginalRelease = originalProperty => ({ - flags: {expose: true, compose: true}, + static composite = { + inheritFromOriginalRelease: originalProperty => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + expose: { + dependencies: ['originalReleaseTrackByRef', 'trackData'], - compute({originalReleaseTrackByRef, trackData}, callback) { - if (!originalReleaseTrackByRef) return callback(); + compute({originalReleaseTrackByRef, trackData}, continuation) { + if (!originalReleaseTrackByRef) return continuation(); - if (!trackData) return null; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return null; - return original[originalProperty]; + if (!trackData) return null; + const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); + if (!original) return null; + return original[originalProperty]; + }, }, - }, - }); + }), - static withAlbumProperties = albumProperties => ({ - flags: {expose: true, compose: true}, + withAlbumProperties: albumProperties => ({ + flags: {expose: true, compose: true}, - expose: { - dependencies: ['albumData'], + expose: { + dependencies: ['albumData'], - compute({albumData, [Track.instance]: track}, callback) { - const album = albumData?.find((album) => album.tracks.includes(track)); + compute({albumData, [Track.instance]: track}, continuation) { + const album = albumData?.find((album) => album.tracks.includes(track)); - const filteredAlbum = Object.create(null); - for (const property of albumProperties) { - filteredAlbum[property] = - (album - ? album[property] - : null); - } + const filteredAlbum = Object.create(null); + for (const property of albumProperties) { + filteredAlbum[property] = + (album + ? album[property] + : null); + } - return callback({album: filteredAlbum}); + return continuation({album: filteredAlbum}); + }, }, - }, - }); + }), + }; [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); -- cgit 1.3.0-6-gf8a5 From 8f8361c7c45b02a2221c01acc492ba4d3ae1c42e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:50:25 -0300 Subject: data: 2x facepalm combobob --- src/data/things/track.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 8aa7ba26..30c6fe58 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -78,7 +78,7 @@ export class Track extends Thing { Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), { - flags: {update: true, expos: true}, + flags: {update: true, expose: true}, update: {validate: isFileExtension}, expose: { dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], @@ -207,7 +207,7 @@ export class Track extends Thing { album: {trackCoverArtistContribsByRef}, }) { if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) true; + if (!empty(coverArtistContribsByRef)) return true; if (!empty(trackCoverArtistContribsByRef)) return true; return false; }, -- cgit 1.3.0-6-gf8a5 From 93448ef747b681d3b87b050b555311c0172b83cc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 22:12:24 -0300 Subject: content, data: be even more guarded about contrib arrays --- src/data/things/artist.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 43628b6b..4f157bc6 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -71,9 +71,9 @@ export class Artist extends Thing { compute: ({trackData, [Artist.instance]: artist}) => trackData?.filter((track) => [ - ...track.artistContribs, - ...track.contributorContribs, - ...track.coverArtistContribs, + ...track.artistContribs ?? [], + ...track.contributorContribs ?? [], + ...track.coverArtistContribs ?? [], ].some(({who}) => who === artist)) ?? [], }, }, -- cgit 1.3.0-6-gf8a5 From 75691866ed68b9261dd920b79d4ab214df3f049b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 13:02:19 -0300 Subject: data: filter only requested deps, require requesting 'this' * Thing.composite.from() only provides the dependencies specified in each step and the base, and prevents '#'-prefixed keys from being specified on the main (composite) dependency list. * CacheableObject no longer provides a "reflection" dependency to every compute/transform function, and now requires the property 'this' to be specified instead of the constructor.instance symbol. (The static CacheableObject.instance, inherited by all subclasses, was also removed.) * Also minor improvements to sugar.js data processing utility functions. --- src/data/things/art-tag.js | 4 +- src/data/things/artist.js | 16 +++---- src/data/things/cacheable-object.js | 33 ++++++++------ src/data/things/flash.js | 8 ++-- src/data/things/group.js | 13 +++--- src/data/things/thing.js | 48 +++++++++++++++------ src/data/things/track.js | 85 ++++++++++++++++++++++++------------- 7 files changed, 129 insertions(+), 78 deletions(-) (limited to 'src/data') diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index c103c4d5..bb36e09e 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -37,8 +37,8 @@ export class ArtTag extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData', 'trackData'], - compute: ({albumData, trackData, [ArtTag.instance]: artTag}) => + dependencies: ['this', 'albumData', 'trackData'], + compute: ({this: artTag, albumData, trackData}) => sortAlbumsTracksChronologically( [...albumData, ...trackData] .filter(({artTags}) => artTags.includes(artTag)), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 4f157bc6..bde84cfa 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -66,9 +66,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Artist.instance]: artist}) => + compute: ({this: artist, trackData}) => trackData?.filter((track) => [ ...track.artistContribs ?? [], @@ -82,9 +82,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Artist.instance]: artist}) => + compute: ({this: artist, trackData}) => trackData?.filter(({commentatorArtists}) => commentatorArtists.includes(artist)) ?? [], }, @@ -103,9 +103,9 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], + dependencies: [this, 'albumData'], - compute: ({albumData, [Artist.instance]: artist}) => + compute: ({this: artist, albumData}) => albumData?.filter(({commentatorArtists}) => commentatorArtists.includes(artist)) ?? [], }, @@ -148,11 +148,11 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], compute: ({ + this: artist, [thingDataProperty]: thingData, - [Artist.instance]: artist }) => thingData?.filter(thing => thing[contribsProperty] diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index ea705a61..24a6cf01 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -83,8 +83,6 @@ function inspect(value) { } export default class CacheableObject { - static instance = Symbol('CacheableObject `this` instance'); - #propertyUpdateValues = Object.create(null); #propertyUpdateCacheInvalidators = Object.create(null); @@ -250,20 +248,27 @@ export default class CacheableObject { let getAllDependencies; - const dependencyKeys = expose.dependencies; - if (dependencyKeys?.length > 0) { - const reflectionEntry = [this.constructor.instance, this]; - const dependencyGetters = dependencyKeys - .map(key => () => [key, this.#propertyUpdateValues[key]]); + if (expose.dependencies?.length > 0) { + const dependencyKeys = expose.dependencies.slice(); + const shouldReflect = dependencyKeys.includes('this'); + + getAllDependencies = () => { + const dependencies = Object.create(null); + + for (const key of dependencyKeys) { + dependencies[key] = this.#propertyUpdateValues[key]; + } - getAllDependencies = () => - Object.fromEntries(dependencyGetters - .map(f => f()) - .concat([reflectionEntry])); + if (shouldReflect) { + dependencies.this = this; + } + + return dependencies; + }; } else { - const allDependencies = {[this.constructor.instance]: this}; - Object.freeze(allDependencies); - getAllDependencies = () => allDependencies; + const dependencies = Object.create(null); + Object.freeze(dependencies); + getAllDependencies = () => dependencies; } if (flags.update) { diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 6eb5234f..445fd07c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -77,9 +77,9 @@ export class Flash extends Thing { flags: {expose: true}, expose: { - dependencies: ['flashActData'], + dependencies: ['this', 'flashActData'], - compute: ({flashActData, [Flash.instance]: flash}) => + compute: ({this: flash, flashActData}) => flashActData.find((act) => act.flashes.includes(flash)) ?? null, }, }, @@ -88,9 +88,9 @@ export class Flash extends Thing { flags: {expose: true}, expose: { - dependencies: ['flashActData'], + dependencies: ['this', 'flashActData'], - compute: ({flashActData, [Flash.instance]: flash}) => + compute: ({this: flash, flashActData}) => flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, }, }, diff --git a/src/data/things/group.js b/src/data/things/group.js index ba339b3e..f552b8f3 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -41,8 +41,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], - compute: ({albumData, [Group.instance]: group}) => + dependencies: ['this', 'albumData'], + compute: ({this: group, albumData}) => albumData?.filter((album) => album.groups.includes(group)) ?? [], }, }, @@ -51,9 +51,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['groupCategoryData'], - - compute: ({groupCategoryData, [Group.instance]: group}) => + dependencies: ['this', 'groupCategoryData'], + compute: ({this: group, groupCategoryData}) => groupCategoryData.find((category) => category.groups.includes(group)) ?.color, }, @@ -63,8 +62,8 @@ export class Group extends Thing { flags: {expose: true}, expose: { - dependencies: ['groupCategoryData'], - compute: ({groupCategoryData, [Group.instance]: group}) => + dependencies: ['this', 'groupCategoryData'], + compute: ({this: group, groupCategoryData}) => groupCategoryData.find((category) => category.groups.includes(group)) ?? null, }, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d14b296..bc10e06b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, openAggregate} from '#sugar'; +import {empty, filterProperties, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -278,6 +278,7 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { dependencies: [ + 'this', contribsByRefProperty, thingDataProperty, nullerProperty, @@ -285,7 +286,7 @@ export default class Thing extends CacheableObject { ].filter(Boolean), compute({ - [Thing.instance]: thing, + this: thing, [nullerProperty]: nuller, [contribsByRefProperty]: contribsByRef, [thingDataProperty]: thingData, @@ -330,9 +331,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], }, }), @@ -344,9 +345,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], }, }), @@ -462,15 +463,19 @@ export default class Thing extends CacheableObject { if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; exposeDependencies.add(dependency); } } + let fn, type; if (base.flags.update) { if (step.expose.transform) { - exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + type = 'transform'; + fn = step.expose.transform; } else { - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } } else { if (step.expose.transform && !step.expose.compute) { @@ -478,8 +483,15 @@ export default class Thing extends CacheableObject { break expose; } - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } + + exposeFunctionOrder.push({ + type, + fn, + ownDependencies: step.expose.dependencies, + }); } }); } @@ -509,15 +521,20 @@ export default class Thing extends CacheableObject { const dependencies = {...initialDependencies}; let valueSoFar = value; - for (const {type, fn} of exposeFunctionOrder) { + for (const {type, fn, ownDependencies} of exposeFunctionOrder) { + const filteredDependencies = + (ownDependencies + ? filterProperties(dependencies, ownDependencies) + : {}) + const result = (type === 'transform' - ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) - : fn(dependencies, providedDependencies => { + : fn(filteredDependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; })); @@ -527,10 +544,13 @@ export default class Thing extends CacheableObject { } } + const filteredDependencies = + filterProperties(dependencies, base.expose.dependencies); + if (base.expose.transform) { - return base.expose.transform(valueSoFar, dependencies); + return base.expose.transform(valueSoFar, filteredDependencies); } else { - return base.expose.compute(dependencies); + return base.expose.compute(filteredDependencies); } }; } else { diff --git a/src/data/things/track.js b/src/data/things/track.js index 30c6fe58..551d9345 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,8 @@ export class Track extends Thing { flags: {update: true, expose: true}, update: {validate: isColor}, expose: { - compute: ({album: {color}}) => color, + dependencies: ['#album.color'], + compute: ({'#album.color': color}) => color, }, }, ]), @@ -75,18 +76,27 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']), + Track.composite.withAlbumProperties([ + 'trackCoverArtistContribsByRef', + 'trackCoverArtFileExtension', + ]), { flags: {update: true, expose: true}, update: {validate: isFileExtension}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackCoverArtistContribsByRef', + '#album.trackCoverArtFileExtension', + ], transform(coverArtFileExtension, { coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension}, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, + '#album.trackCoverArtFileExtension': trackCoverArtFileExtension, }) { if (disableUniqueCoverArt) return null; if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; @@ -101,18 +111,27 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties([ + 'trackArtDate', + 'trackCoverArtistContribsByRef', + ]), { flags: {update: true, expose: true}, update: {validate: isDate}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackArtDate', + '#album.trackCoverArtistContribsByRef', + ], transform(coverArtDate, { coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackArtDate, trackCoverArtistContribsByRef}, + '#album.trackArtDate': trackArtDate, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, }) { if (disableUniqueCoverArt) return null; if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; @@ -148,8 +167,8 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['albumData'], - compute: ({[Track.instance]: track, albumData}) => + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}) => albumData?.find((album) => album.tracks.includes(track)) ?? null, }, }, @@ -182,7 +201,8 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - compute: ({album: {date}}) => date, + dependencies: ['#album.date'], + compute: ({'#album.date': date}) => date, }, }, ]), @@ -200,11 +220,16 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'], + dependencies: [ + 'coverArtistContribsByRef', + 'disableUniqueCoverArt', + '#album.trackCoverArtistContribsByRef', + ], + compute({ coverArtistContribsByRef, disableUniqueCoverArt, - album: {trackCoverArtistContribsByRef}, + '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, }) { if (disableUniqueCoverArt) return false; if (!empty(coverArtistContribsByRef)) return true; @@ -225,12 +250,12 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + dependencies: ['this', 'originalReleaseTrackByRef', 'trackData'], compute: ({ + this: t1, originalReleaseTrackByRef: t1origRef, trackData, - [Track.instance]: t1, }) => { if (!trackData) { return []; @@ -252,15 +277,16 @@ export class Track extends Thing { artistContribs: Thing.composite.from([ Track.composite.inheritFromOriginalRelease('artistContribs'), - Thing.composite.withDynamicContribs('artistContribsByRef', 'artistContribs'), + Thing.composite.withDynamicContribs('artistContribsByRef', '#artistContribs'), Track.composite.withAlbumProperties(['artistContribs']), { flags: {expose: true}, expose: { + dependencies: ['#artistContribs', '#album.artistContribs'], compute: ({ - artistContribs: contribsFromTrack, - album: {artistContribs: contribsFromAlbum}, + '#artistContribs': contribsFromTrack, + '#album.artistContribs': contribsFromAlbum, }) => (empty(contribsFromTrack) ? contribsFromAlbum @@ -290,14 +316,15 @@ export class Track extends Thing { }, Track.composite.withAlbumProperties(['trackCoverArtistContribs']), - Thing.composite.withDynamicContribs('coverArtistContribsByRef', 'coverArtistContribs'), + Thing.composite.withDynamicContribs('coverArtistContribsByRef', '#coverArtistContribs'), { flags: {expose: true}, expose: { + dependencies: ['#coverArtistContribs', '#album.trackCoverArtistContribs'], compute: ({ - coverArtistContribs: contribsFromTrack, - album: {trackCoverArtistContribs: contribsFromAlbum}, + '#coverArtistContribs': contribsFromTrack, + '#album.trackCoverArtistContribs': contribsFromAlbum, }) => (empty(contribsFromTrack) ? contribsFromAlbum @@ -328,9 +355,9 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Track.instance]: track}) => + compute: ({this: track, trackData}) => trackData ? trackData .filter((t) => !t.originalReleaseTrack) @@ -344,9 +371,9 @@ export class Track extends Thing { flags: {expose: true}, expose: { - dependencies: ['trackData'], + dependencies: ['this', 'trackData'], - compute: ({trackData, [Track.instance]: track}) => + compute: ({this: track, trackData}) => trackData ? trackData .filter((t) => !t.originalReleaseTrack) @@ -389,20 +416,20 @@ export class Track extends Thing { flags: {expose: true, compose: true}, expose: { - dependencies: ['albumData'], + dependencies: ['this', 'albumData'], - compute({albumData, [Track.instance]: track}, continuation) { + compute({this: track, albumData}, continuation) { const album = albumData?.find((album) => album.tracks.includes(track)); + const newDependencies = {}; - const filteredAlbum = Object.create(null); for (const property of albumProperties) { - filteredAlbum[property] = + newDependencies['#album.' + property] = (album ? album[property] : null); } - return continuation({album: filteredAlbum}); + return continuation(newDependencies); }, }, }), -- cgit 1.3.0-6-gf8a5 From 1481db921e645ab09aad3a57b4ce308e2c57d738 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 13:52:43 -0300 Subject: data: signature changes to misc compositional functions --- src/data/things/thing.js | 12 +++++--- src/data/things/track.js | 75 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 24 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index bc10e06b..f1ae6c71 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -577,14 +577,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, - withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + // 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. + withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + dependencies: ['artistData', contribsByRefDependency], + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => callback({ - [dependencyName]: + [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 551d9345..985de594 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -53,7 +53,9 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['color']), + Track.composite.withAlbumProperties({ + properties: ['color'], + }), { flags: {update: true, expose: true}, @@ -76,10 +78,12 @@ export class Track extends Thing { // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) coverArtFileExtension: Thing.composite.from([ - Track.composite.withAlbumProperties([ - 'trackCoverArtistContribsByRef', - 'trackCoverArtFileExtension', - ]), + Track.composite.withAlbumProperties({ + properties: [ + 'trackCoverArtistContribsByRef', + 'trackCoverArtFileExtension', + ], + }), { flags: {update: true, expose: true}, @@ -111,10 +115,12 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from([ - Track.composite.withAlbumProperties([ - 'trackArtDate', - 'trackCoverArtistContribsByRef', - ]), + Track.composite.withAlbumProperties({ + properties: [ + 'trackArtDate', + 'trackCoverArtistContribsByRef', + ], + }), { flags: {update: true, expose: true}, @@ -196,7 +202,9 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['date']), + Track.composite.withAlbumProperties({ + properties: ['date'], + }), { flags: {expose: true}, @@ -215,7 +223,9 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from([ - Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef']), + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribsByRef'], + }), { flags: {expose: true}, @@ -277,8 +287,14 @@ export class Track extends Thing { artistContribs: Thing.composite.from([ Track.composite.inheritFromOriginalRelease('artistContribs'), - Thing.composite.withDynamicContribs('artistContribsByRef', '#artistContribs'), - Track.composite.withAlbumProperties(['artistContribs']), + Track.composite.withAlbumProperties({ + properties: 'artistContribs', + }), + + Thing.composite.withResolvedContribs({ + from: 'artistContribsByRef', + to: '#artistContribs', + }), { flags: {expose: true}, @@ -315,8 +331,14 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties(['trackCoverArtistContribs']), - Thing.composite.withDynamicContribs('coverArtistContribsByRef', '#coverArtistContribs'), + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribs'], + }), + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), { flags: {expose: true}, @@ -395,7 +417,12 @@ export class Track extends Thing { }); static composite = { - inheritFromOriginalRelease: originalProperty => ({ + // Returns a value inherited from the original release, if this track + // is a rerelease, and otherwise continues with no further provided + // dependencies. If the second argument is provided true, then the + // continuation will also be called if the original release exposed + // the requested property as null. + inheritFromOriginalRelease: (originalProperty, allowOverride = false) => ({ flags: {expose: true, compose: true}, expose: { @@ -407,12 +434,20 @@ export class Track extends Thing { if (!trackData) return null; const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); if (!original) return null; - return original[originalProperty]; + + const value = original[originalProperty]; + if (allowOverride && value === null) return continuation(); + + return value; }, }, }), - withAlbumProperties: albumProperties => ({ + // Gets the listed properties from this track's album, providing them as + // dependencies (by default) with '#album.' prefixed before each property + // name. If the track's album isn't available, the same dependency names + // will each be provided as null. + withAlbumProperties: ({properties, prefix = '#album'}) => ({ flags: {expose: true, compose: true}, expose: { @@ -422,8 +457,8 @@ export class Track extends Thing { const album = albumData?.find((album) => album.tracks.includes(track)); const newDependencies = {}; - for (const property of albumProperties) { - newDependencies['#album.' + property] = + for (const property of properties) { + newDependencies[prefix + '.' + property] = (album ? album[property] : null); -- cgit 1.3.0-6-gf8a5 From 6483809c6d9c67f1311a64f2572b4fe5881d3a0d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 22:36:20 -0300 Subject: data: composition docs, annotations, nesting --- src/data/things/thing.js | 316 +++++++++++++++++++++++++++++++++++++++++++++-- src/data/things/track.js | 81 +++++++----- 2 files changed, 359 insertions(+), 38 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1ae6c71..1186c389 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -426,14 +426,222 @@ export default class Thing extends CacheableObject { } static composite = { - from(composition) { + // Composes multiple compositional "steps" and a "base" to form a property + // descriptor out of modular building blocks. This is an extension to the + // more general-purpose CacheableObject property descriptor syntax, and + // aims to make modular data processing - which lends to declarativity - + // much easier, without fundamentally altering much of the typical syntax + // or terminology, nor building on it to an excessive degree. + // + // Think of a composition as being a chain of steps which lead into a final + // base property, which is usually responsible for returning the value that + // will actually get exposed when the property being described is accessed. + // + // == The compositional base: == + // + // The final item in a compositional list is its base, and it identifies + // the essential qualities of the property descriptor. The compositional + // steps preceding it may exit early, in which case the expose function + // defined on the base won't be called; or they will provide dependencies + // that the base may use to compute the final value that gets exposed for + // this property. + // + // The base indicates the capabilities of the composition as a whole. + // It should be {expose: true}, since that's the only area that preceding + // compositional steps (currently) can actually influence. If it's also + // {update: true}, then the composition as a whole accepts an update value + // just like normal update-flag property descriptors - meaning it can be + // set with `thing.someProperty = value` and that value will be paseed + // into each (implementing) step's transform() function, as well as the + // base. Bases usually aren't {compose: true}, but can be - check out the + // section on "nesting compositions" for details about that. + // + // Every composition always has exactly one compositional base, and it's + // always the last item in the composition list. All items preceding it + // are compositional steps, described below. + // + // == Compositional steps: == + // + // Compositional steps are, in essence, typical property descriptors with + // the extra flag {compose: true}. They operate on existing dependencies, + // and are typically dynamically constructed by "utility" functions (but + // can also be manually declared within the step list of a composition). + // Compositional steps serve two purposes: + // + // 1. exit early, if some condition is matched, returning and exposing + // some value directly from that step instead of continuing further + // down the step list; + // + // 2. and/or provide new, dynamically created "private" dependencies which + // can be accessed by further steps down the list, or at the base at + // the bottom, modularly supplying information that will contribute to + // the final value exposed for this property. + // + // Usually it's just one of those two, but it's fine for a step to perform + // both jobs if the situation benefits. + // + // Compositional steps are the real "modular" or "compositional" part of + // this data processing style - they're designed to be combined together + // in dynamic, versatile ways, as each property demands it. You usually + // define a compositional step to be returned by some ordinary static + // property-descriptor-returning function (customarily namespaced under + // the relevant Thing class's static `composite` field) - that lets you + // reuse it in multiple compositions later on. + // + // Compositional steps are implemented with "continuation passing style", + // meaning the connection to the next link on the chain is passed right to + // each step's compute (or transform) function, and the implementation gets + // to decide whether to continue on that chain or exit early by returning + // some other value. + // + // Every step along the chain, apart from the base at the bottom, has to + // have the {compose: true} step. That means its compute() or transform() + // function will be passed an extra argument at the end, `continuation`. + // To provide new dependencies to items further down the chain, just pass + // them directly to this continuation() function, customarily with a hash + // ('#') prefixing each name - for example: + // + // compute({..some dependencies..}, continuation) { + // return continuation({ + // '#excitingProperty': (..a value made from dependencies..), + // }); + // } + // + // Performing an early exit is as simple as returning some other value, + // instead of the continuation. + // + // It may be fine to simply provide new dependencies under a hard-coded + // name, such as '#excitingProperty' above, but if you're writing a utility + // that dynamically returns the compositional step and you suspect you + // might want to use this step multiple times in a single composition, + // it's customary to accept a name for the result. + // + // Here's a detailed example showing off early exit, dynamically operating + // on a provided dependency name, and then providing a result in another + // also-provided dependency name: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: [contribsByRefDependency, 'artistData'], + // compute({ + // [contribsByRefDependency]: contribsByRef, + // artistData, + // }, continuation) { + // if (!artistData) return null; /* early exit! */ + // return continuation({ + // [outputDependency]: /* this is the important part */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // And how you might work that into a composition: + // + // static Track[Thing.getPropertyDescriptors].coverArtists = + // Thing.composite.from([ + // Track.composite.doSomethingWhichMightEarlyExit(), + // Thing.composite.withResolvedContribs({ + // from: 'coverArtistContribsByRef', + // to: '#coverArtistContribs', + // }), + // + // { + // flags: {expose: true}, + // expose: { + // dependencies: ['#coverArtistContribs'], + // compute({'#coverArtistContribs': coverArtistContribs}) { + // return coverArtistContribs.map(({who}) => who); + // }, + // }, + // }, + // ]); + // + // == To compute or to transform: == + // + // A compositional step can work directly on a property's stored update + // value, transforming it in place and either early exiting with it or + // passing it on (via continuation) to the next item(s) in the + // compositional step list. (If needed, these can provide dependencies + // the same way as compute functions too - just pass that object after + // the updated (or same) transform value in your call to continuation().) + // + // But in order to make them more versatile, compositional steps have an + // extra trick up their sleeve. If a compositional step implements compute + // and *not* transform, it can still be used in a composition targeting a + // property which updates! These retain their full dependency-providing and + // early exit functionality - they just won't be provided the update value. + // If a compute-implementing step returns its continuation, then whichever + // later step (or the base) next implements transform() will receive the + // update value that had so far been running - as well as any dependencies + // the compute() step returned, of course! + // + // Please note that a compositional step which transforms *should not* + // specify, in its flags, {update: true}. Just provide the transform() + // function in its expose descriptor; it will be automatically detected + // and used when appropriate. + // + // It's actually possible for a step to specify both transform and compute, + // in which case the transform() implementation will only be selected if + // the composition's base is {update: true}. It's not exactly known why you + // would want to specify unique-but-related transform and compute behavior, + // but the basic possibility was too cool to skip out on. + // + // == Nesting compositions: == + // + // Compositional steps are so convenient that you just might want to bundle + // them together, and form a whole new step-shaped unit of its own! + // + // In order to allow for this while helping to ensure internal dependencies + // remain neatly isolated from the composition which nests your bundle, + // the Thing.composite.from() function will accept and adapt to a base that + // specifies the {compose: true} flag, just like the steps preceding it. + // + // The continuation function that gets provided to the base will be mildly + // special - after all, nothing follows the base within the composition's + // own list! Instead of appending dependencies alongside any previously + // provided ones to be available to the next step, the base's continuation + // function should be used to define "exports" of the composition as a + // whole. It's similar to the usual behavior of the continuation, just + // expanded to the scope of the composition instead of following steps. + // + // For example, suppose your composition (which you expect to include in + // other compositions) brings about several internal, hash-prefixed + // dependencies to contribute to its own results. Those dependencies won't + // end up "bleeding" into the dependency list of whichever composition is + // nesting this one - they will totally disappear once all the steps in + // the nested composition have finished up. + // + // To "export" the results of processing all those dependencies (provided + // that's something you want to do and this composition isn't used purely + // for a conditional early-exit), you'll want to define them in the + // continuation passed to the base. (Customarily, those should start with + // a hash just like the exports from any other compositional step; they're + // still dynamically provided dependencies!) + // + from(firstArg, secondArg) { + let annotation, composition; + if (typeof firstArg === 'string') { + [annotation, composition] = [firstArg, secondArg]; + } else { + [annotation, composition] = [null, firstArg]; + } + const base = composition.at(-1); const steps = composition.slice(0, -1); - const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + const aggregate = openAggregate({ + message: + `Errors preparing Thing.composite.from() composition` + + (annotation ? ` (${annotation})` : ''), + }); - if (base.flags.compose) { - aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + if (base.flags.compose && base.flags.compute) { + push(new TypeError(`Base which composes can't also update yet`)); } const exposeFunctionOrder = []; @@ -500,14 +708,18 @@ export default class Thing extends CacheableObject { const constructedDescriptor = {}; + if (annotation) { + constructedDescriptor.annotation = annotation; + } + constructedDescriptor.flags = { update: !!base.flags.update, expose: !!base.flags.expose, - compose: false, + compose: !!base.flags.compose, }; if (base.flags.update) { - constructedDescriptor.update = base.flags.update; + constructedDescriptor.update = base.update; } if (base.flags.expose) { @@ -547,6 +759,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = filterProperties(dependencies, base.expose.dependencies); + // Note: base.flags.compose is not compatible with base.flags.update, + // so the base.flags.compose case is not handled here. + if (base.expose.transform) { return base.expose.transform(valueSoFar, filteredDependencies); } else { @@ -554,7 +769,7 @@ export default class Thing extends CacheableObject { } }; } else { - expose.compute = (initialDependencies) => { + expose.compute = (initialDependencies, continuationIfApplicable) => { const dependencies = {...initialDependencies}; for (const {fn} of exposeFunctionOrder) { @@ -569,7 +784,23 @@ export default class Thing extends CacheableObject { } } - return base.expose.compute(dependencies); + if (base.flags.compose) { + let exportDependencies; + + const result = + base.expose.compute(dependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + + return exportDependencies; + } else { + return base.expose.compute(dependencies); + } }; } } @@ -577,11 +808,48 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Provides dependencies exactly as they are (or null if not defined) to the + // continuation. Although this can *technically* be used to alias existing + // dependencies to some other name within the middle of a composition, it's + // intended to be used only as a composition's base - doing so makes the + // composition as a whole suitable as a step in some other composition, + // providing the listed (internal) dependencies to later steps just like + // other compositional steps. + export(mapping) { + const mappingEntries = Object.entries(mapping); + + return { + annotation: `Thing.composite.export`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: Object.values(mapping), + + compute(dependencies, continuation) { + const exports = {}; + + // Note: This is slightly different behavior from filterProperties, + // as defined in sugar.js, which doesn't fall back to null for + // properties which don't exist on the original object. + for (const [exportKey, dependencyKey] of mappingEntries) { + exports[exportKey] = + (Object.hasOwn(dependencies, dependencyKey) + ? dependencies[dependencyKey] + : null); + } + + return continuation(exports); + } + }, + }; + }, + // 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. withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { @@ -593,5 +861,37 @@ export default class Thing extends CacheableObject { }), }, }), + + // Resolves a reference by using the provided find function to match it + // within the provided thingData dependency. This will early exit if the + // data dependency is null, or, if earlyExitIfNotFound is set to true, + // if the find function doesn't match anything for the reference. + // Otherwise, the data object (or null, if not found) is provided on + // the output dependency. + withResolvedReference({ + ref: refDependency, + data: dataDependency, + to: outputDependency, + find: findFunction, + earlyExitIfNotFound = false, + }) { + return { + annotation: `Thing.composite.withResolvedReference`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: [refDependency, dataDependency], + + compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (data === null) return null; + + const match = findFunction(ref, data, {mode: 'quiet'}); + if (match === null && earlyExitIfNotFound) return null; + + return continuation({[outputDependency]: match}); + }, + }, + }; + } }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 985de594..718eb07e 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -44,7 +44,7 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - color: Thing.composite.from([ + color: Thing.composite.from(`Track.color`, [ { flags: {expose: true, compose: true}, expose: { @@ -77,7 +77,7 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the cover's // main artwork. (It does inherit `trackCoverArtFileExtension` if present // on the album.) - coverArtFileExtension: Thing.composite.from([ + coverArtFileExtension: Thing.composite.from(`Track.coverArtFileExtension`, [ Track.composite.withAlbumProperties({ properties: [ 'trackCoverArtistContribsByRef', @@ -114,7 +114,7 @@ export class Track extends Thing { // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: Thing.composite.from([ + coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ Track.composite.withAlbumProperties({ properties: [ 'trackArtDate', @@ -192,7 +192,7 @@ export class Track extends Thing { find.album ), - date: Thing.composite.from([ + date: Thing.composite.from(`Track.date`, [ { flags: {expose: true, compose: true}, expose: { @@ -222,7 +222,7 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: Thing.composite.from([ + hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ Track.composite.withAlbumProperties({ properties: ['trackCoverArtistContribsByRef'], }), @@ -284,7 +284,7 @@ export class Track extends Thing { }, }, - artistContribs: Thing.composite.from([ + artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease('artistContribs'), Track.composite.withAlbumProperties({ @@ -311,7 +311,7 @@ export class Track extends Thing { }, ]), - contributorContribs: Thing.composite.from([ + contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ Track.composite.inheritFromOriginalRelease('contributorContribs'), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -319,7 +319,7 @@ export class Track extends Thing { // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: Thing.composite.from([ + coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ { flags: {expose: true, compose: true}, expose: { @@ -355,12 +355,12 @@ export class Track extends Thing { }, ]), - referencedTracks: Thing.composite.from([ + referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ Track.composite.inheritFromOriginalRelease('referencedTracks'), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), - sampledTracks: Thing.composite.from([ + sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ Track.composite.inheritFromOriginalRelease('sampledTracks'), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), @@ -417,37 +417,39 @@ export class Track extends Thing { }); static composite = { - // Returns a value inherited from the original release, if this track - // is a rerelease, and otherwise continues with no further provided - // dependencies. If the second argument is provided true, then the - // continuation will also be called if the original release exposed - // the requested property as null. - inheritFromOriginalRelease: (originalProperty, allowOverride = false) => ({ - flags: {expose: true, compose: true}, + // Early exits with a value inherited from the original release, if + // this track is a rerelease, and otherwise continues with no further + // dependencies provided. If allowOverride is true, then the continuation + // will also be called if the original release exposed the requested + // property as null. + inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => + Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ + Track.composite.withOriginalRelease({to: '#originalRelease'}), - expose: { - dependencies: ['originalReleaseTrackByRef', 'trackData'], + { + flags: {expose: true, compose: true}, - compute({originalReleaseTrackByRef, trackData}, continuation) { - if (!originalReleaseTrackByRef) return continuation(); + expose: { + dependencies: ['#originalRelease'], - if (!trackData) return null; - const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); - if (!original) return null; + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation(); - const value = original[originalProperty]; - if (allowOverride && value === null) return continuation(); + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation(); - return value; - }, - }, - }), + return value; + }, + }, + } + ]), // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, the same dependency names // will each be provided as null. withAlbumProperties: ({properties, prefix = '#album'}) => ({ + annotation: `Track.composite.withAlbumProperties`, flags: {expose: true, compose: true}, expose: { @@ -468,6 +470,25 @@ export class Track extends Thing { }, }, }), + + // Just includes the original release of this track as a dependency, or + // null, if it's not a rerelease. Note that this will early exit if the + // original release is specified by reference and that reference doesn't + // resolve to anything. + withOriginalRelease: ({to: outputDependency = '#originalRelease'}) => + Thing.composite.from(`Track.composite.withOriginalRelease`, [ + Thing.composite.withResolvedReference({ + ref: 'originalReleaseTrackByRef', + data: 'trackData', + to: '#originalRelease', + find: find.track, + earlyExitIfNotFound: true, + }), + + Thing.composite.export({ + [outputDependency]: '#originalRelease', + }), + ]), }; [inspect.custom]() { -- cgit 1.3.0-6-gf8a5 From c7e624684069fb9325c426500a5fcc153cf26b41 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 22:39:22 -0300 Subject: data: track: remove unneeded explicit {to} on withOriginalRelease call --- src/data/things/track.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 718eb07e..118e3db0 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -424,7 +424,7 @@ export class Track extends Thing { // property as null. inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ - Track.composite.withOriginalRelease({to: '#originalRelease'}), + Track.composite.withOriginalRelease(), { flags: {expose: true, compose: true}, @@ -474,8 +474,8 @@ export class Track extends Thing { // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't - // resolve to anything. - withOriginalRelease: ({to: outputDependency = '#originalRelease'}) => + // resolve to anything. Outputs to '#originalRelease' by default. + withOriginalRelease: ({to: outputDependency = '#originalRelease'} = {}) => Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ ref: 'originalReleaseTrackByRef', -- cgit 1.3.0-6-gf8a5 From 94f684138329e17ab43bfe552056d7ea3ed28b17 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 12:22:11 -0300 Subject: data: track.hasUniqueCoverArt: operate on resolved contributions --- src/data/things/track.js | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 118e3db0..414d5f29 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -223,29 +223,45 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? false + : continuation()), + }, + }, + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#coverArtistContribs'], + compute: ({'#coverArtistContribs': coverArtistContribs}, continuation) => + (empty(coverArtistContribs) + ? continuation() + : true), + }, + }, + Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribsByRef'], + properties: ['trackCoverArtistContribs'], }), { flags: {expose: true}, expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackCoverArtistContribsByRef', - ], - - compute({ - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - }) { - if (disableUniqueCoverArt) return false; - if (!empty(coverArtistContribsByRef)) return true; - if (!empty(trackCoverArtistContribsByRef)) return true; - return false; - }, + dependencies: ['#album.trackCoverArtistContribs'], + compute: ({'#album.trackCoverArtistContribs': trackCoverArtistContribs}) => + (empty(trackCoverArtistContribs) + ? false + : true), }, }, ]), -- cgit 1.3.0-6-gf8a5 From 13914b9f07f60d6d8aaaddc7df675d41950320c3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 12:22:34 -0300 Subject: test: Track.{color,date,hasUniqueCoverArt} (unit) --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1186c389..decde6f4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -419,7 +419,7 @@ export default class Thing extends CacheableObject { return ( contribsByRef .map(({who, what}) => ({ - who: find.artist(who, artistData), + who: find.artist(who, artistData, {mode: 'quiet'}), what, })) .filter(({who}) => who)); -- cgit 1.3.0-6-gf8a5 From 0f4e27426384536c179583a8ffaf3dd9f121766b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:00:35 -0300 Subject: data: Thing.composite.from: fix not calling export continuation --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index decde6f4..c1f969b2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -797,7 +797,7 @@ export default class Thing extends CacheableObject { return result; } - return exportDependencies; + return continuationIfApplicable(exportDependencies); } else { return base.expose.compute(dependencies); } -- cgit 1.3.0-6-gf8a5 From 8dd100d04fdd13b4ab8348d61378de5fd74f72d4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:01:05 -0300 Subject: data: Thing.composite.withResolvedReference: fix null refs The `earlyExitIfNotFound` flag is only supposed to exit if the reference really existed and failed to match anything. If it was null in the first place, withResolvedReferences should always just pass null ahead. --- src/data/things/thing.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c1f969b2..798a057a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -866,8 +866,9 @@ export default class Thing extends CacheableObject { // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if earlyExitIfNotFound is set to true, // if the find function doesn't match anything for the reference. - // Otherwise, the data object (or null, if not found) is provided on - // the output dependency. + // Otherwise, the data object is provided on the output dependency; + // or null, if the reference doesn't match anything or itself was null + // to begin with. withResolvedReference({ ref: refDependency, data: dataDependency, @@ -883,6 +884,8 @@ export default class Thing extends CacheableObject { dependencies: [refDependency, dataDependency], compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (!ref) return continuation({[outputDependency]: null}); + if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); -- cgit 1.3.0-6-gf8a5 From fab4b46d13795bcc82c8b4dd6b5a39ef23c42430 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:02:44 -0300 Subject: data: fix more bad function signatures --- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 798a057a..eaf4655d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -854,8 +854,8 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => - callback({ + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + continuation({ [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), diff --git a/src/data/things/track.js b/src/data/things/track.js index 414d5f29..74f5d7fb 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -301,12 +301,9 @@ export class Track extends Thing { }, artistContribs: Thing.composite.from(`Track.artistContribs`, [ - Track.composite.inheritFromOriginalRelease('artistContribs'), - - Track.composite.withAlbumProperties({ - properties: 'artistContribs', - }), + Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), + Track.composite.withAlbumProperties({properties: ['artistContribs']}), Thing.composite.withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', @@ -328,7 +325,7 @@ export class Track extends Thing { ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ - Track.composite.inheritFromOriginalRelease('contributorContribs'), + Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}), Thing.common.dynamicContribs('contributorContribsByRef'), ]), @@ -372,12 +369,12 @@ export class Track extends Thing { ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ - Track.composite.inheritFromOriginalRelease('referencedTracks'), + Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), ]), sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ - Track.composite.inheritFromOriginalRelease('sampledTracks'), + Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), -- cgit 1.3.0-6-gf8a5 From 2b2bbe9083d6f205e6b04b08c8bc4339a6a9ed87 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 18:47:09 -0300 Subject: data: Thing.composite.from: mapDependencies/mapContinuation --- src/data/things/thing.js | 162 ++++++++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 71 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index eaf4655d..a9fd220f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -644,7 +644,7 @@ export default class Thing extends CacheableObject { push(new TypeError(`Base which composes can't also update yet`)); } - const exposeFunctionOrder = []; + const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); for (let i = 0; i < steps.length; i++) { @@ -695,11 +695,7 @@ export default class Thing extends CacheableObject { fn = step.expose.compute; } - exposeFunctionOrder.push({ - type, - fn, - ownDependencies: step.expose.dependencies, - }); + exposeSteps.push(step.expose); } }); } @@ -727,81 +723,104 @@ export default class Thing extends CacheableObject { expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol(); + const noTransformSymbol = Symbol(); - if (base.flags.update) { - expose.transform = (value, initialDependencies) => { - const dependencies = {...initialDependencies}; - let valueSoFar = value; - - for (const {type, fn, ownDependencies} of exposeFunctionOrder) { - const filteredDependencies = - (ownDependencies - ? filterProperties(dependencies, ownDependencies) - : {}) - - const result = - (type === 'transform' - ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { + const _filterDependencies = (dependencies, step) => { + const filteredDependencies = + (step.dependencies + ? filterProperties(dependencies, step.dependencies) + : {}); + + if (step.mapDependencies) { + for (const [to, from] of Object.entries(step.mapDependencies)) { + filteredDependencies[to] = dependencies[from] ?? null; + } + } + + return filteredDependencies; + }; + + const _assignDependencies = (continuationAssignment, step) => { + if (!step.mapContinuation) { + return continuationAssignment; + } + + const assignDependencies = {}; + + for (const [from, to] of Object.entries(step.mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } + + return assignDependencies; + }; + + const _computeOrTransform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + + let valueSoFar = value; + + for (const step of exposeSteps) { + const filteredDependencies = _filterDependencies(dependencies, step); + + let assignDependencies = null; + + const result = + (valueSoFar !== noTransformSymbol && step.transform + ? step.transform( + valueSoFar, filteredDependencies, + (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; - Object.assign(dependencies, providedDependencies ?? {}); + assignDependencies = providedDependencies; return continuationSymbol; }) - : fn(filteredDependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); + : step.compute( + filteredDependencies, + (providedDependencies) => { + assignDependencies = providedDependencies; return continuationSymbol; })); - if (result !== continuationSymbol) { - return result; - } + if (result !== continuationSymbol) { + return result; } - const filteredDependencies = - filterProperties(dependencies, base.expose.dependencies); + Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + } - // Note: base.flags.compose is not compatible with base.flags.update, - // so the base.flags.compose case is not handled here. + const filteredDependencies = _filterDependencies(dependencies, base.expose); - if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); - } else { - return base.expose.compute(filteredDependencies); - } - }; - } else { - expose.compute = (initialDependencies, continuationIfApplicable) => { - const dependencies = {...initialDependencies}; - - for (const {fn} of exposeFunctionOrder) { - const result = - fn(dependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); - return continuationSymbol; - }); - - if (result !== continuationSymbol) { - return result; - } - } + // Note: base.flags.compose is not compatible with base.flags.update. + if (base.expose.transform) { + return base.expose.transform(valueSoFar, filteredDependencies); + } else if (base.flags.compose) { + const continuation = continuationIfApplicable; - if (base.flags.compose) { - let exportDependencies; + let exportDependencies; - const result = - base.expose.compute(dependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = + base.expose.compute(filteredDependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); - if (result !== continuationSymbol) { - return result; - } - - return continuationIfApplicable(exportDependencies); - } else { - return base.expose.compute(dependencies); + if (result !== continuationSymbol) { + return result; } - }; + + return continuation(_assignDependencies(exportDependencies, base.expose)); + } else { + return base.expose.compute(filteredDependencies); + } + }; + + if (base.flags.update) { + expose.transform = + (value, initialDependencies) => + _computeOrTransform(value, initialDependencies); + } else { + expose.compute = + (initialDependencies) => + _computeOrTransform(undefined, initialDependencies); } } @@ -848,16 +867,17 @@ export default class Thing extends CacheableObject { // 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. - withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + withResolvedContribs: ({from, to}) => ({ annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => continuation({ - [outputDependency]: - Thing.findArtistsFromContribs(contribsByRef, artistData), + to: Thing.findArtistsFromContribs(from, artistData), }), }, }), -- cgit 1.3.0-6-gf8a5 From 57edc116016f45f1bc9e7e3e6560450b6c480602 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 18:49:11 -0300 Subject: data: fix not passing noTransformSymbol --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a9fd220f..d553a3ec 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -820,7 +820,7 @@ export default class Thing extends CacheableObject { } else { expose.compute = (initialDependencies) => - _computeOrTransform(undefined, initialDependencies); + _computeOrTransform(noTransformSymbol, initialDependencies); } } -- cgit 1.3.0-6-gf8a5 From 287de65cea2fb72833eb2fe596f7e61c61939481 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 19:00:14 -0300 Subject: data: Track.coverArtistContribs: lazier steps --- src/data/things/track.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 74f5d7fb..2a3148ef 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -344,28 +344,36 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - Thing.composite.withResolvedContribs({ from: 'coverArtistContribsByRef', to: '#coverArtistContribs', }), { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['#coverArtistContribs', '#album.trackCoverArtistContribs'], - compute: ({ - '#coverArtistContribs': contribsFromTrack, - '#album.trackCoverArtistContribs': contribsFromAlbum, - }) => + mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, + compute: ({contribsFromTrack}, continuation) => (empty(contribsFromTrack) - ? contribsFromAlbum + ? continuation() : contribsFromTrack), }, }, + + Track.composite.withAlbumProperties({ + properties: ['trackCoverArtistContribs'], + }), + + { + flags: {expose: true}, + expose: { + mapDependencies: {contribsFromAlbum: '#album.trackCoverArtistContribs'}, + compute: ({contribsFromAlbum}) => + (empty(contribsFromAlbum) + ? null + : contribsFromAlbum), + }, + }, ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ -- cgit 1.3.0-6-gf8a5 From eb869dd1b786a4180647e5b8b1b6f20aefb6c004 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 19:43:28 -0300 Subject: data: Track.compposite.from: 'options', cache-safe documentation --- src/data/things/thing.js | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d553a3ec..1bca6c38 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -561,6 +561,97 @@ export default class Thing extends CacheableObject { // }, // ]); // + // == Cache-safe dependency names: == + // + // [Disclosure: The caching engine hasn't actually been implemented yet. + // As such, this section is subject to change, and simply provides sound + // forward-facing advice and interfaces.] + // + // It's a good idea to write individual compositional steps in such a way + // that they're "cache-safe" - meaning the same input (dependency) values + // will always result in the same output (continuation or early exit). + // + // In order to facilitate this, compositional step descriptors may specify + // unique `mapDependencies`, `mapContinuation`, and `options` values. + // + // Consider the `withResolvedContribs` example adjusted to make use of + // two of these options below: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {contribsByRef: contribsByRefDependency}, + // mapContinuation: {outputDependency}, + // compute({ + // contribsByRef, /* no longer in square brackets */ + // artistData, + // }, continuation) { + // if (!artistData) return null; + // return continuation({ + // outputDependency: /* no longer in square brackets */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // With a little destructuring and restructuring JavaScript sugar, the + // above can be simplified some more: + // + // static Thing.composite.withResolvedContribs = ({from, to}) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {from}, + // mapContinuation: {to}, + // compute({artistData, from: contribsByRef}, continuation) { + // if (!artistData) return null; + // return continuation({ + // to: (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // These two properties let you separate the name-mapping behavior (for + // dependencies and the continuation) from the main body of the compute + // function. That means the compute function will *always* get inputs in + // the same form (dependencies 'artistData' and 'from' above), and will + // *always* provide its output in the same form (early return or 'to'). + // + // Thanks to that, this `compute` function is cache-safe! Its outputs can + // be cached corresponding to each set of mapped inputs. So it won't matter + // whether the `from` dependency is named `coverArtistContribsByRef` or + // `contributorContribsByRef` or something else - the compute function + // doesn't care, and only expects that value to be provided via its `from` + // argument. Likewise, it doesn't matter if the output should be sent to + // '#coverArtistContribs` or `#contributorContribs` or some other name; + // the mapping is handled automatically outside, and compute will always + // output its value to the continuation's `to`. + // + // Note that `mapDependencies` and `mapContinuation` should be objects of + // the same "shape" each run - that is, the values will change depending on + // outside context, but the keys are always the same. You shouldn't use + // `mapDependencies` to dynamically select more or fewer dependencies. + // If you need to dynamically select a range of dependencies, just specify + // them in the `dependencies` array like usual. The caching engine will + // understand that differently named `dependencies` indicate separate + // input-output caches should be used. + // + // The 'options' property makes it possible to specify external arguments + // that fundamentally change the behavior of the `compute` function, while + // still remaining cache-safe. It indicates that the caching engine should + // use a completely different input-to-output cache for each permutation + // of the 'options' values. This way, those functions are still cacheable + // at all; they'll just be cached separately for each set of option values. + // Values on the 'options' property will always be provided in compute's + // dependencies under '#options' (to avoid name conflicts with other + // dependencies). + // // == To compute or to transform: == // // A compositional step can work directly on a property's stored update @@ -725,7 +816,7 @@ export default class Thing extends CacheableObject { const continuationSymbol = Symbol(); const noTransformSymbol = Symbol(); - const _filterDependencies = (dependencies, step) => { + function _filterDependencies(dependencies, step) { const filteredDependencies = (step.dependencies ? filterProperties(dependencies, step.dependencies) @@ -737,10 +828,14 @@ export default class Thing extends CacheableObject { } } + if (step.options) { + filteredDependencies['#options'] = step.options; + } + return filteredDependencies; - }; + } - const _assignDependencies = (continuationAssignment, step) => { + function _assignDependencies(continuationAssignment, step) { if (!step.mapContinuation) { return continuationAssignment; } @@ -752,9 +847,9 @@ export default class Thing extends CacheableObject { } return assignDependencies; - }; + } - const _computeOrTransform = (value, initialDependencies) => { + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; let valueSoFar = value; @@ -811,7 +906,7 @@ export default class Thing extends CacheableObject { } else { return base.expose.compute(filteredDependencies); } - }; + } if (base.flags.update) { expose.transform = @@ -842,9 +937,10 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { + options: {mappingEntries}, dependencies: Object.values(mapping), - compute(dependencies, continuation) { + compute({'#options': {mappingEntries}, ...dependencies}, continuation) { const exports = {}; // Note: This is slightly different behavior from filterProperties, @@ -890,9 +986,9 @@ export default class Thing extends CacheableObject { // or null, if the reference doesn't match anything or itself was null // to begin with. withResolvedReference({ - ref: refDependency, - data: dataDependency, - to: outputDependency, + ref, + data, + to, find: findFunction, earlyExitIfNotFound = false, }) { @@ -901,17 +997,19 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { - dependencies: [refDependency, dataDependency], + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {to}, - compute({[refDependency]: ref, [dataDependency]: data}, continuation) { - if (!ref) return continuation({[outputDependency]: null}); + compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { + if (!ref) return continuation({to: null}); if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && earlyExitIfNotFound) return null; - return continuation({[outputDependency]: match}); + return continuation({to: match}); }, }, }; -- cgit 1.3.0-6-gf8a5 From ba1cf3fe611661c85ef4a7150c924f99e1e94ba3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 21:10:46 -0300 Subject: data: bug fixes & Thing.composite.from.debug mode --- src/data/things/thing.js | 272 +++++++++++++++++++++++++++++++++++++++-------- src/data/things/track.js | 3 +- 2 files changed, 229 insertions(+), 46 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1bca6c38..555e443d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -508,7 +508,9 @@ export default class Thing extends CacheableObject { // } // // Performing an early exit is as simple as returning some other value, - // instead of the continuation. + // instead of the continuation. You may also use `continuation.exit(value)` + // to perform the exact same kind of early exit - it's just a different + // syntax that might fit in better in certain longer compositions. // // It may be fine to simply provide new dependencies under a hard-coded // name, such as '#excitingProperty' above, but if you're writing a utility @@ -715,6 +717,17 @@ export default class Thing extends CacheableObject { // still dynamically provided dependencies!) // from(firstArg, secondArg) { + const debug = fn => { + if (Thing.composite.from.debug === true) { + const result = fn(); + if (Array.isArray(result)) { + console.log(`[composite]`, ...result); + } else { + console.log(`[composite]`, result); + } + } + }; + let annotation, composition; if (typeof firstArg === 'string') { [annotation, composition] = [firstArg, secondArg]; @@ -738,6 +751,13 @@ export default class Thing extends CacheableObject { const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); + if (base.expose?.mapDependencies) { + for (const dependency of Object.values(base.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const message = @@ -767,6 +787,13 @@ export default class Thing extends CacheableObject { } } + if (step.expose.mapDependencies) { + for (const dependency of Object.values(step.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + let fn, type; if (base.flags.update) { if (step.expose.transform) { @@ -813,8 +840,8 @@ export default class Thing extends CacheableObject { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); - const continuationSymbol = Symbol(); - const noTransformSymbol = Symbol(); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); function _filterDependencies(dependencies, step) { const filteredDependencies = @@ -849,48 +876,153 @@ export default class Thing extends CacheableObject { return assignDependencies; } + function _prepareContinuation(transform, step) { + const continuationStorage = { + returnedWith: null, + providedDependencies: null, + providedValue: null, + }; + + const continuation = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (base.flags.compose) { + continuation.raise = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + } + + return {continuation, continuationStorage}; + } + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; - let valueSoFar = value; + let valueSoFar = value; // Set only for {update: true} compositions + let exportDependencies = null; // Set only for {compose: true} compositions + + debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + + for (let i = 0; i < exposeSteps.length; i++) { + const step = exposeSteps[i]; + debug(() => [`step #${i+1}:`, step]); + + const transform = + valueSoFar !== noTransformSymbol && + step.transform; - for (const step of exposeSteps) { const filteredDependencies = _filterDependencies(dependencies, step); + const {continuation, continuationStorage} = _prepareContinuation(transform, step); - let assignDependencies = null; + if (transform) { + debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } else { + debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } const result = - (valueSoFar !== noTransformSymbol && step.transform - ? step.transform( - valueSoFar, filteredDependencies, - (updatedValue, providedDependencies) => { - valueSoFar = updatedValue ?? null; - assignDependencies = providedDependencies; - return continuationSymbol; - }) - : step.compute( - filteredDependencies, - (providedDependencies) => { - assignDependencies = providedDependencies; - return continuationSymbol; - })); + (transform + ? step.transform(valueSoFar, filteredDependencies, continuation) + : step.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { + if (base.flags.compose) { + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + } + + debug(() => `step #${i+1} - early-exit (inferred)`); + debug(() => `early-exit: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return result; } - Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + if (continuationStorage.returnedWith === 'exit') { + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationSymbol.providedValue; + } + + if (continuationStorage.returnedWith === 'raise') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + + debug(() => `step #${i+1} - result: raise`); + + break; + } + + if (continuationStorage.returnedWith === 'continuation') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + debug(() => `step #${i+1} - result: continuation`); + + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + + debug(() => [`assign dependencies:`, assignDependencies]); + } + } } + if (exportDependencies) { + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); + } + + debug(() => `completed all steps, reached base`); + const filteredDependencies = _filterDependencies(dependencies, base.expose); // Note: base.flags.compose is not compatible with base.flags.update. if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); + debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.transform(valueSoFar, filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + + return result; } else if (base.flags.compose) { - const continuation = continuationIfApplicable; + const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); - let exportDependencies; + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = base.expose.compute(filteredDependencies, providedDependencies => { @@ -899,12 +1031,39 @@ export default class Thing extends CacheableObject { }); if (result !== continuationSymbol) { - return result; + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'continuation') { + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'exit') { + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationStorage.providedValue; } - return continuation(_assignDependencies(exportDependencies, base.expose)); + if (continuationStorage.returnedWith === 'raise') { + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); + + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationIfApplicable(exportDependencies); + } } else { - return base.expose.compute(filteredDependencies); + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.compute(filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return result; } } @@ -953,7 +1112,7 @@ export default class Thing extends CacheableObject { : null); } - return continuation(exports); + return continuation.raise(exports); } }, }; @@ -985,34 +1144,57 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference({ + withResolvedReference: ({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) { - return { - annotation: `Thing.composite.withResolvedReference`, - flags: {expose: true, compose: true}, + }) => + Thing.composite.from(`Thing.composite.withResolvedReference`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {ref}, + mapContinuation: {to}, + + compute: ({ref}, continuation) => + (ref + ? continuation() + : continuation.raise({to: null})), + }, + }, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {to}, + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {data}, + + compute: ({data}, continuation) => + (data === null + ? continuation.exit(null) + : continuation()), + }, + }, - compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { - if (!ref) return continuation({to: null}); + { + flags: {expose: true, compose: true}, + expose: { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - if (data === null) return null; + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) return null; + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation({to: match}); + return continuation({match}); + }, }, }, - }; - } + ]), }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 2a3148ef..23b6da56 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -475,8 +475,9 @@ export class Track extends Thing { expose: { dependencies: ['this', 'albumData'], + options: {properties, prefix}, - compute({this: track, albumData}, continuation) { + compute({this: track, albumData, '#options': {properties, prefix}}, continuation) { const album = albumData?.find((album) => album.tracks.includes(track)); const newDependencies = {}; -- cgit 1.3.0-6-gf8a5 From 2a87cbaf06e364585bae2b48919c46b6d1f7aa1f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 13:28:29 -0300 Subject: data: Thing.composite.from bugfixes --- src/data/things/thing.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 555e443d..16dd786d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -922,7 +922,7 @@ export default class Thing extends CacheableObject { return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies) { + function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { const dependencies = {...initialDependencies}; let valueSoFar = value; // Set only for {update: true} compositions @@ -1020,15 +1020,11 @@ export default class Thing extends CacheableObject { return result; } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); + const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - const result = - base.expose.compute(filteredDependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = base.expose.compute(filteredDependencies, continuation); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); @@ -1069,12 +1065,12 @@ export default class Thing extends CacheableObject { if (base.flags.update) { expose.transform = - (value, initialDependencies) => - _computeOrTransform(value, initialDependencies); + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); } else { expose.compute = - (initialDependencies) => - _computeOrTransform(noTransformSymbol, initialDependencies); + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); } } -- cgit 1.3.0-6-gf8a5 From fe9bd87d1e6b71c3019b38ca2f99e0c21d916186 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 13:28:56 -0300 Subject: data: use continuation.exit and continuation.raise where needed --- src/data/things/thing.js | 2 +- src/data/things/track.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16dd786d..578a5a4e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1187,7 +1187,7 @@ export default class Thing extends CacheableObject { return continuation.exit(null); } - return continuation({match}); + return continuation.raise({match}); }, }, }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 23b6da56..5c3a1d46 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -454,12 +454,12 @@ export class Track extends Thing { dependencies: ['#originalRelease'], compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation(); + if (!originalRelease) return continuation.raise(); const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation(); + if (allowOverride && value === null) return continuation.raise(); - return value; + return continuation.exit(value); }, }, } -- cgit 1.3.0-6-gf8a5 From c7d8ab3286854faa2ccf54e84b8efbff43a416fc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 13:29:38 -0300 Subject: data: Track.artistContribs: be lazy, like coverArtistContribs --- src/data/things/track.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 5c3a1d46..016f5199 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -303,25 +303,34 @@ export class Track extends Thing { artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), - Track.composite.withAlbumProperties({properties: ['artistContribs']}), Thing.composite.withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', }), { - flags: {expose: true}, + flags: {expose: true, compose: true}, expose: { - dependencies: ['#artistContribs', '#album.artistContribs'], - compute: ({ - '#artistContribs': contribsFromTrack, - '#album.artistContribs': contribsFromAlbum, - }) => + mapDependencies: {contribsFromTrack: '#artistContribs'}, + compute: ({contribsFromTrack}, continuation) => (empty(contribsFromTrack) - ? contribsFromAlbum + ? continuation() : contribsFromTrack), }, }, + + Track.composite.withAlbumProperties({properties: ['artistContribs']}), + + { + flags: {expose: true}, + expose: { + mapDependencies: {contribsFromAlbum: '#album.artistContribs'}, + compute: ({contribsFromAlbum}) => + (empty(contribsFromAlbum) + ? null + : contribsFromAlbum), + }, + }, ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ -- cgit 1.3.0-6-gf8a5 From f562896d4d67558a32726f7086beebf29019a44d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 14:06:00 -0300 Subject: yaml, test: mutate/decache wikiData in more reusable ways --- src/data/yaml.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index 25eda3c5..2ad2d41d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1320,13 +1320,27 @@ export async function loadAndProcessDataDocuments({dataPath}) { // Data linking! Basically, provide (portions of) wikiData to the Things which // require it - they'll expose dynamically computed properties as a result (many -// of which are required for page HTML generation). -export function linkWikiDataArrays(wikiData) { +// of which are required for page HTML generation and other expected behavior). +// +// The XXX_decacheWikiData option should be used specifically to mark +// points where you *aren't* replacing any of the arrays under wikiData with +// new values, and are using linkWikiDataArrays to instead "decache" data +// properties which depend on any of them. It's currently not possible for +// a CacheableObject to depend directly on the value of a property exposed +// on some other CacheableObject, so when those values change, you have to +// manually decache before the object will realize its cache isn't valid +// anymore. +export function linkWikiDataArrays(wikiData, { + XXX_decacheWikiData = false, +} = {}) { function assignWikiData(things, ...keys) { + if (things === undefined) return; for (let i = 0; i < things.length; i++) { const thing = things[i]; for (let j = 0; j < keys.length; j++) { const key = keys[j]; + if (!(key in wikiData)) continue; + if (XXX_decacheWikiData) thing[key] = []; thing[key] = wikiData[key]; } } @@ -1344,7 +1358,7 @@ export function linkWikiDataArrays(wikiData) { assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData'); assignWikiData(WD.flashActData, 'flashData'); assignWikiData(WD.artTagData, 'albumData', 'trackData'); - assignWikiData(WD.homepageLayout.rows, 'albumData', 'groupData'); + assignWikiData(WD.homepageLayout?.rows, 'albumData', 'groupData'); } export function sortWikiDataArrays(wikiData) { -- cgit 1.3.0-6-gf8a5 From 809ae313afdb6c7bb859a94170f6fd2c6c888591 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 18:28:19 -0300 Subject: data: Track.composite.withAlbum --- src/data/things/track.js | 83 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 16 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 016f5199..6c08aa01 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -474,34 +474,85 @@ export class Track extends Thing { } ]), - // Gets the listed properties from this track's album, providing them as - // dependencies (by default) with '#album.' prefixed before each property - // name. If the track's album isn't available, the same dependency names - // will each be provided as null. - withAlbumProperties: ({properties, prefix = '#album'}) => ({ - annotation: `Track.composite.withAlbumProperties`, + // Gets the track's album. Unless earlyExitIfNotFound is overridden false, + // this will early-exit with null in two cases - albumData being missing, + // or not including an album whose .tracks array includes this track. + withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ + annotation: `Track.composite.withAlbum`, flags: {expose: true, compose: true}, expose: { dependencies: ['this', 'albumData'], - options: {properties, prefix}, + mapContinuation: {to}, + options: {earlyExitIfNotFound}, + + compute({ + this: track, + albumData, + '#options': {earlyExitIfNotFound}, + }, continuation) { + if (empty(albumData)) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } - compute({this: track, albumData, '#options': {properties, prefix}}, continuation) { - const album = albumData?.find((album) => album.tracks.includes(track)); - const newDependencies = {}; + const album = + albumData?.find(album => album.tracks.includes(track)); - for (const property of properties) { - newDependencies[prefix + '.' + property] = - (album - ? album[property] - : null); + if (!album) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); } - return continuation(newDependencies); + return continuation({to: album}); }, }, }), + // Gets the listed properties from this track's album, providing them as + // dependencies (by default) with '#album.' prefixed before each property + // name. If the track's album isn't available, and earlyExitIfNotFound + // hasn't been set, the same dependency names will be provided as null. + withAlbumProperties: ({ + properties, + prefix = '#album', + earlyExitIfNotFound = false, + }) => + Thing.composite.from(`Track.composite.withAlbumProperties`, [ + Track.composite.withAlbum({earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; + } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } + + return continuation.raise(raise); + }, + }, + }, + ]), + // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't -- cgit 1.3.0-6-gf8a5 From e2f1cd30f8d5804f97043faedc5aea9fe06cea32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 18:46:14 -0300 Subject: data: Thing.composite.from: fix undefined return for explicit exit --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 578a5a4e..c870b89c 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -969,7 +969,7 @@ export default class Thing extends CacheableObject { debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); - return continuationSymbol.providedValue; + return continuationStorage.providedValue; } if (continuationStorage.returnedWith === 'raise') { -- cgit 1.3.0-6-gf8a5 From 25beb8731d756bfa4fe6babb9e4b0a707c7823e0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 19:22:38 -0300 Subject: data, test: misc. additions * Thing.composite.expose * Thing.composite.exposeUpdateValueOrContinue * Track.composite.withAlbumProperty * refactor: Track.color, Track.album, Track.date * refactor: Track.coverArtistContribs * test: Track.album (unit) --- src/data/things/thing.js | 51 ++++++++++++++++++++++++++ src/data/things/track.js | 95 +++++++++++++++++++++--------------------------- 2 files changed, 92 insertions(+), 54 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c870b89c..2af06904 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1114,6 +1114,57 @@ export default class Thing extends CacheableObject { }; }, + // Exposes a dependency exactly as it is; this is typically the base of a + // composition which was created to serve as one property's descriptor. + // Since this serves as a base, specify {update: true} to indicate that + // the property as a whole updates (and some previous compositional step + // works with that update value). + // + // Please note that this *doesn't* verify that the dependency exists, so + // if you provide the wrong name or it hasn't been set by a previous + // compositional step, the property will be exposed as undefined instead + // of null. + // + expose: (dependency, {update = false} = {}) => ({ + annotation: `Thing.composite.expose`, + flags: {expose: true, update}, + + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, + }), + + // Exposes the update value of an {update: true} property, or continues if + // it's unavailable. By default, "unavailable" means value === null, but + // set {mode: 'empty'} to + exposeUpdateValueOrContinue({mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeUpdateValueOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + + transform(value, {'#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(value) + : value === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(value); + } + } + }, + }; + }, + // 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 diff --git a/src/data/things/track.js b/src/data/things/track.js index 6c08aa01..15a48bb4 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -45,26 +45,9 @@ export class Track extends Thing { artTagsByRef: Thing.common.referenceList(ArtTag), color: Thing.composite.from(`Track.color`, [ - { - flags: {expose: true, compose: true}, - expose: { - transform: (color, {}, continuation) => - color ?? continuation(), - }, - }, - - Track.composite.withAlbumProperties({ - properties: ['color'], - }), - - { - flags: {update: true, expose: true}, - update: {validate: isColor}, - expose: { - dependencies: ['#album.color'], - compute: ({'#album.color': color}) => color, - }, - }, + Thing.composite.exposeUpdateValueOrContinue(), + Track.composite.withAlbumProperty('color'), + Thing.composite.expose('#album.color', {update: true}), ]), // Disables presenting the track as though it has its own unique artwork. @@ -169,15 +152,11 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}) => - albumData?.find((album) => album.tracks.includes(track)) ?? null, - }, - }, + album: + Thing.composite.from(`Track.album`, [ + Track.composite.withAlbum(), + Thing.composite.expose('#album'), + ]), // Note - this is an internal property used only to help identify a track. // It should not be assumed in general that the album and dataSourceAlbum match @@ -202,17 +181,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['date'], - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#album.date'], - compute: ({'#album.date': date}) => date, - }, - }, + Track.composite.withAlbumProperties({properties: ['date']}), + Thing.composite.expose('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -369,20 +339,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - - { - flags: {expose: true}, - expose: { - mapDependencies: {contribsFromAlbum: '#album.trackCoverArtistContribs'}, - compute: ({contribsFromAlbum}) => - (empty(contribsFromAlbum) - ? null - : contribsFromAlbum), - }, - }, + Track.composite.withAlbumProperty('trackCoverArtistContribs'), + Thing.composite.expose('#album.trackCoverArtistContribs'), ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ @@ -513,6 +471,35 @@ export class Track extends Thing { }, }), + // Gets a single property from this track's album, providing it as the same + // property name prefixed with '#album.' (by default). If the track's album + // isn't available, and earlyExitIfNotFound hasn't been set, the property + // will be provided as null. + withAlbumProperty: (property, { + to = '#album.' + property, + earlyExitIfNotFound = false, + } = {}) => + Thing.composite.from(`Track.composite.withAlbumProperty`, [ + Track.composite.withAlbum({earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), + }, + }, + ]), + // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, and earlyExitIfNotFound -- cgit 1.3.0-6-gf8a5 From 12b8040b05e81a523ef59ba583dde751206f2e1d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 20:38:27 -0300 Subject: data, test: retain validator for Track.color --- src/data/things/thing.js | 20 ++++++++++++++------ src/data/things/track.js | 4 +++- 2 files changed, 17 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2af06904..2adba5c4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1116,9 +1116,11 @@ export default class Thing extends CacheableObject { // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify {update: true} to indicate that - // the property as a whole updates (and some previous compositional step - // works with that update value). + // Since this serves as a base, specify a value for {update} to indicate + // that the property as a whole updates (and some previous compositional + // step works with that update value). Set {update: true} to only enable + // the update flag, or set update to an object to specify a descriptor + // (e.g. for custom value validation). // // Please note that this *doesn't* verify that the dependency exists, so // if you provide the wrong name or it hasn't been set by a previous @@ -1127,17 +1129,23 @@ export default class Thing extends CacheableObject { // expose: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, - flags: {expose: true, update}, + flags: {expose: true, update: !!update}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, + + update: + (typeof update === 'object' + ? update + : null), }), // Exposes the update value of an {update: true} property, or continues if // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to + // set {mode: 'empty'} to check with empty() instead, continuing for empty + // arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1156,7 +1164,7 @@ export default class Thing extends CacheableObject { : value === null); if (shouldContinue) { - return continuation(); + return continuation(value); } else { return continuation.exit(value); } diff --git a/src/data/things/track.js b/src/data/things/track.js index 15a48bb4..8d0a7ad4 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -47,7 +47,9 @@ export class Track extends Thing { color: Thing.composite.from(`Track.color`, [ Thing.composite.exposeUpdateValueOrContinue(), Track.composite.withAlbumProperty('color'), - Thing.composite.expose('#album.color', {update: true}), + Thing.composite.expose('#album.color', { + update: {validate: isColor}, + }), ]), // Disables presenting the track as though it has its own unique artwork. -- cgit 1.3.0-6-gf8a5 From e6038d8c07971447f444cf597328ca8d9863f8fd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 21:18:43 -0300 Subject: data, test: Track.color inherits from track section --- src/data/things/thing.js | 2 +- src/data/things/track.js | 69 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2adba5c4..f5dc786e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1127,7 +1127,7 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - expose: (dependency, {update = false} = {}) => ({ + exposeDependency: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, flags: {expose: true, update: !!update}, diff --git a/src/data/things/track.js b/src/data/things/track.js index 8d0a7ad4..621044d5 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -46,8 +46,25 @@ export class Track extends Thing { color: Thing.composite.from(`Track.color`, [ Thing.composite.exposeUpdateValueOrContinue(), + Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#trackSection'], + compute: ({'#trackSection': trackSection}, continuation) => + // Album.trackSections guarantees the track section will have a + // color property (inheriting from the album's own color), but only + // if it's actually present! Color will be inherited directly from + // album otherwise. + (trackSection + ? trackSection.color + : continuation()), + }, + }, + Track.composite.withAlbumProperty('color'), - Thing.composite.expose('#album.color', { + Thing.composite.exposeDependency('#album.color', { update: {validate: isColor}, }), ]), @@ -157,7 +174,7 @@ export class Track extends Thing { album: Thing.composite.from(`Track.album`, [ Track.composite.withAlbum(), - Thing.composite.expose('#album'), + Thing.composite.exposeDependency('#album'), ]), // Note - this is an internal property used only to help identify a track. @@ -183,8 +200,8 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({properties: ['date']}), - Thing.composite.expose('#album.date'), + Track.composite.withAlbumProperty('date'), + Thing.composite.exposeDependency('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -342,7 +359,7 @@ export class Track extends Thing { }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), - Thing.composite.expose('#album.trackCoverArtistContribs'), + Thing.composite.exposeDependency('#album.trackCoverArtistContribs'), ]), referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ @@ -435,7 +452,7 @@ export class Track extends Thing { ]), // Gets the track's album. Unless earlyExitIfNotFound is overridden false, - // this will early-exit with null in two cases - albumData being missing, + // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ annotation: `Track.composite.withAlbum`, @@ -542,6 +559,46 @@ export class Track extends Thing { }, ]), + // Gets the track section containing this track from its album's track list. + // Unless earlyExitIfNotFound is overridden false, this will early exit if + // the album can't be found or if none of its trackSections includes the + // track for some reason. + withContainingTrackSection: ({ + to = '#trackSection', + earlyExitIfNotFound = true, + } = {}) => + Thing.composite.from(`Track.composite.withContainingTrackSection`, [ + Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } + }, + }, + }, + ]), + // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't -- cgit 1.3.0-6-gf8a5 From 618f49e0ddcea245a4e0972efe5450419b27c639 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 21:31:07 -0300 Subject: data: Thing.composite.exposeDependencyOrContinue --- src/data/things/thing.js | 48 +++++++++++++++++++++++++++++++++++++++++++----- src/data/things/track.js | 10 +--------- 2 files changed, 44 insertions(+), 14 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f5dc786e..f88e8726 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1077,6 +1077,8 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // -- Compositional steps for compositions to nest -- + // Provides dependencies exactly as they are (or null if not defined) to the // continuation. Although this can *technically* be used to alias existing // dependencies to some other name within the middle of a composition, it's @@ -1114,6 +1116,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for top-level property descriptors -- + // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate @@ -1128,7 +1132,7 @@ export default class Thing extends CacheableObject { // of null. // exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.expose`, + annotation: `Thing.composite.exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -1142,10 +1146,42 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes the update value of an {update: true} property, or continues if - // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to check with empty() instead, continuing for empty - // arrays also. + // Exposes a dependency as it is, or continues if it's unavailable. + // By default, "unavailable" means dependency === null; provide + // {mode: 'empty'} to check with empty() instead, continuing for + // empty arrays also. + exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeDependencyOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + mapDependencies: {dependency}, + + compute({dependency, '#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(dependency) + : dependency === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(dependency); + } + }, + }, + }; + }, + + // Exposes the update value of an {update: true} property as it is, + // or continues if it's unavailable. By default, "unavailable" means + // value === null; provide {mode: 'empty'} to check with empty() instead, + // continuing for empty arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1173,6 +1209,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for processing data -- + // 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 diff --git a/src/data/things/track.js b/src/data/things/track.js index 621044d5..228b2af1 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -191,15 +191,7 @@ export class Track extends Thing { ), date: Thing.composite.from(`Track.date`, [ - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['dateFirstReleased'], - compute: ({dateFirstReleased}, continuation) => - dateFirstReleased ?? continuation(), - }, - }, - + Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), Track.composite.withAlbumProperty('date'), Thing.composite.exposeDependency('#album.date'), ]), -- cgit 1.3.0-6-gf8a5 From 083a4b8c3a0e545a2d8195255d57c5b7e0c49028 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 27 Aug 2023 16:15:34 -0300 Subject: data: misc. additions, fixes & refactoring Thing.composite.from: * Transparently support expose.transform steps inside nested compositions, w/ various Thing.composite.from clean-up * Support continuation.raise() without provided dependencies * add Thing.composite.exposeConstant * add Thing.composite.withResultOfAvailabilityCheck * supports {mode: 'null' | 'empty' | 'falsy'} * works with dependency or update value * add Thing.composite.earlyExitWithoutDependency * refactor Thing.composite.exposeDependencyOrContinue * refactor Thing.composite.exposeUpdateValueOrContinue * add Track.withHasUniqueCoverArt * refactor Track.coverArtFileExtension * refactor Track.hasUniqueCoverArt --- src/data/things/thing.js | 433 +++++++++++++++++++++++++++++------------------ src/data/things/track.js | 137 +++++++-------- 2 files changed, 331 insertions(+), 239 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f88e8726..892a3a4b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -780,6 +780,16 @@ export default class Thing extends CacheableObject { break expose; } + if ( + step.expose.transform && + !step.expose.compute && + !base.flags.update && + !base.flags.compose + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { if (typeof dependency === 'string' && dependency.startsWith('#')) continue; @@ -794,26 +804,7 @@ export default class Thing extends CacheableObject { } } - let fn, type; - if (base.flags.update) { - if (step.expose.transform) { - type = 'transform'; - fn = step.expose.transform; - } else { - type = 'compute'; - fn = step.expose.compute; - } - } else { - if (step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; - } - - type = 'compute'; - fn = step.expose.compute; - } - - exposeSteps.push(step.expose); + exposeSteps.push(step); } }); } @@ -845,38 +836,38 @@ export default class Thing extends CacheableObject { function _filterDependencies(dependencies, step) { const filteredDependencies = - (step.dependencies - ? filterProperties(dependencies, step.dependencies) + (step.expose.dependencies + ? filterProperties(dependencies, step.expose.dependencies) : {}); - if (step.mapDependencies) { - for (const [to, from] of Object.entries(step.mapDependencies)) { + if (step.expose.mapDependencies) { + for (const [to, from] of Object.entries(step.expose.mapDependencies)) { filteredDependencies[to] = dependencies[from] ?? null; } } - if (step.options) { - filteredDependencies['#options'] = step.options; + if (step.expose.options) { + filteredDependencies['#options'] = step.expose.options; } return filteredDependencies; } function _assignDependencies(continuationAssignment, step) { - if (!step.mapContinuation) { + if (!step.expose.mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.mapContinuation)) { + for (const [from, to] of Object.entries(step.expose.mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform, step) { + function _prepareContinuation(transform) { const continuationStorage = { returnedWith: null, providedDependencies: null, @@ -930,27 +921,25 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition (annotation: ${annotation})`)); - for (let i = 0; i < exposeSteps.length; i++) { + stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; debug(() => [`step #${i+1}:`, step]); const transform = valueSoFar !== noTransformSymbol && - step.transform; + step.expose.transform; const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform, step); + const {continuation, continuationStorage} = _prepareContinuation(transform); - if (transform) { - debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } else { - debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } + debug(() => + `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = (transform - ? step.transform(valueSoFar, filteredDependencies, continuation) - : step.compute(filteredDependencies, continuation)); + ? step.expose.transform(valueSoFar, filteredDependencies, continuation) + : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { if (base.flags.compose) { @@ -964,39 +953,34 @@ export default class Thing extends CacheableObject { return result; } - if (continuationStorage.returnedWith === 'exit') { - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - - debug(() => `step #${i+1} - result: raise`); - - break; - } - - if (continuationStorage.returnedWith === 'continuation') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - debug(() => `step #${i+1} - result: continuation`); + switch (continuationStorage.returnedWith) { + case 'exit': + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + debug(() => `step #${i+1} - result: raise`); + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; + if (transform) valueSoFar = continuationStorage.providedValue; + break stepLoop; + + case 'continuation': + if (transform) { + valueSoFar = continuationStorage.providedValue; + } - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + debug(() => `step #${i+1} - result: continuation`); + debug(() => [`assign dependencies:`, assignDependencies]); + } else { + debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); + } - debug(() => [`assign dependencies:`, assignDependencies]); - } + break; } } @@ -1008,53 +992,50 @@ export default class Thing extends CacheableObject { debug(() => `completed all steps, reached base`); - const filteredDependencies = _filterDependencies(dependencies, base.expose); + const filteredDependencies = _filterDependencies(dependencies, base); - // Note: base.flags.compose is not compatible with base.flags.update. - if (base.expose.transform) { - debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + const transform = + valueSoFar !== noTransformSymbol && + base.expose.transform; - const result = base.expose.transform(valueSoFar, filteredDependencies); + debug(() => + `base - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - - return result; - } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); - - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + if (base.flags.compose) { + const {continuation, continuationStorage} = _prepareContinuation(transform); - const result = base.expose.compute(filteredDependencies, continuation); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies, continuation) + : base.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); } - if (continuationStorage.returnedWith === 'continuation') { - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); - } - - if (continuationStorage.returnedWith === 'exit') { - debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); - - debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationIfApplicable(exportDependencies); + switch (continuationStorage.returnedWith) { + case 'continuation': + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + + case 'exit': + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); } } else { - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - - const result = base.expose.compute(filteredDependencies); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies) + : base.expose.compute(filteredDependencies)); debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); @@ -1063,14 +1044,23 @@ export default class Thing extends CacheableObject { } } - if (base.flags.update) { - expose.transform = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); + const transformFn = + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); + + const computeFn = + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + + if (base.flags.compose) { + if (exposeSteps.some(step => step.expose.transform)) { + expose.transform = transformFn; + } + expose.compute = computeFn; + } else if (base.flags.update) { + expose.transform = transformFn; } else { - expose.compute = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + expose.compute = computeFn; } } @@ -1146,68 +1136,177 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes a dependency as it is, or continues if it's unavailable. - // By default, "unavailable" means dependency === null; provide - // {mode: 'empty'} to check with empty() instead, continuing for - // empty arrays also. - exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); + // Exposes a constant value exactly as it is; like exposeDependency, this + // is typically the base of a composition serving as a particular property + // descriptor. It generally follows steps which will conditionally early + // exit with some other value, with the exposeConstant base serving as the + // fallback default value. Like exposeDependency, set {update} to true or + // an object to indicate that the property as a whole updates. + exposeConstant: (value, {update = false} = {}) => ({ + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, + + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, + + update: + (typeof update === 'object' + ? update + : null), + }), + + // Checks the availability of a dependency or the update value and provides + // the result to later steps under '#availability' (by default). This is + // mainly intended for use by the more specific utilities, which you should + // consider using instead. Customize {mode} to select one of these modes, + // or leave unset and default to 'null': + // + // * 'null': Check that the value isn't null. + // * 'empty': Check that the value is neither null nor an empty array. + // * 'falsy': Check that the value isn't false when treated as a boolean + // (nor an empty array). Keep in mind this will also be false + // for values like zero and the empty string! + // + withResultOfAvailabilityCheck({ + fromUpdateValue, + fromDependency, + mode = 'null', + to = '#availability', + }) { + if (!['null', 'empty', 'falsy'].includes(mode)) { + throw new TypeError(`Expected mode to be null, empty, or falsy`); } - return { - annotation: `Thing.composite.exposeDependencyOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - mapDependencies: {dependency}, - - compute({dependency, '#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(dependency) - : dependency === null); - - if (shouldContinue) { - return continuation(); - } else { - return continuation.exit(dependency); - } - }, - }, + if (fromUpdateValue && fromDependency) { + throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); + } + + if (!fromUpdateValue && !fromDependency) { + throw new TypeError(`Missing dependency name (or fromUpdateValue)`); + } + + const checkAvailability = (value, mode) => { + switch (mode) { + case 'null': return value !== null; + case 'empty': return !empty(value); + case 'falsy': return !empty(value) && !!value; + default: return false; + } }; + + if (fromDependency) { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {from: fromDependency}, + mapContinuation: {to}, + options: {mode}, + compute: ({from, '#options': {mode}}, continuation) => + continuation({to: checkAvailability(from, mode)}), + }, + }; + } else { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + flags: {expose: true, compose: true}, + expose: { + mapContinuation: {to}, + options: {mode}, + transform: (value, {'#options': {mode}}, continuation) => + continuation(value, {to: checkAvailability(value, mode)}), + }, + }; + } }, + // Exposes a dependency as it is, or continues if it's unavailable. + // See withResultOfAvailabilityCheck for {mode} options! + exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), + }, + }, + ]), + // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. By default, "unavailable" means - // value === null; provide {mode: 'empty'} to check with empty() instead, - // continuing for empty arrays also. - exposeUpdateValueOrContinue({mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); - } + // or continues if it's unavailable. See withResultOfAvailabilityCheck + // for {mode} options! + exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromUpdateValue: true, + mode, + }), - return { - annotation: `Thing.composite.exposeUpdateValueOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - - transform(value, {'#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(value) - : value === null); - - if (shouldContinue) { - return continuation(value); - } else { - return continuation.exit(value); - } - } + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, }, - }; - }, + + { + flags: {expose: true, compose: true}, + expose: { + transform: (value, {}, continuation) => + continuation.exit(value), + }, + }, + ]), + + // Early exits if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => + Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + options: {value}, + + compute: ({ + '#availability': availability, + '#options': {value}, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + }, + ]), // -- Compositional steps for processing data -- diff --git a/src/data/things/track.js b/src/data/things/track.js index 228b2af1..dc1f5f2a 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -76,40 +76,24 @@ export class Track extends Thing { disableUniqueCoverArt: Thing.common.flag(), // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the cover's - // main artwork. (It does inherit `trackCoverArtFileExtension` if present - // on the album.) + // track's unique cover artwork, if any, and does not inherit the extension + // of the album's main artwork. It does inherit trackCoverArtFileExtension, + // if present on the album. coverArtFileExtension: Thing.composite.from(`Track.coverArtFileExtension`, [ - Track.composite.withAlbumProperties({ - properties: [ - 'trackCoverArtistContribsByRef', - 'trackCoverArtFileExtension', - ], - }), + // No cover art file extension if the track doesn't have unique artwork + // in the first place. + Track.composite.withHasUniqueCoverArt(), + Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - { - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackCoverArtistContribsByRef', - '#album.trackCoverArtFileExtension', - ], + // Expose custom coverArtFileExtension update value first. + Thing.composite.exposeUpdateValueOrContinue(), - transform(coverArtFileExtension, { - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - '#album.trackCoverArtFileExtension': trackCoverArtFileExtension, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtFileExtension ?? trackCoverArtFileExtension ?? 'jpg'; - }, - }, - }, + // Expose album's trackCoverArtFileExtension if no update value set. + Track.composite.withAlbumProperty('trackCoverArtFileExtension'), + Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + + // Fallback to 'jpg'. + Thing.composite.exposeConstant('jpg'), ]), // Date of cover art release. Like coverArtFileExtension, this represents @@ -204,47 +188,8 @@ export class Track extends Thing { // the usual hasCoverArt to emphasize that it does not inherit from the // album.) hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? false - : continuation()), - }, - }, - - Thing.composite.withResolvedContribs({ - from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', - }), - - { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#coverArtistContribs'], - compute: ({'#coverArtistContribs': coverArtistContribs}, continuation) => - (empty(coverArtistContribs) - ? continuation() - : true), - }, - }, - - Track.composite.withAlbumProperties({ - properties: ['trackCoverArtistContribs'], - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#album.trackCoverArtistContribs'], - compute: ({'#album.trackCoverArtistContribs': trackCoverArtistContribs}) => - (empty(trackCoverArtistContribs) - ? false - : true), - }, - }, + Track.composite.withHasUniqueCoverArt(), + Thing.composite.exposeDependency('#hasUniqueCoverArt'), ]), originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( @@ -609,6 +554,54 @@ export class Track extends Thing { [outputDependency]: '#originalRelease', }), ]), + + // The algorithm for checking if a track has unique cover art is used in a + // couple places, so it's defined in full as a compositional step. + withHasUniqueCoverArt: ({to = '#hasUniqueCoverArt'} = {}) => + Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), + }, + }, + + Thing.composite.withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), + }, + }, + + Track.composite.withAlbumProperty('trackCoverArtistContribs'), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), + }, + }, + ]), }; [inspect.custom]() { -- cgit 1.3.0-6-gf8a5 From 29580733b79872333f3f9e45d50d987218d334ea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 28 Aug 2023 13:19:57 -0300 Subject: data: fix annotation typo --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 892a3a4b..78ff4c81 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1198,7 +1198,7 @@ export default class Thing extends CacheableObject { if (fromDependency) { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -1210,7 +1210,7 @@ export default class Thing extends CacheableObject { }; } else { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, -- cgit 1.3.0-6-gf8a5 From 895712f5a0381c41557c6d306d6697019368bb7b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:33:46 -0300 Subject: data: clean up Thing.composite.from debug messaging * print annotation next to every log message, instead of just the begin/end messages * add Thing.composite.debug() to conveniently wrap one property access * don't output (and don't access) track album in inspect.custom when depth < 0 --- src/data/things/thing.js | 69 ++++++++++++++++++++++++++++++++---------------- src/data/things/track.js | 40 ++++++++++------------------ 2 files changed, 60 insertions(+), 49 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 78ff4c81..4fd6a26a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -719,11 +719,18 @@ export default class Thing extends CacheableObject { from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { + const label = + (annotation + ? color.dim(`[composite: ${annotation}]`) + : color.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { - console.log(`[composite]`, ...result); + console.log(label, ...result.map(value => + (typeof value === 'object' + ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + : value))); } else { - console.log(`[composite]`, result); + console.log(label, result); } } }; @@ -919,7 +926,7 @@ export default class Thing extends CacheableObject { let valueSoFar = value; // Set only for {update: true} compositions let exportDependencies = null; // Set only for {compose: true} compositions - debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + debug(() => color.bright(`begin composition`)); stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; @@ -932,9 +939,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = _filterDependencies(dependencies, step); const {continuation, continuationStorage} = _prepareContinuation(transform); - debug(() => - `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); const result = (transform @@ -946,18 +953,15 @@ export default class Thing extends CacheableObject { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); } - debug(() => `step #${i+1} - early-exit (inferred)`); - debug(() => `early-exit: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - + debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); + debug(() => color.bright(`end composition`)); return result; } switch (continuationStorage.returnedWith) { case 'exit': - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': @@ -986,7 +990,7 @@ export default class Thing extends CacheableObject { if (exportDependencies) { debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } @@ -998,9 +1002,9 @@ export default class Thing extends CacheableObject { valueSoFar !== noTransformSymbol && base.expose.transform; - debug(() => - `base - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `base - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); if (base.flags.compose) { const {continuation, continuationStorage} = _prepareContinuation(transform); @@ -1020,15 +1024,15 @@ export default class Thing extends CacheableObject { case 'exit': debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`early-exit:`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } } else { @@ -1037,8 +1041,8 @@ export default class Thing extends CacheableObject { ? base.expose.transform(valueSoFar, filteredDependencies) : base.expose.compute(filteredDependencies)); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`base - non-compose (final) result:`, result]); + debug(() => color.bright(`end composition`)); return result; } @@ -1067,6 +1071,25 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Evaluates a function with composite debugging enabled, turns debugging + // off again, and returns the result of the function. This is mostly syntax + // sugar, but also helps avoid unit tests avoid accidentally printing debug + // info for a bunch of unrelated composites (due to property enumeration + // when displaying an unexpected result). Use as so: + // + // Without debugging: + // t.same(thing.someProp, value) + // + // With debugging: + // t.same(Thing.composite.debug(() => thing.someProp), value) + // + debug(fn) { + Thing.composite.from.debug = true; + const value = fn(); + Thing.composite.from.debug = false; + return value; + }, + // -- Compositional steps for compositions to nest -- // Provides dependencies exactly as they are (or null if not defined) to the diff --git a/src/data/things/track.js b/src/data/things/track.js index dc1f5f2a..8ddf3624 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -604,38 +604,26 @@ export class Track extends Thing { ]), }; - [inspect.custom]() { - const base = Thing.prototype[inspect.custom].apply(this); + [inspect.custom](depth) { + const parts = []; - const rereleasePart = - (this.originalReleaseTrackByRef - ? `${color.yellow('[rerelease]')} ` - : ``); + parts.push(Thing.prototype[inspect.custom].apply(this)); - const {album, dataSourceAlbum} = this; + if (this.originalReleaseTrackByRef) { + parts.unshift(`${color.yellow('[rerelease]')} `); + } - const albumName = - (album - ? album.name - : dataSourceAlbum?.name); - - const albumIndex = - albumName && - (album - ? album.tracks.indexOf(this) - : dataSourceAlbum.tracks.indexOf(this)); - - const trackNum = - albumName && + let album; + if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { + const albumName = album.name; + const albumIndex = album.tracks.indexOf(this); + const trackNum = (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`); + parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + } - const albumPart = - albumName - ? ` (${color.yellow(trackNum)} in ${color.green(albumName)})` - : ``; - - return rereleasePart + base + albumPart; + return parts.join(''); } } -- cgit 1.3.0-6-gf8a5 From 3336d5f15e29350656273a37c0a1c7a69d24663b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:56:04 -0300 Subject: data: Thing.composite.from: fix including '#' deps from base ...in the final composition's dependencies. --- src/data/things/thing.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 4fd6a26a..25d8c8a3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -756,7 +756,14 @@ export default class Thing extends CacheableObject { } const exposeSteps = []; - const exposeDependencies = new Set(base.expose?.dependencies); + const exposeDependencies = new Set(); + + if (base.expose?.dependencies) { + for (const dependency of base.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } if (base.expose?.mapDependencies) { for (const dependency of Object.values(base.expose.mapDependencies)) { -- cgit 1.3.0-6-gf8a5 From 7d6d8a2839ece38c4a70bd9e3fda73b2e0aa39b8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:57:22 -0300 Subject: data: Thing.composite.earlyExitWithoutDependency: latest syntax --- src/data/things/thing.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 25d8c8a3..6bdc897f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1321,19 +1321,25 @@ export default class Thing extends CacheableObject { mode, }), + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + { flags: {expose: true, compose: true}, expose: { dependencies: ['#availability'], options: {value}, - compute: ({ - '#availability': availability, - '#options': {value}, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, }, ]), -- cgit 1.3.0-6-gf8a5 From 011c197aeedab56d501b03b800433dd0cd9bc4f7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:28:47 -0300 Subject: data: always define composite utilities with `key() {}` syntax Sublime Text doesn't index the key in `key: () => {}` as a symbol for function definitions if the parameter list takes up more than one line, but always works for `key() {}`. This also just makes it a little easier to add "preamble" before the main return value, when relevant. Consistent syntax is usually a plus for recurring behavioral forms! --- src/data/things/thing.js | 121 +++++++++++++++++++++++++++------------------- src/data/things/track.js | 123 ++++++++++++++++++++++++++--------------------- 2 files changed, 140 insertions(+), 104 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 6bdc897f..cd62288e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1151,20 +1151,24 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, + exposeDependency(dependency, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeDependency`, + flags: {expose: true, update: !!update}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1172,20 +1176,24 @@ export default class Thing extends CacheableObject { // exit with some other value, with the exposeConstant base serving as the // fallback default value. Like exposeDependency, set {update} to true or // an object to indicate that the property as a whole updates. - exposeConstant: (value, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, + exposeConstant(value, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Checks the availability of a dependency or the update value and provides // the result to later steps under '#availability' (by default). This is @@ -1254,8 +1262,10 @@ export default class Thing extends CacheableObject { // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + exposeDependencyOrContinue(dependency, { + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1280,13 +1290,16 @@ export default class Thing extends CacheableObject { continuation.exit(dependency), }, }, - ]), + ]); + }, // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck // for {mode} options! - exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + exposeUpdateValueOrContinue({ + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1310,12 +1323,16 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => - Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1342,7 +1359,8 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // -- Compositional steps for processing data -- @@ -1350,20 +1368,22 @@ export default class Thing extends CacheableObject { // 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. - withResolvedContribs: ({from, to}) => ({ - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + withResolvedContribs({from, to}) { + return { + annotation: `Thing.composite.withResolvedContribs`, + flags: {expose: true, compose: true}, - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }), + expose: { + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => + continuation({ + to: Thing.findArtistsFromContribs(from, artistData), + }), + }, + }; + }, // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -1372,14 +1392,14 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference: ({ + withResolvedReference({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) => - Thing.composite.from(`Thing.composite.withResolvedReference`, [ + }) { + return Thing.composite.from(`Thing.composite.withResolvedReference`, [ { flags: {expose: true, compose: true}, expose: { @@ -1423,6 +1443,7 @@ export default class Thing extends CacheableObject { }, }, }, - ]), + ]); + }, }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 8ddf3624..cdc9cec3 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -366,8 +366,11 @@ export class Track extends Thing { // dependencies provided. If allowOverride is true, then the continuation // will also be called if the original release exposed the requested // property as null. - inheritFromOriginalRelease: ({property: originalProperty, allowOverride = false}) => - Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ + inheritFromOriginalRelease({ + property: originalProperty, + allowOverride = false, + }) { + return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ Track.composite.withOriginalRelease(), { @@ -386,56 +389,59 @@ export class Track extends Thing { }, }, } - ]), + ]); + }, // Gets the track's album. Unless earlyExitIfNotFound is overridden false, // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. - withAlbum: ({to = '#album', earlyExitIfNotFound = true} = {}) => ({ - annotation: `Track.composite.withAlbum`, - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['this', 'albumData'], - mapContinuation: {to}, - options: {earlyExitIfNotFound}, - - compute({ - this: track, - albumData, - '#options': {earlyExitIfNotFound}, - }, continuation) { - if (empty(albumData)) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - const album = - albumData?.find(album => album.tracks.includes(track)); - - if (!album) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } + withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { + return { + annotation: `Track.composite.withAlbum`, + flags: {expose: true, compose: true}, - return continuation({to: album}); + expose: { + dependencies: ['this', 'albumData'], + mapContinuation: {to}, + options: {earlyExitIfNotFound}, + + compute({ + this: track, + albumData, + '#options': {earlyExitIfNotFound}, + }, continuation) { + if (empty(albumData)) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } + + const album = + albumData?.find(album => album.tracks.includes(track)); + + if (!album) { + return ( + (earlyExitIfNotFound + ? continuation.exit(null) + : continuation({to: null}))); + } + + return continuation({to: album}); + }, }, - }, - }), + }; + }, // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, and earlyExitIfNotFound hasn't been set, the property // will be provided as null. - withAlbumProperty: (property, { + withAlbumProperty(property, { to = '#album.' + property, earlyExitIfNotFound = false, - } = {}) => - Thing.composite.from(`Track.composite.withAlbumProperty`, [ + } = {}) { + return Thing.composite.from(`Track.composite.withAlbumProperty`, [ Track.composite.withAlbum({earlyExitIfNotFound}), { @@ -454,18 +460,19 @@ export class Track extends Thing { : continuation.raise({to: null})), }, }, - ]), + ]); + }, // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property // name. If the track's album isn't available, and earlyExitIfNotFound // hasn't been set, the same dependency names will be provided as null. - withAlbumProperties: ({ + withAlbumProperties({ properties, prefix = '#album', earlyExitIfNotFound = false, - }) => - Thing.composite.from(`Track.composite.withAlbumProperties`, [ + }) { + return Thing.composite.from(`Track.composite.withAlbumProperties`, [ Track.composite.withAlbum({earlyExitIfNotFound}), { @@ -494,17 +501,18 @@ export class Track extends Thing { }, }, }, - ]), + ]); + }, // Gets the track section containing this track from its album's track list. // Unless earlyExitIfNotFound is overridden false, this will early exit if // the album can't be found or if none of its trackSections includes the // track for some reason. - withContainingTrackSection: ({ + withContainingTrackSection({ to = '#trackSection', earlyExitIfNotFound = true, - } = {}) => - Thing.composite.from(`Track.composite.withContainingTrackSection`, [ + } = {}) { + return Thing.composite.from(`Track.composite.withContainingTrackSection`, [ Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), { @@ -534,14 +542,17 @@ export class Track extends Thing { }, }, }, - ]), + ]); + }, // Just includes the original release of this track as a dependency, or // null, if it's not a rerelease. Note that this will early exit if the // original release is specified by reference and that reference doesn't // resolve to anything. Outputs to '#originalRelease' by default. - withOriginalRelease: ({to: outputDependency = '#originalRelease'} = {}) => - Thing.composite.from(`Track.composite.withOriginalRelease`, [ + withOriginalRelease({ + to = '#originalRelease', + } = {}) { + return Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ ref: 'originalReleaseTrackByRef', data: 'trackData', @@ -553,12 +564,15 @@ export class Track extends Thing { Thing.composite.export({ [outputDependency]: '#originalRelease', }), - ]), + ]); + }, // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. - withHasUniqueCoverArt: ({to = '#hasUniqueCoverArt'} = {}) => - Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ + withHasUniqueCoverArt({ + to = '#hasUniqueCoverArt', + } = {}) { + return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ { flags: {expose: true, compose: true}, expose: { @@ -601,7 +615,8 @@ export class Track extends Thing { : continuation.raise({to: true})), }, }, - ]), + ]); + }, }; [inspect.custom](depth) { -- cgit 1.3.0-6-gf8a5 From b38a3c787dc4fbec5e2dc0c297bbcd3ceae83349 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:31:54 -0300 Subject: data: update Track.otherReleases implementation Also adds {selfIfOriginal} option to withOriginalRelease(). --- src/data/things/track.js | 81 +++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 32 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index cdc9cec3..3b2029e8 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -198,33 +198,30 @@ export class Track extends Thing { find.track ), - otherReleases: { - flags: {expose: true}, + otherReleases: + Thing.composite.from(`Track.otherReleases`, [ + Thing.composite.earlyExitWithoutDependency('trackData'), + Track.composite.withOriginalRelease({selfIfOriginal: true}), - expose: { - dependencies: ['this', 'originalReleaseTrackByRef', 'trackData'], - - compute: ({ - this: t1, - originalReleaseTrackByRef: t1origRef, - trackData, - }) => { - if (!trackData) { - return []; - } - - const t1orig = find.track(t1origRef, trackData); - - return [ - t1orig, - ...trackData.filter((t2) => { - const {originalReleaseTrack: t2orig} = t2; - return t2 !== t1 && t2orig && (t2orig === t1orig || t2orig === t1); - }), - ].filter(Boolean); + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }, }, - }, - }, + ]), artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), @@ -545,12 +542,15 @@ export class Track extends Thing { ]); }, - // Just includes the original release of this track as a dependency, or - // null, if it's not a rerelease. Note that this will early exit if the - // original release is specified by reference and that reference doesn't - // resolve to anything. Outputs to '#originalRelease' by default. + // Just includes the original release of this track as a dependency. + // If this track isn't a rerelease, then it'll provide null, unless the + // {selfIfOriginal} option is set, in which case it'll provide this track + // itself. Note that this will early exit if the original release is + // specified by reference and that reference doesn't resolve to anything. + // Outputs to '#originalRelease' by default. withOriginalRelease({ to = '#originalRelease', + selfIfOriginal = false, } = {}) { return Thing.composite.from(`Track.composite.withOriginalRelease`, [ Thing.composite.withResolvedReference({ @@ -561,9 +561,26 @@ export class Track extends Thing { earlyExitIfNotFound: true, }), - Thing.composite.export({ - [outputDependency]: '#originalRelease', - }), + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + }, ]); }, -- cgit 1.3.0-6-gf8a5 From 1cf06b4898b517993a171a5f6c39d00609105253 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:32:40 -0300 Subject: data, infra: only make exposed properties enumerable This prevents them from being displayed in, for example, node-tap mismatched test case output. AFAIK, we generally don't depend on the enumerability of properties anywhere in hsmusic's codebase, and it doesn't really make sense for unexposed properties to be enumerable in the first place. --- src/data/things/cacheable-object.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 24a6cf01..62c23d13 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -141,7 +141,7 @@ export default class CacheableObject { const definition = { configurable: false, - enumerable: true, + enumerable: flags.expose, }; if (flags.update) { -- cgit 1.3.0-6-gf8a5 From c98a1a5faa40bfad79bc3b07aa8e9d53111b10a7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:46:58 -0300 Subject: data: Track: misc. minor fixes --- src/data/things/track.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 3b2029e8..724c8606 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -200,7 +200,7 @@ export class Track extends Thing { otherReleases: Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData'), + Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), Track.composite.withOriginalRelease({selfIfOriginal: true}), { @@ -242,7 +242,7 @@ export class Track extends Thing { }, }, - Track.composite.withAlbumProperties({properties: ['artistContribs']}), + Track.composite.withAlbumProperty('artistContribs'), { flags: {expose: true}, -- cgit 1.3.0-6-gf8a5 From d4e5d37ef9bc09789715f978e1b636bc2a1f8e97 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:47:16 -0300 Subject: data: update Track.composite.withAlbum implementation --- src/data/things/track.js | 84 ++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 32 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 724c8606..22043688 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -393,41 +393,61 @@ export class Track extends Thing { // this will early exit with null in two cases - albumData being missing, // or not including an album whose .tracks array includes this track. withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { - return { - annotation: `Track.composite.withAlbum`, - flags: {expose: true, compose: true}, + return Thing.composite.from(`Track.composite.withAlbum`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: 'albumData', + mode: 'empty', + to: '#albumDataAvailability', + }), - expose: { - dependencies: ['this', 'albumData'], - mapContinuation: {to}, - options: {earlyExitIfNotFound}, - - compute({ - this: track, - albumData, - '#options': {earlyExitIfNotFound}, - }, continuation) { - if (empty(albumData)) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - const album = - albumData?.find(album => album.tracks.includes(track)); - - if (!album) { - return ( - (earlyExitIfNotFound - ? continuation.exit(null) - : continuation({to: null}))); - } - - return continuation({to: album}); + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, }, - }; + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), + }, + }, + ]); }, // Gets a single property from this track's album, providing it as the same -- cgit 1.3.0-6-gf8a5 From f874ea879e8b9555baaaa3a38ec6a00432721846 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 11:00:07 -0300 Subject: data, test: update & test Track.originalReleaseTrack --- src/data/things/track.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 22043688..9b1a8226 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -192,11 +192,10 @@ export class Track extends Thing { Thing.composite.exposeDependency('#hasUniqueCoverArt'), ]), - originalReleaseTrack: Thing.common.dynamicThingFromSingleReference( - 'originalReleaseTrackByRef', - 'trackData', - find.track - ), + originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [ + Track.composite.withOriginalRelease(), + Thing.composite.exposeDependency('#originalRelease'), + ]), otherReleases: Thing.composite.from(`Track.otherReleases`, [ -- cgit 1.3.0-6-gf8a5 From 59023bad2de5cd76edced5393cc38afc6b46fc1c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 11:01:25 -0300 Subject: data: fix mis-indented Thing.composite.from calls --- src/data/things/track.js | 52 +++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 9b1a8226..3a4e1585 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -155,11 +155,10 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: - Thing.composite.from(`Track.album`, [ - Track.composite.withAlbum(), - Thing.composite.exposeDependency('#album'), - ]), + album: Thing.composite.from(`Track.album`, [ + Track.composite.withAlbum(), + Thing.composite.exposeDependency('#album'), + ]), // Note - this is an internal property used only to help identify a track. // It should not be assumed in general that the album and dataSourceAlbum match @@ -197,30 +196,29 @@ export class Track extends Thing { Thing.composite.exposeDependency('#originalRelease'), ]), - otherReleases: - Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), - Track.composite.withOriginalRelease({selfIfOriginal: true}), + otherReleases: Thing.composite.from(`Track.otherReleases`, [ + Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), + Track.composite.withOriginalRelease({selfIfOriginal: true}), - { - flags: {expose: true}, - expose: { - dependencies: ['this', 'trackData', '#originalRelease'], - compute: ({ - this: thisTrack, - trackData, - '#originalRelease': originalRelease, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), }, - ]), + }, + ]), artistContribs: Thing.composite.from(`Track.artistContribs`, [ Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), -- cgit 1.3.0-6-gf8a5 From 5e5c2d9e1ee9dbe1c715e4d53bcb244ffcf606b0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 11:05:58 -0300 Subject: data: misc. style consistency tweaks --- src/data/things/album.js | 15 +++------------ src/data/things/artist.js | 6 ++---- src/data/things/flash.js | 6 +----- src/data/things/track.js | 19 ++++--------------- src/data/things/wiki-info.js | 6 +----- 5 files changed, 11 insertions(+), 41 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index c012c243..06982903 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -128,6 +128,9 @@ export class Album extends Thing { commentatorArtists: Thing.common.commentatorArtists(), + groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group), + artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'), @@ -146,18 +149,6 @@ export class Album extends Thing { : [], }, }, - - groups: Thing.common.dynamicThingsFromReferenceList( - 'groupsByRef', - 'groupData', - find.group - ), - - artTags: Thing.common.dynamicThingsFromReferenceList( - 'artTagsByRef', - 'artTagData', - find.artTag - ), }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/artist.js b/src/data/things/artist.js index bde84cfa..b2383057 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -111,10 +111,8 @@ export class Artist extends Thing { }, }, - flashesAsContributor: Artist.filterByContrib( - 'flashData', - 'contributorContribs' - ), + flashesAsContributor: + Artist.filterByContrib('flashData', 'contributorContribs'), }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 445fd07c..3f870c51 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -141,10 +141,6 @@ export class FlashAct extends Thing { // Expose only - flashes: Thing.common.dynamicThingsFromReferenceList( - 'flashesByRef', - 'flashData', - find.flash - ), + flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash), }) } diff --git a/src/data/things/track.js b/src/data/things/track.js index 3a4e1585..74713a00 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -167,11 +167,7 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.dynamicThingFromSingleReference( - 'dataSourceAlbumByRef', - 'albumData', - find.album - ), + dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album), date: Thing.composite.from(`Track.date`, [ Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), @@ -303,6 +299,8 @@ export class Track extends Thing { Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), ]), + artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. @@ -342,16 +340,7 @@ export class Track extends Thing { }, }, - featuredInFlashes: Thing.common.reverseReferenceList( - 'flashData', - 'featuredTracks' - ), - - artTags: Thing.common.dynamicThingsFromReferenceList( - 'artTagsByRef', - 'artTagData', - find.artTag - ), + featuredInFlashes: Thing.common.reverseReferenceList('flashData', 'featuredTracks'), }); static composite = { diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index e906cab1..e8279987 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -59,10 +59,6 @@ export class WikiInfo extends Thing { // Expose only - divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList( - 'divideTrackListsByGroupsByRef', - 'groupData', - find.group - ), + divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group), }); } -- cgit 1.3.0-6-gf8a5 From 001bcb69db4f4050fca222568ae2895f58a2f0df Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:51:01 -0300 Subject: data: simplify Thing.composite.from (needs docs update) --- src/data/things/thing.js | 402 +++++++++++++++++++++++++++-------------------- 1 file changed, 230 insertions(+), 172 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cd62288e..782946ce 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -743,7 +743,7 @@ export default class Thing extends CacheableObject { } const base = composition.at(-1); - const steps = composition.slice(0, -1); + const steps = composition.slice(); const aggregate = openAggregate({ message: @@ -751,78 +751,118 @@ export default class Thing extends CacheableObject { (annotation ? ` (${annotation})` : ''), }); - if (base.flags.compose && base.flags.compute) { - push(new TypeError(`Base which composes can't also update yet`)); - } + const baseExposes = + (base.flags + ? base.flags.expose + : true); - const exposeSteps = []; - const exposeDependencies = new Set(); + const baseUpdates = + (base.flags + ? base.flags.update + : false); - if (base.expose?.dependencies) { - for (const dependency of base.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } - } + const baseComposes = + (base.flags + ? base.flags.compose + : true); - if (base.expose?.mapDependencies) { - for (const dependency of Object.values(base.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); } + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + for (let i = 0; i < steps.length; i++) { const step = steps[i]; + const isBase = i === steps.length - 1; const message = - (step.annotation - ? `Errors in step #${i + 1} (${step.annotation})` - : `Errors in step #${i + 1}`); + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (!step.flags.compose) { - push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); - } + if (step.flags) { + let flagsErrored = false; - if (step.flags.update) { - push(new Error(`Steps which update aren't supported yet`)); - } - - if (step.flags.expose) expose: { - if (!step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); - break expose; + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; } - if ( - step.expose.transform && - !step.expose.compute && - !base.flags.update && - !base.flags.compose - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; } - if (step.expose.dependencies) { - for (const dependency of step.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (flagsErrored) { + return; } + } - if (step.expose.mapDependencies) { - for (const dependency of Object.values(step.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; } - exposeSteps.push(step); + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); } }); } + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + aggregate.close(); const constructedDescriptor = {}; @@ -832,64 +872,68 @@ export default class Thing extends CacheableObject { } constructedDescriptor.flags = { - update: !!base.flags.update, - expose: !!base.flags.expose, - compose: !!base.flags.compose, + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, }; - if (base.flags.update) { + if (baseUpdates) { constructedDescriptor.update = base.update; } - if (base.flags.expose) { + if (baseExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol('continuation symbol'); const noTransformSymbol = Symbol('no-transform symbol'); - function _filterDependencies(dependencies, step) { + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { const filteredDependencies = - (step.expose.dependencies - ? filterProperties(dependencies, step.expose.dependencies) + (dependencies + ? filterProperties(availableDependencies, dependencies) : {}); - if (step.expose.mapDependencies) { - for (const [to, from] of Object.entries(step.expose.mapDependencies)) { - filteredDependencies[to] = dependencies[from] ?? null; + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; } } - if (step.expose.options) { - filteredDependencies['#options'] = step.expose.options; + if (options) { + filteredDependencies['#options'] = options; } return filteredDependencies; } - function _assignDependencies(continuationAssignment, step) { - if (!step.expose.mapContinuation) { + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.expose.mapContinuation)) { + for (const [from, to] of Object.entries(mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform) { + function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, - providedDependencies: null, - providedValue: null, + providedDependencies: undefined, + providedValue: undefined, }; const continuation = - (transform + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { continuationStorage.returnedWith = 'continuation'; continuationStorage.providedDependencies = providedDependencies; @@ -908,150 +952,166 @@ export default class Thing extends CacheableObject { return continuationSymbol; }; - if (base.flags.compose) { - continuation.raise = - (transform + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { - const dependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - let valueSoFar = value; // Set only for {update: true} compositions - let exportDependencies = null; // Set only for {compose: true} compositions + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - debug(() => color.bright(`begin composition`)); + const availableDependencies = {...initialDependencies}; - stepLoop: for (let i = 0; i < exposeSteps.length; i++) { - const step = exposeSteps[i]; - debug(() => [`step #${i+1}:`, step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const transform = - valueSoFar !== noTransformSymbol && - step.expose.transform; + stepLoop: for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform); + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ - `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); const result = - (transform + (callingTransformForThisStep ? step.expose.transform(valueSoFar, filteredDependencies, continuation) : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { - if (base.flags.compose) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); - debug(() => color.bright(`end composition`)); + debug(() => color.bright(`end composition - exit (inferred)`)); + return result; } - switch (continuationStorage.returnedWith) { - case 'exit': - debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; - - case 'raise': - debug(() => `step #${i+1} - result: raise`); - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; - if (transform) valueSoFar = continuationStorage.providedValue; - break stepLoop; + const {returnedWith} = continuationStorage; - case 'continuation': - if (transform) { - valueSoFar = continuationStorage.providedValue; - } + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); - debug(() => `step #${i+1} - result: continuation`); - debug(() => [`assign dependencies:`, assignDependencies]); - } else { - debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); - break; + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } } - } - if (exportDependencies) { - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - - debug(() => `completed all steps, reached base`); + const {providedValue, providedDependencies} = continuationStorage; - const filteredDependencies = _filterDependencies(dependencies, base); + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); - const transform = - valueSoFar !== noTransformSymbol && - base.expose.transform; + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - debug(() => [ - `base - ${transform ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform); - - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies, continuation) - : base.expose.compute(filteredDependencies, continuation)); + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; - if (result !== continuationSymbol) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); - } + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } - switch (continuationStorage.returnedWith) { - case 'continuation': - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'exit': - debug(() => `base - result: early-exit (explicit)`); - debug(() => [`early-exit:`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + switch (returnedWith) { case 'raise': - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); - debug(() => `base - result: raise`); - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - } else { - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies) - : base.expose.compute(filteredDependencies)); + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); - debug(() => [`base - non-compose (final) result:`, result]); - debug(() => color.bright(`end composition`)); + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); - return result; + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } } } @@ -1063,12 +1123,10 @@ export default class Thing extends CacheableObject { (initialDependencies, continuationIfApplicable) => _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - if (base.flags.compose) { - if (exposeSteps.some(step => step.expose.transform)) { - expose.transform = transformFn; - } - expose.compute = computeFn; - } else if (base.flags.update) { + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { expose.transform = transformFn; } else { expose.compute = computeFn; @@ -1229,7 +1287,7 @@ export default class Thing extends CacheableObject { switch (mode) { case 'null': return value !== null; case 'empty': return !empty(value); - case 'falsy': return !empty(value) && !!value; + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; } }; -- cgit 1.3.0-6-gf8a5 From c0bbd7e8fa6c76df4fa492e3a9d3b5e9ef42ec5c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:53:02 -0300 Subject: data: misc. utility additions * add earlyExitWithoutUpdateValue * add raiseWithoutDependency * add raiseWithoutUpdateValue * add earlyExitIfAvailabilityCheckFailed (internal) * refactor earlyExitWithoutDependency The "raise" utilities make use of the new `raiseAbove` continuation feature. --- src/data/things/thing.js | 100 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 8 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 782946ce..501286d7 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1384,6 +1384,36 @@ export default class Thing extends CacheableObject { ]); }, + // Early exits if an availability check fails. + // This is for internal use only - use `earlyExitWithoutDependency` or + // `earlyExitWIthoutUpdateValue` instead. + earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, + }) { + return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), + }, + }, + ]); + }, + // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! earlyExitWithoutDependency(dependency, { @@ -1391,10 +1421,32 @@ export default class Thing extends CacheableObject { value = null, } = {}) { return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Early exits if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Raises if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { flags: {expose: true, compose: true}, @@ -1410,11 +1462,43 @@ export default class Thing extends CacheableObject { { flags: {expose: true, compose: true}, expose: { - dependencies: ['#availability'], - options: {value}, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + }, + ]); + }, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), + // Raises if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, }, ]); -- cgit 1.3.0-6-gf8a5 From 918fb043a640cf937de604fc74cb95566fa66459 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:56:34 -0300 Subject: data: refactor Thing.composite.withResolvedReference --- src/data/things/thing.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 501286d7..389b3845 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1542,30 +1542,8 @@ export default class Thing extends CacheableObject { earlyExitIfNotFound = false, }) { return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {ref}, - mapContinuation: {to}, - - compute: ({ref}, continuation) => - (ref - ? continuation() - : continuation.raise({to: null})), - }, - }, - - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {data}, - - compute: ({data}, continuation) => - (data === null - ? continuation.exit(null) - : continuation()), - }, - }, + Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + Thing.composite.earlyExitWithoutDependency(data), { flags: {expose: true, compose: true}, -- cgit 1.3.0-6-gf8a5 From 5a63b96cfd3d26e4b74ff4c6dfc793aef057f81b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:57:15 -0300 Subject: data: update Thing.common.dynamicThingsFromReferenceList Only the internal implementation. This should really be updated to take key/value-style parameters, and probably be renamed, but this helps to confirm a swathe of expected behavior continues to work with an existing `common` utility reimplemented compositionally. --- src/data/things/thing.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 389b3845..751e168f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -192,26 +192,29 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList: ( - referenceListProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, + dynamicThingsFromReferenceList( + refs, + data, + findFunction + ) { + return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(refs, {value: []}), + Thing.composite.earlyExitWithoutDependency(data, {value: []}), - expose: { - dependencies: [referenceListProperty, thingDataProperty], - compute: ({ - [referenceListProperty]: refs, - [thingDataProperty]: thingData, - }) => - refs && thingData - ? refs - .map((ref) => findFn(ref, thingData, {mode: 'quiet'})) - .filter(Boolean) - : [], - }, - }), + { + flags: {expose: true}, + expose: { + mapDependencies: {refs, data}, + options: {findFunction}, + + compute: ({refs, data, '#options': {findFunction}}) => + refs + .map(ref => findFunction(ref, data, {mode: 'quiet'})) + .filter(Boolean), + }, + }, + ]); + }, // Corresponding function for a single reference. dynamicThingFromSingleReference: ( -- cgit 1.3.0-6-gf8a5 From f3162203ef1f758d500e065804f9dbe478d0481d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 16:05:53 -0300 Subject: data: Thing.composite.from: fix missed step.expose assumptions --- src/data/things/thing.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 751e168f..d4d7c850 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -993,7 +993,7 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition - not transforming`)); } - stepLoop: for (let i = 0; i < steps.length; i++) { + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const isBase = i === steps.length - 1; @@ -1021,8 +1021,8 @@ export default class Thing extends CacheableObject { const result = (callingTransformForThisStep - ? step.expose.transform(valueSoFar, filteredDependencies, continuation) - : step.expose.compute(filteredDependencies, continuation)); + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); -- cgit 1.3.0-6-gf8a5 From 56a8e47f0e5ad276baef9d27c16960e3ea2c583b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 16:06:58 -0300 Subject: data: remove lots of boilerplate {expose: true, compose: true} --- src/data/things/thing.js | 133 +++++++------------ src/data/things/track.js | 336 ++++++++++++++++++++--------------------------- 2 files changed, 195 insertions(+), 274 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d4d7c850..15ec62c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1333,23 +1333,17 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), }, ]); }, @@ -1367,22 +1361,16 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - transform: (value, {}, continuation) => - continuation.exit(value), - }, + transform: (value, {}, continuation) => + continuation.exit(value), }, ]); }, @@ -1396,23 +1384,17 @@ export default class Thing extends CacheableObject { }) { return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, ]); }, @@ -1452,24 +1434,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1485,24 +1461,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1549,21 +1519,18 @@ export default class Thing extends CacheableObject { Thing.composite.earlyExitWithoutDependency(data), { - flags: {expose: true, compose: true}, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation.raise({match}); - }, + return continuation.raise({match}); }, }, ]); diff --git a/src/data/things/track.js b/src/data/things/track.js index 74713a00..ad001445 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -49,18 +49,15 @@ export class Track extends Thing { Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#trackSection'], - compute: ({'#trackSection': trackSection}, continuation) => - // Album.trackSections guarantees the track section will have a - // color property (inheriting from the album's own color), but only - // if it's actually present! Color will be inherited directly from - // album otherwise. - (trackSection - ? trackSection.color - : continuation()), - }, + dependencies: ['#trackSection'], + compute: ({'#trackSection': trackSection}, continuation) => + // Album.trackSections guarantees the track section will have a + // color property (inheriting from the album's own color), but only + // if it's actually present! Color will be inherited directly from + // album otherwise. + (trackSection + ? trackSection.color + : continuation()), }, Track.composite.withAlbumProperty('color'), @@ -225,14 +222,11 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {contribsFromTrack: '#artistContribs'}, - compute: ({contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + mapDependencies: {contribsFromTrack: '#artistContribs'}, + compute: ({contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : contribsFromTrack), }, Track.composite.withAlbumProperty('artistContribs'), @@ -259,14 +253,11 @@ export class Track extends Thing { // of the track. coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? null - : continuation()), - }, + dependencies: ['disableUniqueCoverArt'], + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? null + : continuation()), }, Thing.composite.withResolvedContribs({ @@ -275,14 +266,11 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, - compute: ({contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, + compute: ({contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : contribsFromTrack), }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), @@ -357,21 +345,16 @@ export class Track extends Thing { Track.composite.withOriginalRelease(), { - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['#originalRelease'], + dependencies: ['#originalRelease'], + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation.raise(); - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation.raise(); - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); - - return continuation.exit(value); - }, + return continuation.exit(value); }, - } + }, ]); }, @@ -387,51 +370,43 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: null}))), - }, + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), }, { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => - continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), - }), - }, + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#album': album, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (album - ? continuation.raise({to: album}) - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: album}))), - }, + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), }, ]); }, @@ -448,20 +423,17 @@ export class Track extends Thing { Track.composite.withAlbum({earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {property}, - mapContinuation: {to}, - - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), - }, + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), }, ]); }, @@ -479,29 +451,26 @@ export class Track extends Thing { Track.composite.withAlbum({earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } - return continuation.raise(raise); - }, + return continuation.raise(raise); }, }, ]); @@ -519,30 +488,27 @@ export class Track extends Thing { Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', '#album.trackSections'], - mapContinuation: {to}, - - compute({ - this: track, - '#album.trackSections': trackSections, - }, continuation) { - if (!trackSections) { - return continuation.raise({to: null}); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { - return continuation.exit(null); - } else { - return continuation.raise({to: null}); - } - }, + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } }, }, ]); @@ -568,24 +534,21 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {to}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ - to: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), }, ]); }, @@ -597,15 +560,12 @@ export class Track extends Thing { } = {}) { return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? continuation.raise({to: false}) - : continuation()), - }, + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), }, Thing.composite.withResolvedContribs({ @@ -614,29 +574,23 @@ export class Track extends Thing { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raise({to: true})), - }, + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), }, Track.composite.withAlbumProperty('trackCoverArtistContribs'), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), - }, + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), }, ]); }, -- cgit 1.3.0-6-gf8a5 From f0a94d03d01220ff44c9c7cf610373781dd4c09d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 19:13:32 -0300 Subject: data: refactor Track.coverArtDate --- src/data/things/track.js | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index ad001445..ad90dd2c 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -98,36 +98,15 @@ export class Track extends Thing { // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ - Track.composite.withAlbumProperties({ - properties: [ - 'trackArtDate', - 'trackCoverArtistContribsByRef', - ], - }), + Track.composite.withHasUniqueCoverArt(), + Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - { - flags: {update: true, expose: true}, + Thing.composite.exposeUpdateValueOrContinue(), + + Track.composite.withAlbumProperty('trackArtDate'), + Thing.composite.exposeDependency('#album.trackArtDate', { update: {validate: isDate}, - expose: { - dependencies: [ - 'coverArtistContribsByRef', - 'disableUniqueCoverArt', - '#album.trackArtDate', - '#album.trackCoverArtistContribsByRef', - ], - - transform(coverArtDate, { - coverArtistContribsByRef, - disableUniqueCoverArt, - '#album.trackArtDate': trackArtDate, - '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef, - }) { - if (disableUniqueCoverArt) return null; - if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null; - return coverArtDate ?? trackArtDate; - }, - }, - } + }), ]), originalReleaseTrackByRef: Thing.common.singleReference(Track), -- cgit 1.3.0-6-gf8a5 From 6f54b1211b5b07fe747ce4ebafdf917ce7851324 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 19:22:54 -0300 Subject: test: Track.coverArtFileExtension (unit) --- src/data/things/track.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index ad90dd2c..0b34de20 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -90,7 +90,9 @@ export class Track extends Thing { Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), // Fallback to 'jpg'. - Thing.composite.exposeConstant('jpg'), + Thing.composite.exposeConstant('jpg', { + update: {validate: isFileExtension}, + }), ]), // Date of cover art release. Like coverArtFileExtension, this represents -- cgit 1.3.0-6-gf8a5 From 6325a70991396412eb8e93cee5f17bdb2859ae9d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 19:52:42 -0300 Subject: data, test: update & test misc. Track reverse reference lists * update & test Track.referencedByTracks * update & test Track.sampledByTracks * update & test Track.featuredInFlashes * update Thing.common.reverseReferenceList * add Thing.composite.withReverseReferenceList * add Track.composite.trackReverseReferenceList --- src/data/things/thing.js | 43 +++++++++++++++++++++++++++++--------- src/data/things/track.js | 54 ++++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 39 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 15ec62c3..1c99a323 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -330,16 +330,15 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], - }, - }), + reverseReferenceList({ + data, + refList, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.exposeDependency('#reverseReferenceList'), + ]); + }, // Corresponding function for single references. Note that the return value // is still a list - this is for matching all the objects whose single @@ -1535,5 +1534,29 @@ export default class Thing extends CacheableObject { }, ]); }, + + // Check out the info on Thing.common.reverseReferenceList! + // This is its composable form. + withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); + }, }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 0b34de20..bc9affbe 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -278,38 +278,15 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'trackData'], - - compute: ({this: track, trackData}) => - trackData - ? trackData - .filter((t) => !t.originalReleaseTrack) - .filter((t) => t.referencedTracks?.includes(track)) - : [], - }, - }, + referencedByTracks: Track.composite.trackReverseReferenceList('referencedTracks'), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'trackData'], - - compute: ({this: track, trackData}) => - trackData - ? trackData - .filter((t) => !t.originalReleaseTrack) - .filter((t) => t.sampledTracks?.includes(track)) - : [], - }, - }, + sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'), - featuredInFlashes: Thing.common.reverseReferenceList('flashData', 'featuredTracks'), + featuredInFlashes: Thing.common.reverseReferenceList({ + data: 'flashData', + refList: 'featuredTracks', + }), }); static composite = { @@ -575,6 +552,25 @@ export class Track extends Thing { }, ]); }, + + trackReverseReferenceList(refListProperty) { + return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ + Thing.composite.withReverseReferenceList({ + data: 'trackData', + refList: refListProperty, + originalTracksOnly: true, + }), + + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({'#reverseReferenceList': reverseReferenceList}) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ]); + }, }; [inspect.custom](depth) { -- cgit 1.3.0-6-gf8a5 From 8e783429194f58909f26c7b11d558d5b0a9b163f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 19:59:11 -0300 Subject: data: clean up bad mapDependencies usages --- src/data/things/track.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index bc9affbe..bf56a6dd 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -203,25 +203,15 @@ export class Track extends Thing { }), { - mapDependencies: {contribsFromTrack: '#artistContribs'}, - compute: ({contribsFromTrack}, continuation) => + dependencies: ['#artistContribs'], + compute: ({'#artistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() : contribsFromTrack), }, Track.composite.withAlbumProperty('artistContribs'), - - { - flags: {expose: true}, - expose: { - mapDependencies: {contribsFromAlbum: '#album.artistContribs'}, - compute: ({contribsFromAlbum}) => - (empty(contribsFromAlbum) - ? null - : contribsFromAlbum), - }, - }, + Thing.composite.exposeDependency('#album.artistContribs'), ]), contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ @@ -247,8 +237,8 @@ export class Track extends Thing { }), { - mapDependencies: {contribsFromTrack: '#coverArtistContribs'}, - compute: ({contribsFromTrack}, continuation) => + dependencies: ['#coverArtistContribs'], + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() : contribsFromTrack), -- cgit 1.3.0-6-gf8a5 From a3b80f08fc54cda6a6787bcd078059823026add6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 20:19:22 -0300 Subject: data: update Thing.composition.from documentation --- src/data/things/thing.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1c99a323..19f5fb53 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -565,6 +565,14 @@ export default class Thing extends CacheableObject { // }, // ]); // + // One last note! A super common code pattern when creating more complex + // compositions is to have several steps which *only* expose and compose. + // As a syntax shortcut, you can skip the outer section. It's basically + // like writing out just the {expose: {...}} part. Remember that this + // indicates that the step you're defining is compositional, so you have + // to specify the flags manually for the base, even if this property isn't + // going to get an {update: true} flag. + // // == Cache-safe dependency names: == // // [Disclosure: The caching engine hasn't actually been implemented yet. @@ -705,7 +713,7 @@ export default class Thing extends CacheableObject { // expanded to the scope of the composition instead of following steps. // // For example, suppose your composition (which you expect to include in - // other compositions) brings about several internal, hash-prefixed + // other compositions) brings about several private, hash-prefixed // dependencies to contribute to its own results. Those dependencies won't // end up "bleeding" into the dependency list of whichever composition is // nesting this one - they will totally disappear once all the steps in @@ -718,6 +726,40 @@ export default class Thing extends CacheableObject { // a hash just like the exports from any other compositional step; they're // still dynamically provided dependencies!) // + // Another way to "export" dependencies is by using calling *any* step's + // `continuation.raise()` function. This is sort of like early exiting, + // but instead of quitting out the whole entire property, it will just + // break out of the current, nested composition's list of steps, acting + // as though the composition had finished naturally. The dependencies + // passed to `raise` will be the ones which get exported. + // + // Since `raise` is another way to export dependencies, if you're using + // dynamic export names, you should specify `mapContinuation` on the step + // calling `continuation.raise` as well. + // + // An important note on `mapDependencies` here: A nested composition gets + // free access to all the ordinary properties defined on the thing it's + // working on, but if you want it to depend on *private* dependencies - + // ones prefixed with '#' - which were provided by some other compositional + // step preceding wherever this one gets nested, then you *have* to use + // `mapDependencies` to gain access. Check out the section on "cache-safe + // dependency names" for information on this syntax! + // + // Also - on rare occasion - you might want to make a reusable composition + // that itself causes the composition *it's* nested in to raise. If that's + // the case, give `composition.raiseAbove()` a go! This effectively means + // kicking out of *two* layers of nested composition - the one including + // the step with the `raiseAbove` call, and the composition which that one + // is nested within. You don't need to use `raiseAbove` if the reusable + // utility function just returns a single compositional step, but if you + // want to make use of other compositional steps, it gives you access to + // the same conditional-raise capabilities. + // + // Have some syntax sugar! Since nested compositions are defined by having + // the base be {compose: true}, the composition will infer as much if you + // don't specifying the base's flags at all. Simply use the same shorthand + // syntax as for other compositional steps, and it'll work out cleanly! + // from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { -- cgit 1.3.0-6-gf8a5 From 9d8616ced8f505b499780e859d96f288d67f2154 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 12:13:25 -0300 Subject: data: remove unused Thing.common utilities dynamicInheritContribs is replaced by more specialized behavior on tracks (which are the only thing that inherit contribs this way), and reverseSingleReference, introduced with reverseReferenceList, was never used anywhere. --- src/data/things/thing.js | 68 ------------------------------------------------ 1 file changed, 68 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f5fb53..ad27ca55 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -257,60 +257,6 @@ export default class Thing extends CacheableObject { }, }), - // Dynamically inherit a contribution list from some other object, if it - // hasn't been overridden on this object. This is handy for solo albums - // where all tracks have the same artist, for example. - dynamicInheritContribs: ( - // If this property is explicitly false, the contribution list returned - // will always be empty. - nullerProperty, - - // Property holding contributions on the current object. - contribsByRefProperty, - - // Property holding corresponding "default" contributions on the parent - // object, which will fallen back to if the object doesn't have its own - // contribs. - parentContribsByRefProperty, - - // Data array to search in and "find" function to locate parent object - // (which will be passed the child object and the wiki data array). - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - expose: { - dependencies: [ - 'this', - contribsByRefProperty, - thingDataProperty, - nullerProperty, - 'artistData', - ].filter(Boolean), - - compute({ - this: thing, - [nullerProperty]: nuller, - [contribsByRefProperty]: contribsByRef, - [thingDataProperty]: thingData, - artistData, - }) { - if (!artistData) return []; - if (nuller === false) return []; - const refs = - contribsByRef ?? - findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty]; - if (!refs) return []; - return refs - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who); - }, - }, - }), - // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. contribsPresent: (contribsByRefProperty) => ({ @@ -340,20 +286,6 @@ export default class Thing extends CacheableObject { ]); }, - // Corresponding function for single references. Note that the return value - // is still a list - this is for matching all the objects whose single - // reference (in the given property) matches this Thing. - reverseSingleReference: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], - }, - }), - // General purpose wiki data constructor, for properties like artistData, // trackData, etc. wikiData: (thingClass) => ({ -- cgit 1.3.0-6-gf8a5 From 703f065560e71ec7f750ea8a9dfdff2c71e0fde8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 12:27:50 -0300 Subject: data: move Thing.composite definition into dedicated file --- src/data/things/composite.js | 1179 ++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 1176 +---------------------------------------- 2 files changed, 1181 insertions(+), 1174 deletions(-) create mode 100644 src/data/things/composite.js (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js new file mode 100644 index 00000000..1be60cd1 --- /dev/null +++ b/src/data/things/composite.js @@ -0,0 +1,1179 @@ +// Composes multiple compositional "steps" and a "base" to form a property +// descriptor out of modular building blocks. This is an extension to the +// more general-purpose CacheableObject property descriptor syntax, and +// aims to make modular data processing - which lends to declarativity - +// much easier, without fundamentally altering much of the typical syntax +// or terminology, nor building on it to an excessive degree. +// +// Think of a composition as being a chain of steps which lead into a final +// base property, which is usually responsible for returning the value that +// will actually get exposed when the property being described is accessed. +// +// == The compositional base: == +// +// The final item in a compositional list is its base, and it identifies +// the essential qualities of the property descriptor. The compositional +// steps preceding it may exit early, in which case the expose function +// defined on the base won't be called; or they will provide dependencies +// that the base may use to compute the final value that gets exposed for +// this property. +// +// The base indicates the capabilities of the composition as a whole. +// It should be {expose: true}, since that's the only area that preceding +// compositional steps (currently) can actually influence. If it's also +// {update: true}, then the composition as a whole accepts an update value +// just like normal update-flag property descriptors - meaning it can be +// set with `thing.someProperty = value` and that value will be paseed +// into each (implementing) step's transform() function, as well as the +// base. Bases usually aren't {compose: true}, but can be - check out the +// section on "nesting compositions" for details about that. +// +// Every composition always has exactly one compositional base, and it's +// always the last item in the composition list. All items preceding it +// are compositional steps, described below. +// +// == Compositional steps: == +// +// Compositional steps are, in essence, typical property descriptors with +// the extra flag {compose: true}. They operate on existing dependencies, +// and are typically dynamically constructed by "utility" functions (but +// can also be manually declared within the step list of a composition). +// Compositional steps serve two purposes: +// +// 1. exit early, if some condition is matched, returning and exposing +// some value directly from that step instead of continuing further +// down the step list; +// +// 2. and/or provide new, dynamically created "private" dependencies which +// can be accessed by further steps down the list, or at the base at +// the bottom, modularly supplying information that will contribute to +// the final value exposed for this property. +// +// Usually it's just one of those two, but it's fine for a step to perform +// both jobs if the situation benefits. +// +// Compositional steps are the real "modular" or "compositional" part of +// this data processing style - they're designed to be combined together +// in dynamic, versatile ways, as each property demands it. You usually +// define a compositional step to be returned by some ordinary static +// property-descriptor-returning function (customarily namespaced under +// the relevant Thing class's static `composite` field) - that lets you +// reuse it in multiple compositions later on. +// +// Compositional steps are implemented with "continuation passing style", +// meaning the connection to the next link on the chain is passed right to +// each step's compute (or transform) function, and the implementation gets +// to decide whether to continue on that chain or exit early by returning +// some other value. +// +// Every step along the chain, apart from the base at the bottom, has to +// have the {compose: true} step. That means its compute() or transform() +// function will be passed an extra argument at the end, `continuation`. +// To provide new dependencies to items further down the chain, just pass +// them directly to this continuation() function, customarily with a hash +// ('#') prefixing each name - for example: +// +// compute({..some dependencies..}, continuation) { +// return continuation({ +// '#excitingProperty': (..a value made from dependencies..), +// }); +// } +// +// Performing an early exit is as simple as returning some other value, +// instead of the continuation. You may also use `continuation.exit(value)` +// to perform the exact same kind of early exit - it's just a different +// syntax that might fit in better in certain longer compositions. +// +// It may be fine to simply provide new dependencies under a hard-coded +// name, such as '#excitingProperty' above, but if you're writing a utility +// that dynamically returns the compositional step and you suspect you +// might want to use this step multiple times in a single composition, +// it's customary to accept a name for the result. +// +// Here's a detailed example showing off early exit, dynamically operating +// on a provided dependency name, and then providing a result in another +// also-provided dependency name: +// +// static Thing.composite.withResolvedContribs = ({ +// from: contribsByRefDependency, +// to: outputDependency, +// }) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: [contribsByRefDependency, 'artistData'], +// compute({ +// [contribsByRefDependency]: contribsByRef, +// artistData, +// }, continuation) { +// if (!artistData) return null; /* early exit! */ +// return continuation({ +// [outputDependency]: /* this is the important part */ +// (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// And how you might work that into a composition: +// +// static Track[Thing.getPropertyDescriptors].coverArtists = +// Thing.composite.from([ +// Track.composite.doSomethingWhichMightEarlyExit(), +// Thing.composite.withResolvedContribs({ +// from: 'coverArtistContribsByRef', +// to: '#coverArtistContribs', +// }), +// +// { +// flags: {expose: true}, +// expose: { +// dependencies: ['#coverArtistContribs'], +// compute({'#coverArtistContribs': coverArtistContribs}) { +// return coverArtistContribs.map(({who}) => who); +// }, +// }, +// }, +// ]); +// +// One last note! A super common code pattern when creating more complex +// compositions is to have several steps which *only* expose and compose. +// As a syntax shortcut, you can skip the outer section. It's basically +// like writing out just the {expose: {...}} part. Remember that this +// indicates that the step you're defining is compositional, so you have +// to specify the flags manually for the base, even if this property isn't +// going to get an {update: true} flag. +// +// == Cache-safe dependency names: == +// +// [Disclosure: The caching engine hasn't actually been implemented yet. +// As such, this section is subject to change, and simply provides sound +// forward-facing advice and interfaces.] +// +// It's a good idea to write individual compositional steps in such a way +// that they're "cache-safe" - meaning the same input (dependency) values +// will always result in the same output (continuation or early exit). +// +// In order to facilitate this, compositional step descriptors may specify +// unique `mapDependencies`, `mapContinuation`, and `options` values. +// +// Consider the `withResolvedContribs` example adjusted to make use of +// two of these options below: +// +// static Thing.composite.withResolvedContribs = ({ +// from: contribsByRefDependency, +// to: outputDependency, +// }) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: ['artistData'], +// mapDependencies: {contribsByRef: contribsByRefDependency}, +// mapContinuation: {outputDependency}, +// compute({ +// contribsByRef, /* no longer in square brackets */ +// artistData, +// }, continuation) { +// if (!artistData) return null; +// return continuation({ +// outputDependency: /* no longer in square brackets */ +// (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// With a little destructuring and restructuring JavaScript sugar, the +// above can be simplified some more: +// +// static Thing.composite.withResolvedContribs = ({from, to}) => ({ +// flags: {expose: true, compose: true}, +// expose: { +// dependencies: ['artistData'], +// mapDependencies: {from}, +// mapContinuation: {to}, +// compute({artistData, from: contribsByRef}, continuation) { +// if (!artistData) return null; +// return continuation({ +// to: (..resolve contributions one way or another..), +// }); +// }, +// }, +// }); +// +// These two properties let you separate the name-mapping behavior (for +// dependencies and the continuation) from the main body of the compute +// function. That means the compute function will *always* get inputs in +// the same form (dependencies 'artistData' and 'from' above), and will +// *always* provide its output in the same form (early return or 'to'). +// +// Thanks to that, this `compute` function is cache-safe! Its outputs can +// be cached corresponding to each set of mapped inputs. So it won't matter +// whether the `from` dependency is named `coverArtistContribsByRef` or +// `contributorContribsByRef` or something else - the compute function +// doesn't care, and only expects that value to be provided via its `from` +// argument. Likewise, it doesn't matter if the output should be sent to +// '#coverArtistContribs` or `#contributorContribs` or some other name; +// the mapping is handled automatically outside, and compute will always +// output its value to the continuation's `to`. +// +// Note that `mapDependencies` and `mapContinuation` should be objects of +// the same "shape" each run - that is, the values will change depending on +// outside context, but the keys are always the same. You shouldn't use +// `mapDependencies` to dynamically select more or fewer dependencies. +// If you need to dynamically select a range of dependencies, just specify +// them in the `dependencies` array like usual. The caching engine will +// understand that differently named `dependencies` indicate separate +// input-output caches should be used. +// +// The 'options' property makes it possible to specify external arguments +// that fundamentally change the behavior of the `compute` function, while +// still remaining cache-safe. It indicates that the caching engine should +// use a completely different input-to-output cache for each permutation +// of the 'options' values. This way, those functions are still cacheable +// at all; they'll just be cached separately for each set of option values. +// Values on the 'options' property will always be provided in compute's +// dependencies under '#options' (to avoid name conflicts with other +// dependencies). +// +// == To compute or to transform: == +// +// A compositional step can work directly on a property's stored update +// value, transforming it in place and either early exiting with it or +// passing it on (via continuation) to the next item(s) in the +// compositional step list. (If needed, these can provide dependencies +// the same way as compute functions too - just pass that object after +// the updated (or same) transform value in your call to continuation().) +// +// But in order to make them more versatile, compositional steps have an +// extra trick up their sleeve. If a compositional step implements compute +// and *not* transform, it can still be used in a composition targeting a +// property which updates! These retain their full dependency-providing and +// early exit functionality - they just won't be provided the update value. +// If a compute-implementing step returns its continuation, then whichever +// later step (or the base) next implements transform() will receive the +// update value that had so far been running - as well as any dependencies +// the compute() step returned, of course! +// +// Please note that a compositional step which transforms *should not* +// specify, in its flags, {update: true}. Just provide the transform() +// function in its expose descriptor; it will be automatically detected +// and used when appropriate. +// +// It's actually possible for a step to specify both transform and compute, +// in which case the transform() implementation will only be selected if +// the composition's base is {update: true}. It's not exactly known why you +// would want to specify unique-but-related transform and compute behavior, +// but the basic possibility was too cool to skip out on. +// +// == Nesting compositions: == +// +// Compositional steps are so convenient that you just might want to bundle +// them together, and form a whole new step-shaped unit of its own! +// +// In order to allow for this while helping to ensure internal dependencies +// remain neatly isolated from the composition which nests your bundle, +// the Thing.composite.from() function will accept and adapt to a base that +// specifies the {compose: true} flag, just like the steps preceding it. +// +// The continuation function that gets provided to the base will be mildly +// special - after all, nothing follows the base within the composition's +// own list! Instead of appending dependencies alongside any previously +// provided ones to be available to the next step, the base's continuation +// function should be used to define "exports" of the composition as a +// whole. It's similar to the usual behavior of the continuation, just +// expanded to the scope of the composition instead of following steps. +// +// For example, suppose your composition (which you expect to include in +// other compositions) brings about several private, hash-prefixed +// dependencies to contribute to its own results. Those dependencies won't +// end up "bleeding" into the dependency list of whichever composition is +// nesting this one - they will totally disappear once all the steps in +// the nested composition have finished up. +// +// To "export" the results of processing all those dependencies (provided +// that's something you want to do and this composition isn't used purely +// for a conditional early-exit), you'll want to define them in the +// continuation passed to the base. (Customarily, those should start with +// a hash just like the exports from any other compositional step; they're +// still dynamically provided dependencies!) +// +// Another way to "export" dependencies is by using calling *any* step's +// `continuation.raise()` function. This is sort of like early exiting, +// but instead of quitting out the whole entire property, it will just +// break out of the current, nested composition's list of steps, acting +// as though the composition had finished naturally. The dependencies +// passed to `raise` will be the ones which get exported. +// +// Since `raise` is another way to export dependencies, if you're using +// dynamic export names, you should specify `mapContinuation` on the step +// calling `continuation.raise` as well. +// +// An important note on `mapDependencies` here: A nested composition gets +// free access to all the ordinary properties defined on the thing it's +// working on, but if you want it to depend on *private* dependencies - +// ones prefixed with '#' - which were provided by some other compositional +// step preceding wherever this one gets nested, then you *have* to use +// `mapDependencies` to gain access. Check out the section on "cache-safe +// dependency names" for information on this syntax! +// +// Also - on rare occasion - you might want to make a reusable composition +// that itself causes the composition *it's* nested in to raise. If that's +// the case, give `composition.raiseAbove()` a go! This effectively means +// kicking out of *two* layers of nested composition - the one including +// the step with the `raiseAbove` call, and the composition which that one +// is nested within. You don't need to use `raiseAbove` if the reusable +// utility function just returns a single compositional step, but if you +// want to make use of other compositional steps, it gives you access to +// the same conditional-raise capabilities. +// +// Have some syntax sugar! Since nested compositions are defined by having +// the base be {compose: true}, the composition will infer as much if you +// don't specifying the base's flags at all. Simply use the same shorthand +// syntax as for other compositional steps, and it'll work out cleanly! +// + +import {empty, filterProperties, openAggregate} from '#sugar'; + +import Thing from './thing.js'; + +export {compositeFrom as from}; +function compositeFrom(firstArg, secondArg) { + const debug = fn => { + if (compositeFrom.debug === true) { + const label = + (annotation + ? color.dim(`[composite: ${annotation}]`) + : color.dim(`[composite]`)); + const result = fn(); + if (Array.isArray(result)) { + console.log(label, ...result.map(value => + (typeof value === 'object' + ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + : value))); + } else { + console.log(label, result); + } + } + }; + + let annotation, composition; + if (typeof firstArg === 'string') { + [annotation, composition] = [firstArg, secondArg]; + } else { + [annotation, composition] = [null, firstArg]; + } + + const base = composition.at(-1); + const steps = composition.slice(); + + const aggregate = openAggregate({ + message: + `Errors preparing Thing.composite.from() composition` + + (annotation ? ` (${annotation})` : ''), + }); + + const baseExposes = + (base.flags + ? base.flags.expose + : true); + + const baseUpdates = + (base.flags + ? base.flags.update + : false); + + const baseComposes = + (base.flags + ? base.flags.compose + : true); + + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); + } + + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; + const message = + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); + + aggregate.nest({message}, ({push}) => { + if (step.flags) { + let flagsErrored = false; + + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; + } + + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; + } + + if (flagsErrored) { + return; + } + } + + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; + } + + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); + } + }); + } + + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + + aggregate.close(); + + const constructedDescriptor = {}; + + if (annotation) { + constructedDescriptor.annotation = annotation; + } + + constructedDescriptor.flags = { + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, + }; + + if (baseUpdates) { + constructedDescriptor.update = base.update; + } + + if (baseExposes) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); + + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); + + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { + const filteredDependencies = + (dependencies + ? filterProperties(availableDependencies, dependencies) + : {}); + + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; + } + } + + if (options) { + filteredDependencies['#options'] = options; + } + + return filteredDependencies; + } + + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { + return continuationAssignment; + } + + const assignDependencies = {}; + + for (const [from, to] of Object.entries(mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } + + return assignDependencies; + } + + function _prepareContinuation(callingTransformForThisStep) { + const continuationStorage = { + returnedWith: null, + providedDependencies: undefined, + providedValue: undefined, + }; + + const continuation = + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = returnWith; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = returnWith; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); + } + + return {continuation, continuationStorage}; + } + + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; + + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); + + const availableDependencies = {...initialDependencies}; + + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; + + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); + + debug(() => [ + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); + + const result = + (callingTransformForThisStep + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); + + if (result !== continuationSymbol) { + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); + } + + debug(() => color.bright(`end composition - exit (inferred)`)); + + return result; + } + + const {returnedWith} = continuationStorage; + + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; + + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); + + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } + } + + const {providedValue, providedDependencies} = continuationStorage; + + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); + + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); + + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; + + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } + + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } + + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + + switch (returnedWith) { + case 'raise': + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); + + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); + + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } + } + } + + const transformFn = + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); + + const computeFn = + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { + expose.transform = transformFn; + } else { + expose.compute = computeFn; + } + } + + return constructedDescriptor; +} + +// Evaluates a function with composite debugging enabled, turns debugging +// off again, and returns the result of the function. This is mostly syntax +// sugar, but also helps avoid unit tests avoid accidentally printing debug +// info for a bunch of unrelated composites (due to property enumeration +// when displaying an unexpected result). Use as so: +// +// Without debugging: +// t.same(thing.someProp, value) +// +// With debugging: +// t.same(Thing.composite.debug(() => thing.someProp), value) +// +export function debug(fn) { + compositeFrom.debug = true; + const value = fn(); + compositeFrom.debug = false; + return value; +} + +// -- Compositional steps for compositions to nest -- + +// Provides dependencies exactly as they are (or null if not defined) to the +// continuation. Although this can *technically* be used to alias existing +// dependencies to some other name within the middle of a composition, it's +// intended to be used only as a composition's base - doing so makes the +// composition as a whole suitable as a step in some other composition, +// providing the listed (internal) dependencies to later steps just like +// other compositional steps. +export {_export as export}; +function _export(mapping) { + const mappingEntries = Object.entries(mapping); + + return { + annotation: `Thing.composite.export`, + flags: {expose: true, compose: true}, + + expose: { + options: {mappingEntries}, + dependencies: Object.values(mapping), + + compute({'#options': {mappingEntries}, ...dependencies}, continuation) { + const exports = {}; + + // Note: This is slightly different behavior from filterProperties, + // as defined in sugar.js, which doesn't fall back to null for + // properties which don't exist on the original object. + for (const [exportKey, dependencyKey] of mappingEntries) { + exports[exportKey] = + (Object.hasOwn(dependencies, dependencyKey) + ? dependencies[dependencyKey] + : null); + } + + return continuation.raise(exports); + } + }, + }; +} + +// -- Compositional steps for top-level property descriptors -- + +// Exposes a dependency exactly as it is; this is typically the base of a +// composition which was created to serve as one property's descriptor. +// Since this serves as a base, specify a value for {update} to indicate +// that the property as a whole updates (and some previous compositional +// step works with that update value). Set {update: true} to only enable +// the update flag, or set update to an object to specify a descriptor +// (e.g. for custom value validation). +// +// Please note that this *doesn't* verify that the dependency exists, so +// if you provide the wrong name or it hasn't been set by a previous +// compositional step, the property will be exposed as undefined instead +// of null. +// +export function exposeDependency(dependency, { + update = false, +} = {}) { + return { + annotation: `Thing.composite.exposeDependency`, + flags: {expose: true, update: !!update}, + + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, + + update: + (typeof update === 'object' + ? update + : null), + }; +} + +// Exposes a constant value exactly as it is; like exposeDependency, this +// is typically the base of a composition serving as a particular property +// descriptor. It generally follows steps which will conditionally early +// exit with some other value, with the exposeConstant base serving as the +// fallback default value. Like exposeDependency, set {update} to true or +// an object to indicate that the property as a whole updates. +export function exposeConstant(value, { + update = false, +} = {}) { + return { + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, + + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, + + update: + (typeof update === 'object' + ? update + : null), + }; +} + +// Checks the availability of a dependency or the update value and provides +// the result to later steps under '#availability' (by default). This is +// mainly intended for use by the more specific utilities, which you should +// consider using instead. Customize {mode} to select one of these modes, +// or leave unset and default to 'null': +// +// * 'null': Check that the value isn't null. +// * 'empty': Check that the value is neither null nor an empty array. +// * 'falsy': Check that the value isn't false when treated as a boolean +// (nor an empty array). Keep in mind this will also be false +// for values like zero and the empty string! +// +export function withResultOfAvailabilityCheck({ + fromUpdateValue, + fromDependency, + mode = 'null', + to = '#availability', +}) { + if (!['null', 'empty', 'falsy'].includes(mode)) { + throw new TypeError(`Expected mode to be null, empty, or falsy`); + } + + if (fromUpdateValue && fromDependency) { + throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); + } + + if (!fromUpdateValue && !fromDependency) { + throw new TypeError(`Missing dependency name (or fromUpdateValue)`); + } + + const checkAvailability = (value, mode) => { + switch (mode) { + case 'null': return value !== null; + case 'empty': return !empty(value); + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); + default: return false; + } + }; + + if (fromDependency) { + return { + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {from: fromDependency}, + mapContinuation: {to}, + options: {mode}, + compute: ({from, '#options': {mode}}, continuation) => + continuation({to: checkAvailability(from, mode)}), + }, + }; + } else { + return { + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, + flags: {expose: true, compose: true}, + expose: { + mapContinuation: {to}, + options: {mode}, + transform: (value, {'#options': {mode}}, continuation) => + continuation(value, {to: checkAvailability(value, mode)}), + }, + }; + } +} + +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options! +export function exposeDependencyOrContinue(dependency, { + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), + }, + ]); +} + +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. See withResultOfAvailabilityCheck +// for {mode} options! +export function exposeUpdateValueOrContinue({ + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + withResultOfAvailabilityCheck({ + fromUpdateValue: true, + mode, + }), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + transform: (value, {}, continuation) => + continuation.exit(value), + }, + ]); +} + +// Early exits if an availability check has failed. +// This is for internal use only - use `earlyExitWithoutDependency` or +// `earlyExitWIthoutUpdateValue` instead. +export function earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), + }, + ]); +} + +// Early exits if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + + { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + ]); +} + +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + + { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + + { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + ]); +} + +// -- Compositional steps for processing data -- + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export function withResolvedContribs({from, to}) { + return { + annotation: `Thing.composite.withResolvedContribs`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => + continuation({ + to: Thing.findArtistsFromContribs(from, artistData), + }), + }, + }; +} + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if earlyExitIfNotFound is set to true, +// if the find function doesn't match anything for the reference. +// Otherwise, the data object is provided on the output dependency; +// or null, if the reference doesn't match anything or itself was null +// to begin with. +export function withResolvedReference({ + ref, + data, + to, + find: findFunction, + earlyExitIfNotFound = false, +}) { + return compositeFrom(`Thing.composite.withResolvedReference`, [ + raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + earlyExitWithoutDependency(data), + + { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, + + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, +}) { + return compositeFrom(`Thing.common.reverseReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); +} diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ad27ca55..01aa8b1b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; +import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -359,1178 +360,5 @@ export default class Thing extends CacheableObject { .filter(({who}) => who)); } - static composite = { - // Composes multiple compositional "steps" and a "base" to form a property - // descriptor out of modular building blocks. This is an extension to the - // more general-purpose CacheableObject property descriptor syntax, and - // aims to make modular data processing - which lends to declarativity - - // much easier, without fundamentally altering much of the typical syntax - // or terminology, nor building on it to an excessive degree. - // - // Think of a composition as being a chain of steps which lead into a final - // base property, which is usually responsible for returning the value that - // will actually get exposed when the property being described is accessed. - // - // == The compositional base: == - // - // The final item in a compositional list is its base, and it identifies - // the essential qualities of the property descriptor. The compositional - // steps preceding it may exit early, in which case the expose function - // defined on the base won't be called; or they will provide dependencies - // that the base may use to compute the final value that gets exposed for - // this property. - // - // The base indicates the capabilities of the composition as a whole. - // It should be {expose: true}, since that's the only area that preceding - // compositional steps (currently) can actually influence. If it's also - // {update: true}, then the composition as a whole accepts an update value - // just like normal update-flag property descriptors - meaning it can be - // set with `thing.someProperty = value` and that value will be paseed - // into each (implementing) step's transform() function, as well as the - // base. Bases usually aren't {compose: true}, but can be - check out the - // section on "nesting compositions" for details about that. - // - // Every composition always has exactly one compositional base, and it's - // always the last item in the composition list. All items preceding it - // are compositional steps, described below. - // - // == Compositional steps: == - // - // Compositional steps are, in essence, typical property descriptors with - // the extra flag {compose: true}. They operate on existing dependencies, - // and are typically dynamically constructed by "utility" functions (but - // can also be manually declared within the step list of a composition). - // Compositional steps serve two purposes: - // - // 1. exit early, if some condition is matched, returning and exposing - // some value directly from that step instead of continuing further - // down the step list; - // - // 2. and/or provide new, dynamically created "private" dependencies which - // can be accessed by further steps down the list, or at the base at - // the bottom, modularly supplying information that will contribute to - // the final value exposed for this property. - // - // Usually it's just one of those two, but it's fine for a step to perform - // both jobs if the situation benefits. - // - // Compositional steps are the real "modular" or "compositional" part of - // this data processing style - they're designed to be combined together - // in dynamic, versatile ways, as each property demands it. You usually - // define a compositional step to be returned by some ordinary static - // property-descriptor-returning function (customarily namespaced under - // the relevant Thing class's static `composite` field) - that lets you - // reuse it in multiple compositions later on. - // - // Compositional steps are implemented with "continuation passing style", - // meaning the connection to the next link on the chain is passed right to - // each step's compute (or transform) function, and the implementation gets - // to decide whether to continue on that chain or exit early by returning - // some other value. - // - // Every step along the chain, apart from the base at the bottom, has to - // have the {compose: true} step. That means its compute() or transform() - // function will be passed an extra argument at the end, `continuation`. - // To provide new dependencies to items further down the chain, just pass - // them directly to this continuation() function, customarily with a hash - // ('#') prefixing each name - for example: - // - // compute({..some dependencies..}, continuation) { - // return continuation({ - // '#excitingProperty': (..a value made from dependencies..), - // }); - // } - // - // Performing an early exit is as simple as returning some other value, - // instead of the continuation. You may also use `continuation.exit(value)` - // to perform the exact same kind of early exit - it's just a different - // syntax that might fit in better in certain longer compositions. - // - // It may be fine to simply provide new dependencies under a hard-coded - // name, such as '#excitingProperty' above, but if you're writing a utility - // that dynamically returns the compositional step and you suspect you - // might want to use this step multiple times in a single composition, - // it's customary to accept a name for the result. - // - // Here's a detailed example showing off early exit, dynamically operating - // on a provided dependency name, and then providing a result in another - // also-provided dependency name: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: [contribsByRefDependency, 'artistData'], - // compute({ - // [contribsByRefDependency]: contribsByRef, - // artistData, - // }, continuation) { - // if (!artistData) return null; /* early exit! */ - // return continuation({ - // [outputDependency]: /* this is the important part */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // And how you might work that into a composition: - // - // static Track[Thing.getPropertyDescriptors].coverArtists = - // Thing.composite.from([ - // Track.composite.doSomethingWhichMightEarlyExit(), - // Thing.composite.withResolvedContribs({ - // from: 'coverArtistContribsByRef', - // to: '#coverArtistContribs', - // }), - // - // { - // flags: {expose: true}, - // expose: { - // dependencies: ['#coverArtistContribs'], - // compute({'#coverArtistContribs': coverArtistContribs}) { - // return coverArtistContribs.map(({who}) => who); - // }, - // }, - // }, - // ]); - // - // One last note! A super common code pattern when creating more complex - // compositions is to have several steps which *only* expose and compose. - // As a syntax shortcut, you can skip the outer section. It's basically - // like writing out just the {expose: {...}} part. Remember that this - // indicates that the step you're defining is compositional, so you have - // to specify the flags manually for the base, even if this property isn't - // going to get an {update: true} flag. - // - // == Cache-safe dependency names: == - // - // [Disclosure: The caching engine hasn't actually been implemented yet. - // As such, this section is subject to change, and simply provides sound - // forward-facing advice and interfaces.] - // - // It's a good idea to write individual compositional steps in such a way - // that they're "cache-safe" - meaning the same input (dependency) values - // will always result in the same output (continuation or early exit). - // - // In order to facilitate this, compositional step descriptors may specify - // unique `mapDependencies`, `mapContinuation`, and `options` values. - // - // Consider the `withResolvedContribs` example adjusted to make use of - // two of these options below: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {contribsByRef: contribsByRefDependency}, - // mapContinuation: {outputDependency}, - // compute({ - // contribsByRef, /* no longer in square brackets */ - // artistData, - // }, continuation) { - // if (!artistData) return null; - // return continuation({ - // outputDependency: /* no longer in square brackets */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // With a little destructuring and restructuring JavaScript sugar, the - // above can be simplified some more: - // - // static Thing.composite.withResolvedContribs = ({from, to}) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {from}, - // mapContinuation: {to}, - // compute({artistData, from: contribsByRef}, continuation) { - // if (!artistData) return null; - // return continuation({ - // to: (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // These two properties let you separate the name-mapping behavior (for - // dependencies and the continuation) from the main body of the compute - // function. That means the compute function will *always* get inputs in - // the same form (dependencies 'artistData' and 'from' above), and will - // *always* provide its output in the same form (early return or 'to'). - // - // Thanks to that, this `compute` function is cache-safe! Its outputs can - // be cached corresponding to each set of mapped inputs. So it won't matter - // whether the `from` dependency is named `coverArtistContribsByRef` or - // `contributorContribsByRef` or something else - the compute function - // doesn't care, and only expects that value to be provided via its `from` - // argument. Likewise, it doesn't matter if the output should be sent to - // '#coverArtistContribs` or `#contributorContribs` or some other name; - // the mapping is handled automatically outside, and compute will always - // output its value to the continuation's `to`. - // - // Note that `mapDependencies` and `mapContinuation` should be objects of - // the same "shape" each run - that is, the values will change depending on - // outside context, but the keys are always the same. You shouldn't use - // `mapDependencies` to dynamically select more or fewer dependencies. - // If you need to dynamically select a range of dependencies, just specify - // them in the `dependencies` array like usual. The caching engine will - // understand that differently named `dependencies` indicate separate - // input-output caches should be used. - // - // The 'options' property makes it possible to specify external arguments - // that fundamentally change the behavior of the `compute` function, while - // still remaining cache-safe. It indicates that the caching engine should - // use a completely different input-to-output cache for each permutation - // of the 'options' values. This way, those functions are still cacheable - // at all; they'll just be cached separately for each set of option values. - // Values on the 'options' property will always be provided in compute's - // dependencies under '#options' (to avoid name conflicts with other - // dependencies). - // - // == To compute or to transform: == - // - // A compositional step can work directly on a property's stored update - // value, transforming it in place and either early exiting with it or - // passing it on (via continuation) to the next item(s) in the - // compositional step list. (If needed, these can provide dependencies - // the same way as compute functions too - just pass that object after - // the updated (or same) transform value in your call to continuation().) - // - // But in order to make them more versatile, compositional steps have an - // extra trick up their sleeve. If a compositional step implements compute - // and *not* transform, it can still be used in a composition targeting a - // property which updates! These retain their full dependency-providing and - // early exit functionality - they just won't be provided the update value. - // If a compute-implementing step returns its continuation, then whichever - // later step (or the base) next implements transform() will receive the - // update value that had so far been running - as well as any dependencies - // the compute() step returned, of course! - // - // Please note that a compositional step which transforms *should not* - // specify, in its flags, {update: true}. Just provide the transform() - // function in its expose descriptor; it will be automatically detected - // and used when appropriate. - // - // It's actually possible for a step to specify both transform and compute, - // in which case the transform() implementation will only be selected if - // the composition's base is {update: true}. It's not exactly known why you - // would want to specify unique-but-related transform and compute behavior, - // but the basic possibility was too cool to skip out on. - // - // == Nesting compositions: == - // - // Compositional steps are so convenient that you just might want to bundle - // them together, and form a whole new step-shaped unit of its own! - // - // In order to allow for this while helping to ensure internal dependencies - // remain neatly isolated from the composition which nests your bundle, - // the Thing.composite.from() function will accept and adapt to a base that - // specifies the {compose: true} flag, just like the steps preceding it. - // - // The continuation function that gets provided to the base will be mildly - // special - after all, nothing follows the base within the composition's - // own list! Instead of appending dependencies alongside any previously - // provided ones to be available to the next step, the base's continuation - // function should be used to define "exports" of the composition as a - // whole. It's similar to the usual behavior of the continuation, just - // expanded to the scope of the composition instead of following steps. - // - // For example, suppose your composition (which you expect to include in - // other compositions) brings about several private, hash-prefixed - // dependencies to contribute to its own results. Those dependencies won't - // end up "bleeding" into the dependency list of whichever composition is - // nesting this one - they will totally disappear once all the steps in - // the nested composition have finished up. - // - // To "export" the results of processing all those dependencies (provided - // that's something you want to do and this composition isn't used purely - // for a conditional early-exit), you'll want to define them in the - // continuation passed to the base. (Customarily, those should start with - // a hash just like the exports from any other compositional step; they're - // still dynamically provided dependencies!) - // - // Another way to "export" dependencies is by using calling *any* step's - // `continuation.raise()` function. This is sort of like early exiting, - // but instead of quitting out the whole entire property, it will just - // break out of the current, nested composition's list of steps, acting - // as though the composition had finished naturally. The dependencies - // passed to `raise` will be the ones which get exported. - // - // Since `raise` is another way to export dependencies, if you're using - // dynamic export names, you should specify `mapContinuation` on the step - // calling `continuation.raise` as well. - // - // An important note on `mapDependencies` here: A nested composition gets - // free access to all the ordinary properties defined on the thing it's - // working on, but if you want it to depend on *private* dependencies - - // ones prefixed with '#' - which were provided by some other compositional - // step preceding wherever this one gets nested, then you *have* to use - // `mapDependencies` to gain access. Check out the section on "cache-safe - // dependency names" for information on this syntax! - // - // Also - on rare occasion - you might want to make a reusable composition - // that itself causes the composition *it's* nested in to raise. If that's - // the case, give `composition.raiseAbove()` a go! This effectively means - // kicking out of *two* layers of nested composition - the one including - // the step with the `raiseAbove` call, and the composition which that one - // is nested within. You don't need to use `raiseAbove` if the reusable - // utility function just returns a single compositional step, but if you - // want to make use of other compositional steps, it gives you access to - // the same conditional-raise capabilities. - // - // Have some syntax sugar! Since nested compositions are defined by having - // the base be {compose: true}, the composition will infer as much if you - // don't specifying the base's flags at all. Simply use the same shorthand - // syntax as for other compositional steps, and it'll work out cleanly! - // - from(firstArg, secondArg) { - const debug = fn => { - if (Thing.composite.from.debug === true) { - const label = - (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); - const result = fn(); - if (Array.isArray(result)) { - console.log(label, ...result.map(value => - (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) - : value))); - } else { - console.log(label, result); - } - } - }; - - let annotation, composition; - if (typeof firstArg === 'string') { - [annotation, composition] = [firstArg, secondArg]; - } else { - [annotation, composition] = [null, firstArg]; - } - - const base = composition.at(-1); - const steps = composition.slice(); - - const aggregate = openAggregate({ - message: - `Errors preparing Thing.composite.from() composition` + - (annotation ? ` (${annotation})` : ''), - }); - - const baseExposes = - (base.flags - ? base.flags.expose - : true); - - const baseUpdates = - (base.flags - ? base.flags.update - : false); - - const baseComposes = - (base.flags - ? base.flags.compose - : true); - - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } - - const exposeDependencies = new Set(); - - let anyStepsCompute = false; - let anyStepsTransform = false; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - const message = - `Errors in step #${i + 1}` + - (isBase ? ` (base)` : ``) + - (step.annotation ? ` (${step.annotation})` : ``); - - aggregate.nest({message}, ({push}) => { - if (step.flags) { - let flagsErrored = false; - - if (!step.flags.compose && !isBase) { - push(new TypeError(`All steps but base must compose`)); - flagsErrored = true; - } - - if (!step.flags.expose) { - push(new TypeError(`All steps must expose`)); - flagsErrored = true; - } - - if (flagsErrored) { - return; - } - } - - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose.compute; - const stepTransforms = !!expose.transform; - - if (!stepComputes && !stepTransforms) { - push(new TypeError(`Steps must provide compute or transform (or both)`)); - return; - } - - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - return; - } - - if (stepComputes) { - anyStepsCompute = true; - } - - if (stepTransforms) { - anyStepsTransform = true; - } - - // Unmapped dependencies are exposed on the final composition only if - // they're "public", i.e. pointing to update values of other properties - // on the CacheableObject. - for (const dependency of expose.dependencies ?? []) { - if (typeof dependency === 'string' && dependency.startsWith('#')) { - continue; - } - - exposeDependencies.add(dependency); - } - - // Mapped dependencies are always exposed on the final composition. - // These are explicitly for reading values which are named outside of - // the current compositional step. - for (const dependency of Object.values(expose.mapDependencies ?? {})) { - exposeDependencies.add(dependency); - } - }); - } - - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); - } - } - } - - aggregate.close(); - - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } - - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; - - if (baseUpdates) { - constructedDescriptor.update = base.update; - } - - if (baseExposes) { - const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); - - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - - function _prepareContinuation(callingTransformForThisStep) { - const continuationStorage = { - returnedWith: null, - providedDependencies: undefined, - providedValue: undefined, - }; - - const continuation = - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.exit = (providedValue) => { - continuationStorage.returnedWith = 'exit'; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - }; - - if (baseComposes) { - const makeRaiseLike = returnWith => - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); - } - - return {continuation, continuationStorage}; - } - - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; - - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); - - const availableDependencies = {...initialDependencies}; - - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); - - const expose = - (step.flags - ? step.expose - : step); - - const callingTransformForThisStep = - expectingTransform && expose.transform; - - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); - - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); - - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } - - debug(() => color.bright(`end composition - exit (inferred)`)); - - return result; - } - - const {returnedWith} = continuationStorage; - - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; - - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); - - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } - } - - const {providedValue, providedDependencies} = continuationStorage; - - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - - debug(() => { - const base = `step #${i+1} - result: ` + returnedWith; - const parts = []; - - if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } - } - - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } - - if (empty(parts)) { - return base; - } else { - return [base + ' ->', ...parts]; - } - }); - - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); - - case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); - - case 'continuation': - if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); - return continuationIfApplicable(...continuationArgs); - } else { - Object.assign(availableDependencies, continuingWithDependencies); - break; - } - } - } - } - - const transformFn = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); - - const computeFn = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - - if (baseComposes) { - if (anyStepsTransform) expose.transform = transformFn; - if (anyStepsCompute) expose.compute = computeFn; - } else if (baseUpdates) { - expose.transform = transformFn; - } else { - expose.compute = computeFn; - } - } - - return constructedDescriptor; - }, - - // Evaluates a function with composite debugging enabled, turns debugging - // off again, and returns the result of the function. This is mostly syntax - // sugar, but also helps avoid unit tests avoid accidentally printing debug - // info for a bunch of unrelated composites (due to property enumeration - // when displaying an unexpected result). Use as so: - // - // Without debugging: - // t.same(thing.someProp, value) - // - // With debugging: - // t.same(Thing.composite.debug(() => thing.someProp), value) - // - debug(fn) { - Thing.composite.from.debug = true; - const value = fn(); - Thing.composite.from.debug = false; - return value; - }, - - // -- Compositional steps for compositions to nest -- - - // Provides dependencies exactly as they are (or null if not defined) to the - // continuation. Although this can *technically* be used to alias existing - // dependencies to some other name within the middle of a composition, it's - // intended to be used only as a composition's base - doing so makes the - // composition as a whole suitable as a step in some other composition, - // providing the listed (internal) dependencies to later steps just like - // other compositional steps. - export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `Thing.composite.export`, - flags: {expose: true, compose: true}, - - expose: { - options: {mappingEntries}, - dependencies: Object.values(mapping), - - compute({'#options': {mappingEntries}, ...dependencies}, continuation) { - const exports = {}; - - // Note: This is slightly different behavior from filterProperties, - // as defined in sugar.js, which doesn't fall back to null for - // properties which don't exist on the original object. - for (const [exportKey, dependencyKey] of mappingEntries) { - exports[exportKey] = - (Object.hasOwn(dependencies, dependencyKey) - ? dependencies[dependencyKey] - : null); - } - - return continuation.raise(exports); - } - }, - }; - }, - - // -- Compositional steps for top-level property descriptors -- - - // Exposes a dependency exactly as it is; this is typically the base of a - // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify a value for {update} to indicate - // that the property as a whole updates (and some previous compositional - // step works with that update value). Set {update: true} to only enable - // the update flag, or set update to an object to specify a descriptor - // (e.g. for custom value validation). - // - // Please note that this *doesn't* verify that the dependency exists, so - // if you provide the wrong name or it hasn't been set by a previous - // compositional step, the property will be exposed as undefined instead - // of null. - // - exposeDependency(dependency, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, - - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Exposes a constant value exactly as it is; like exposeDependency, this - // is typically the base of a composition serving as a particular property - // descriptor. It generally follows steps which will conditionally early - // exit with some other value, with the exposeConstant base serving as the - // fallback default value. Like exposeDependency, set {update} to true or - // an object to indicate that the property as a whole updates. - exposeConstant(value, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, - - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Checks the availability of a dependency or the update value and provides - // the result to later steps under '#availability' (by default). This is - // mainly intended for use by the more specific utilities, which you should - // consider using instead. Customize {mode} to select one of these modes, - // or leave unset and default to 'null': - // - // * 'null': Check that the value isn't null. - // * 'empty': Check that the value is neither null nor an empty array. - // * 'falsy': Check that the value isn't false when treated as a boolean - // (nor an empty array). Keep in mind this will also be false - // for values like zero and the empty string! - // - withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - mode = 'null', - to = '#availability', - }) { - if (!['null', 'empty', 'falsy'].includes(mode)) { - throw new TypeError(`Expected mode to be null, empty, or falsy`); - } - - if (fromUpdateValue && fromDependency) { - throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); - } - - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } - - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; - - if (fromDependency) { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {to}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {to}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), - }, - }; - } - }, - - // Exposes a dependency as it is, or continues if it's unavailable. - // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue(dependency, { - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); - }, - - // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. See withResultOfAvailabilityCheck - // for {mode} options! - exposeUpdateValueOrContinue({ - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - transform: (value, {}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if an availability check fails. - // This is for internal use only - use `earlyExitWithoutDependency` or - // `earlyExitWIthoutUpdateValue` instead. - earlyExitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, - }) { - return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency(dependency, { - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Early exits if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutUpdateValue({ - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Raises if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutDependency(dependency, { - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // Raises if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // -- Compositional steps for processing data -- - - // 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. - withResolvedContribs({from, to}) { - return { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }; - }, - - // Resolves a reference by using the provided find function to match it - // within the provided thingData dependency. This will early exit if the - // data dependency is null, or, if earlyExitIfNotFound is set to true, - // if the find function doesn't match anything for the reference. - // Otherwise, the data object is provided on the output dependency; - // or null, if the reference doesn't match anything or itself was null - // to begin with. - withResolvedReference({ - ref, - data, - to, - find: findFunction, - earlyExitIfNotFound = false, - }) { - return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - Thing.composite.earlyExitWithoutDependency(data), - - { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, - - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, - }, - ]); - }, - - // Check out the info on Thing.common.reverseReferenceList! - // This is its composable form. - withReverseReferenceList({ - data, - to = '#reverseReferenceList', - refList: refListProperty, - }) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {to}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ]); - }, - }; + static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 6f242fc864028a12321255ba04a88c6190801510 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 14:34:12 -0300 Subject: data: isolate withResolvedContribs internal behavior --- src/data/things/composite.js | 26 ++++++++++++++++++++------ src/data/things/thing.js | 30 ++++++++++-------------------- 2 files changed, 30 insertions(+), 26 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1be60cd1..bf2d11ea 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -331,6 +331,7 @@ // syntax as for other compositional steps, and it'll work out cleanly! // +import find from '#find'; import {empty, filterProperties, openAggregate} from '#sugar'; import Thing from './thing.js'; @@ -1102,20 +1103,33 @@ export function raiseWithoutUpdateValue({ // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { - return { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ + Thing.composite.earlyExitWithoutDependency('artistData', { + value: [], + }), - expose: { + Thing.composite.raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { dependencies: ['artistData'], mapDependencies: {from}, mapContinuation: {to}, compute: ({artistData, from}, continuation) => continuation({ - to: Thing.findArtistsFromContribs(from, artistData), + to: + from + .map(({who, what}) => ({ + who: find.artist(who, artistData, {mode: 'quiet'}), + what, + })) + .filter(({who}) => who), }), }, - }; + ]); } // Resolves a reference by using the provided find function to match it diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 01aa8b1b..9bfed080 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -249,14 +249,16 @@ export default class Thing extends CacheableObject { // filtered out. (So if the list is all empty, chances are that either the // reference list is somehow messed up, or artistData isn't being provided // properly.) - dynamicContribs: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - Thing.findArtistsFromContribs(contribsByRef, artistData), - }, - }), + dynamicContribs(contribsByRefProperty) { + return Thing.composite.from(`Thing.common.dynamicContribs`, [ + Thing.composite.withResolvedContribs({ + from: contribsByRefProperty, + to: '#contribs', + }), + + Thing.composite.exposeDependency('#contribs'), + ]); + }, // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. @@ -348,17 +350,5 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - static findArtistsFromContribs(contribsByRef, artistData) { - if (empty(contribsByRef)) return null; - - return ( - contribsByRef - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who)); - } - static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 117f1e6b707dfe102b968e421b21906d03100dc8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 14:57:16 -0300 Subject: data: new withResolvedReferenceList utility --- src/data/things/composite.js | 107 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 19 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bf2d11ea..18a5f434 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,15 @@ +import find from '#find'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + empty, + filterProperties, + openAggregate, + stitchArrays, +} from '#sugar'; + +import Thing from './thing.js'; + // Composes multiple compositional "steps" and a "base" to form a property // descriptor out of modular building blocks. This is an extension to the // more general-purpose CacheableObject property descriptor syntax, and @@ -331,11 +343,6 @@ // syntax as for other compositional steps, and it'll work out cleanly! // -import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; - -import Thing from './thing.js'; - export {compositeFrom as from}; function compositeFrom(firstArg, secondArg) { const debug = fn => { @@ -1104,10 +1111,6 @@ export function raiseWithoutUpdateValue({ // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.earlyExitWithoutDependency('artistData', { - value: [], - }), - Thing.composite.raiseWithoutDependency(from, { mode: 'empty', map: {to}, @@ -1115,20 +1118,32 @@ export function withResolvedContribs({from, to}) { }), { - dependencies: ['artistData'], mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => + compute: ({from}, continuation) => continuation({ - to: - from - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who), + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), }), }, + + withResolvedReferenceList({ + refList: '#whoByRef', + data: 'artistData', + to: '#who', + find: find.artist, + notFoundMode: 'null', + }), + + { + dependencies: ['#who', '#what'], + mapContinuation: {to}, + compute({'#who': who, '#what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + to: stitchArrays({who, what}), + }); + }, + }, ]); } @@ -1168,6 +1183,60 @@ export function withResolvedReference({ ]); } +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export function withResolvedReferenceList({ + refList, + data, + to, + find: findFunction, + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(refList, { + map: {to}, + raise: [], + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {refList, data}, + mapContinuation: {matches: to}, + + compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + const matches = + refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); + + if (!matches.includes(null)) { + return continuation.raise({matches}); + } + + switch (notFoundMode) { + case 'filter': + matches = matches.filter(value => value !== null); + return contination.raise({matches}); + + case 'exit': + return continuation.exit([]); + + case 'null': + return continuation.raise({matches}); + } + }, + }, + ]); +} + // Check out the info on Thing.common.reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ -- cgit 1.3.0-6-gf8a5 From 91624e5d61a1473e143bad8860c8c2ccffec38fe Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:10:48 -0300 Subject: data: misc. eslint-caught fixes in composite.js --- src/data/things/composite.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 18a5f434..4f1abdb4 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,6 @@ +import {inspect} from 'node:util'; + +import {color} from '#cli'; import find from '#find'; import {filterMultipleArrays} from '#wiki-data'; @@ -482,11 +485,11 @@ function compositeFrom(firstArg, secondArg) { if (!baseComposes) { if (baseUpdates) { if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); + aggregate.push(new TypeError(`Expected at least one step to transform`)); } } else { if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); + aggregate.push(new TypeError(`Expected at least one step to compute`)); } } } @@ -1087,8 +1090,8 @@ export function raiseWithoutUpdateValue({ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - mapDependencies: {availability}, - compute: ({availability}, continuation) => + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => (availability ? continuation.raise() : continuation()), @@ -1214,7 +1217,7 @@ export function withResolvedReferenceList({ mapContinuation: {matches: to}, compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { - const matches = + let matches = refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { @@ -1224,7 +1227,7 @@ export function withResolvedReferenceList({ switch (notFoundMode) { case 'filter': matches = matches.filter(value => value !== null); - return contination.raise({matches}); + return continuation.raise({matches}); case 'exit': return continuation.exit([]); -- cgit 1.3.0-6-gf8a5 From 137bd813980d77441a86303ac6c04b61d9ccb8da Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:14:37 -0300 Subject: data: isolate internals of dynamicThingsFromReferenceList --- src/data/things/composite.js | 2 +- src/data/things/thing.js | 26 ++++++++++---------------- 2 files changed, 11 insertions(+), 17 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4f1abdb4..e930e228 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1207,7 +1207,7 @@ export function withResolvedReferenceList({ raiseWithoutDependency(refList, { map: {to}, - raise: [], + raise: {to: []}, mode: 'empty', }), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9bfed080..9f77c3fc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -194,26 +194,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. dynamicThingsFromReferenceList( - refs, + refList, data, findFunction ) { return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(refs, {value: []}), - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - flags: {expose: true}, - expose: { - mapDependencies: {refs, data}, - options: {findFunction}, - - compute: ({refs, data, '#options': {findFunction}}) => - refs - .map(ref => findFunction(ref, data, {mode: 'quiet'})) - .filter(Boolean), - }, - }, + Thing.composite.withResolvedReferenceList({ + refList, + data, + to: '#things', + find: findFunction, + notFoundMode: 'filter', + }), + + Thing.composite.exposeDependency('#things'), ]); }, -- cgit 1.3.0-6-gf8a5 From 2d7c536ee91a8f5bf8f16db1fc2d0a4d8bb4fc85 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:22:58 -0300 Subject: data: dynamicThingsFromReferenceList -> resolvedReferenceList --- src/data/things/album.js | 13 +++++++++++-- src/data/things/composite.js | 12 ++++++------ src/data/things/flash.js | 16 ++++++++++------ src/data/things/group.js | 16 ++++++++++------ src/data/things/homepage-layout.js | 10 +++++----- src/data/things/thing.js | 12 ++++++------ src/data/things/track.js | 18 +++++++++++++++--- src/data/things/wiki-info.js | 6 +++++- 8 files changed, 68 insertions(+), 35 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 06982903..81f04f70 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -128,8 +128,17 @@ export class Album extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group), - artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + groups: Thing.common.resolvedReferenceList({ + list: 'groupsByRef', + data: 'groupData', + find: find.group, + }), + + artTags: Thing.common.resolvedReferenceList({ + list: 'artTagsByRef', + data: 'artTagData', + find: find.artTag, + }), hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e930e228..7f3463cf 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1130,7 +1130,7 @@ export function withResolvedContribs({from, to}) { }, withResolvedReferenceList({ - refList: '#whoByRef', + list: '#whoByRef', data: 'artistData', to: '#who', find: find.artist, @@ -1192,7 +1192,7 @@ export function withResolvedReference({ // it will filter out references which don't match, but this can be changed // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). export function withResolvedReferenceList({ - refList, + list, data, to, find: findFunction, @@ -1205,7 +1205,7 @@ export function withResolvedReferenceList({ return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), - raiseWithoutDependency(refList, { + raiseWithoutDependency(list, { map: {to}, raise: {to: []}, mode: 'empty', @@ -1213,12 +1213,12 @@ export function withResolvedReferenceList({ { options: {findFunction, notFoundMode}, - mapDependencies: {refList, data}, + mapDependencies: {list, data}, mapContinuation: {matches: to}, - compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = - refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { return continuation.raise({matches}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 3f870c51..baef23d8 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -67,11 +67,11 @@ export class Flash extends Thing { contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), - featuredTracks: Thing.common.dynamicThingsFromReferenceList( - 'featuredTracksByRef', - 'trackData', - find.track - ), + featuredTracks: Thing.common.resolvedReferenceList({ + list: 'featuredTracksByRef', + data: 'trackData', + find: find.track, + }), act: { flags: {expose: true}, @@ -141,6 +141,10 @@ export class FlashAct extends Thing { // Expose only - flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash), + flashes: Thing.common.resolvedReferenceList({ + list: 'flashesByRef', + data: 'flashData', + find: find.flash, + }), }) } diff --git a/src/data/things/group.js b/src/data/things/group.js index f552b8f3..d04fcf56 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -26,7 +26,11 @@ export class Group extends Thing { // Expose only - featuredAlbums: Thing.common.dynamicThingsFromReferenceList('featuredAlbumsByRef', 'albumData', find.album), + featuredAlbums: Thing.common.resolvedReferenceList({ + list: 'featuredAlbumsByRef', + data: 'albumData', + find: find.album, + }), descriptionShort: { flags: {expose: true}, @@ -88,10 +92,10 @@ export class GroupCategory extends Thing { // Expose only - groups: Thing.common.dynamicThingsFromReferenceList( - 'groupsByRef', - 'groupData', - find.group - ), + groups: Thing.common.resolvedReferenceList({ + list: 'groupsByRef', + data: 'groupData', + find: find.group, + }), }); } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index ec9e9556..cbdcb99a 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -125,10 +125,10 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { find.group ), - sourceAlbums: Thing.common.dynamicThingsFromReferenceList( - 'sourceAlbumsByRef', - 'albumData', - find.album - ), + sourceAlbums: Thing.common.resolvedReferenceList({ + list: 'sourceAlbumsByRef', + data: 'albumData', + find: find.album, + }), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9f77c3fc..f36b08bc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,14 +193,14 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList( - refList, + resolvedReferenceList({ + list, data, - findFunction - ) { - return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + find: findFunction, + }) { + return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - refList, + list, data, to: '#things', find: findFunction, diff --git a/src/data/things/track.js b/src/data/things/track.js index bf56a6dd..733c81c9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -250,15 +250,27 @@ export class Track extends Thing { referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), - Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track), + Thing.common.resolvedReferenceList({ + list: 'referencedTracksByRef', + data: 'trackData', + find: find.track, + }), ]), sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), - Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track), + Thing.common.resolvedReferenceList({ + list: 'sampledTracksByRef', + data: 'trackData', + find: find.track, + }), ]), - artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag), + artTags: Thing.common.resolvedReferenceList({ + list: 'artTagsByRef', + data: 'artTagData', + find: find.artTag, + }), // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index e8279987..d6790c55 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -59,6 +59,10 @@ export class WikiInfo extends Thing { // Expose only - divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group), + divideTrackListsByGroups: Thing.common.resolvedReferenceList({ + list: 'divideTrackListsByGroupsByRef', + data: 'groupData', + find: find.group, + }), }); } -- cgit 1.3.0-6-gf8a5 From 007c70642a60ed83bd840f550aa06563d4ba6a99 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:31:41 -0300 Subject: data: reverseReferenceList refList -> list --- src/data/things/composite.js | 2 +- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7f3463cf..138814d9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1244,8 +1244,8 @@ export function withResolvedReferenceList({ // This is its composable form. export function withReverseReferenceList({ data, + list: refListProperty, to = '#reverseReferenceList', - refList: refListProperty, }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f36b08bc..915474d4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -275,10 +275,10 @@ export default class Thing extends CacheableObject { // wiki data provided, not the requesting Thing itself. reverseReferenceList({ data, - refList, + list, }) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), ]); }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 733c81c9..87e796b9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -287,7 +287,7 @@ export class Track extends Thing { featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', - refList: 'featuredTracks', + list: 'featuredTracks', }), }); @@ -559,8 +559,7 @@ export class Track extends Thing { return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ Thing.composite.withReverseReferenceList({ data: 'trackData', - refList: refListProperty, - originalTracksOnly: true, + list: refListProperty, }), { -- cgit 1.3.0-6-gf8a5 From 2437ac322a4c44f2fd9f6a77ac7a65bbb3afc2c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:42:47 -0300 Subject: data: dynamicThingFromSingleReference -> resolvedReference --- src/data/things/composite.js | 4 ++-- src/data/things/homepage-layout.js | 10 +++++----- src/data/things/thing.js | 40 ++++++++++---------------------------- src/data/things/track.js | 6 +++++- 4 files changed, 22 insertions(+), 38 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 138814d9..d3f76b11 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1160,8 +1160,8 @@ export function withResolvedContribs({from, to}) { export function withResolvedReference({ ref, data, - to, find: findFunction, + to = '#resolvedReference', earlyExitIfNotFound = false, }) { return compositeFrom(`Thing.composite.withResolvedReference`, [ @@ -1194,8 +1194,8 @@ export function withResolvedReference({ export function withResolvedReferenceList({ list, data, - to, find: findFunction, + to = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index cbdcb99a..c478bc41 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -119,11 +119,11 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { // Expose only - sourceGroup: Thing.common.dynamicThingFromSingleReference( - 'sourceGroupByRef', - 'groupData', - find.group - ), + sourceGroup: Thing.common.resolvedReference({ + ref: 'sourceGroupByRef', + data: 'groupData', + find: find.group, + }), sourceAlbums: Thing.common.resolvedReferenceList({ list: 'sourceAlbumsByRef', diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 915474d4..36a1f58a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,40 +193,23 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - resolvedReferenceList({ - list, - data, - find: findFunction, - }) { + resolvedReferenceList({list, data, find}) { return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - list, - data, - to: '#things', - find: findFunction, + list, data, find, notFoundMode: 'filter', }), - - Thing.composite.exposeDependency('#things'), + Thing.composite.exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. - dynamicThingFromSingleReference: ( - singleReferenceProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - - expose: { - dependencies: [singleReferenceProperty, thingDataProperty], - compute: ({ - [singleReferenceProperty]: ref, - [thingDataProperty]: thingData, - }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null), - }, - }), + resolvedReference({ref, data, find}) { + return Thing.composite.from(`Thing.common.resolvedReference`, [ + Thing.composite.withResolvedReference({ref, data, find}), + Thing.composite.exposeDependency('#resolvedReference'), + ]); + }, // Corresponding dynamic property to contribsByRef, which takes the values // in the provided property and searches the object's artistData for @@ -273,10 +256,7 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList({ - data, - list, - }) { + reverseReferenceList({data, list}) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), diff --git a/src/data/things/track.js b/src/data/things/track.js index 87e796b9..2b628b66 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -145,7 +145,11 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album), + dataSourceAlbum: Thing.common.resolvedReference({ + ref: 'dataSourceAlbumByRef', + data: 'albumData', + find: find.album, + }), date: Thing.composite.from(`Track.date`, [ Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), -- cgit 1.3.0-6-gf8a5 From 659620b7522d0e36ca15a54716b46d83f0bfc4f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:45:56 -0300 Subject: data: move composite helper functions to top function scope --- src/data/things/composite.js | 390 +++++++++++++++++++++---------------------- 1 file changed, 195 insertions(+), 195 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index d3f76b11..805331a9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -496,255 +496,255 @@ function compositeFrom(firstArg, secondArg) { aggregate.close(); - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { + const filteredDependencies = + (dependencies + ? filterProperties(availableDependencies, dependencies) + : {}); + + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; + } + } - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; + if (options) { + filteredDependencies['#options'] = options; + } - if (baseUpdates) { - constructedDescriptor.update = base.update; + return filteredDependencies; } - if (baseExposes) { - const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); - - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { + return continuationAssignment; } - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } + const assignDependencies = {}; - const assignDependencies = {}; + for (const [from, to] of Object.entries(mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } + return assignDependencies; + } - return assignDependencies; - } + function _prepareContinuation(callingTransformForThisStep) { + const continuationStorage = { + returnedWith: null, + providedDependencies: undefined, + providedValue: undefined, + }; - function _prepareContinuation(callingTransformForThisStep) { - const continuationStorage = { - returnedWith: null, - providedDependencies: undefined, - providedValue: undefined, - }; + const continuation = + (callingTransformForThisStep + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; - const continuation = + if (baseComposes) { + const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); - continuation.exit = (providedValue) => { - continuationStorage.returnedWith = 'exit'; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - }; - - if (baseComposes) { - const makeRaiseLike = returnWith => - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); - } - - return {continuation, continuationStorage}; + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; + return {continuation, continuationStorage}; + } - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); - const availableDependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + const availableDependencies = {...initialDependencies}; - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const expose = - (step.flags - ? step.expose - : step); + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const callingTransformForThisStep = - expectingTransform && expose.transform; + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); + const expose = + (step.flags + ? step.expose + : step); - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const callingTransformForThisStep = + expectingTransform && expose.transform; - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + debug(() => [ + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } + const result = + (callingTransformForThisStep + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); - debug(() => color.bright(`end composition - exit (inferred)`)); + if (result !== continuationSymbol) { + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - return result; + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - const {returnedWith} = continuationStorage; + debug(() => color.bright(`end composition - exit (inferred)`)); + + return result; + } - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; + const {returnedWith} = continuationStorage; - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); + + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; } + } - const {providedValue, providedDependencies} = continuationStorage; - - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - - debug(() => { - const base = `step #${i+1} - result: ` + returnedWith; - const parts = []; - - if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } - } + const {providedValue, providedDependencies} = continuationStorage; - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); + + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - if (empty(parts)) { - return base; + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; + + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); } else { - return [base + ' ->', ...parts]; + parts.push(`value:`, providedValue); } - }); + } - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); - - case 'continuation': - if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); - return continuationIfApplicable(...continuationArgs); - } else { - Object.assign(availableDependencies, continuingWithDependencies); - break; - } + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; } + }); + + switch (returnedWith) { + case 'raise': + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); + + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); + + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } } } + } + + const constructedDescriptor = {}; + + if (annotation) { + constructedDescriptor.annotation = annotation; + } + + constructedDescriptor.flags = { + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, + }; + + if (baseUpdates) { + constructedDescriptor.update = base.update; + } + + if (baseExposes) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); const transformFn = (value, initialDependencies, continuationIfApplicable) => -- cgit 1.3.0-6-gf8a5 From 1594885c506ed76c0f4f1dc58ab14a4fabba6be5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:46:36 -0300 Subject: data: don't pass dependencies without expose properties --- src/data/things/composite.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 805331a9..21cf365f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -501,6 +501,10 @@ function compositeFrom(firstArg, secondArg) { mapDependencies, options, }) { + if (!dependencies && !mapDependencies && !options) { + return null; + } + const filteredDependencies = (dependencies ? filterProperties(availableDependencies, dependencies) @@ -629,8 +633,12 @@ function compositeFrom(firstArg, secondArg) { const result = (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + ? (filteredDependencies + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.transform(valueSoFar, continuation)) + : (filteredDependencies + ? expose.compute(filteredDependencies, continuation) + : expose.compute(continuation))); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -998,7 +1006,7 @@ export function exposeUpdateValueOrContinue({ }, { - transform: (value, {}, continuation) => + transform: (value, continuation) => continuation.exit(value), }, ]); -- cgit 1.3.0-6-gf8a5 From c6ba294c4fef425074f2352b640cc02c4768ee6e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:49:56 -0300 Subject: data: unused import fixes --- src/data/things/thing.js | 2 +- src/data/things/track.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 36a1f58a..968dd102 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; +import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { diff --git a/src/data/things/track.js b/src/data/things/track.js index 2b628b66..7d7e8a68 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -16,7 +16,6 @@ export class Track extends Thing { Flash, validators: { - isBoolean, isColor, isDate, isDuration, -- cgit 1.3.0-6-gf8a5 From ff6d14354612a9da430d523fa9dbc237cae3a6e2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 09:38:26 -0300 Subject: infra, data: allow exporting non-classes from things/ files --- src/data/things/index.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/data') diff --git a/src/data/things/index.js b/src/data/things/index.js index 591cdc3b..2d4f77d7 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -82,6 +82,8 @@ function errorDuplicateClassNames() { function flattenClassLists() { for (const classes of Object.values(allClassLists)) { for (const [name, constructor] of Object.entries(classes)) { + if (typeof constructor !== 'function') continue; + if (!(constructor.prototype instanceof Thing)) continue; allClasses[name] = constructor; } } -- cgit 1.3.0-6-gf8a5 From 78d293d5f4eea7ed6ee6f3cddd3ffcf73c5056a0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 09:40:55 -0300 Subject: data: directly import from #composite; define own utils at module --- src/data/things/composite.js | 32 +-- src/data/things/thing.js | 36 ++- src/data/things/track.js | 670 ++++++++++++++++++++++--------------------- 3 files changed, 379 insertions(+), 359 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 21cf365f..bcc52a2a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -808,7 +808,7 @@ function _export(mapping) { const mappingEntries = Object.entries(mapping); return { - annotation: `Thing.composite.export`, + annotation: `export`, flags: {expose: true, compose: true}, expose: { @@ -853,7 +853,7 @@ export function exposeDependency(dependency, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeDependency`, + annotation: `exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -878,7 +878,7 @@ export function exposeConstant(value, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeConstant`, + annotation: `exposeConstant`, flags: {expose: true, update: !!update}, expose: { @@ -934,7 +934,7 @@ export function withResultOfAvailabilityCheck({ if (fromDependency) { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, + annotation: `withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -946,7 +946,7 @@ export function withResultOfAvailabilityCheck({ }; } else { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, + annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, @@ -963,7 +963,7 @@ export function withResultOfAvailabilityCheck({ export function exposeDependencyOrContinue(dependency, { mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -991,7 +991,7 @@ export function exposeDependencyOrContinue(dependency, { export function exposeUpdateValueOrContinue({ mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + return compositeFrom(`exposeUpdateValueOrContinue`, [ withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1019,7 +1019,7 @@ export function earlyExitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1042,7 +1042,7 @@ export function earlyExitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1054,7 +1054,7 @@ export function earlyExitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1067,7 +1067,7 @@ export function raiseWithoutDependency(dependency, { map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { @@ -1094,7 +1094,7 @@ export function raiseWithoutUpdateValue({ map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + return compositeFrom(`raiseWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { @@ -1121,8 +1121,8 @@ export function raiseWithoutUpdateValue({ // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { - return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.raiseWithoutDependency(from, { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { mode: 'empty', map: {to}, raise: {to: []}, @@ -1172,7 +1172,7 @@ export function withResolvedReference({ to = '#resolvedReference', earlyExitIfNotFound = false, }) { - return compositeFrom(`Thing.composite.withResolvedReference`, [ + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), earlyExitWithoutDependency(data), @@ -1210,7 +1210,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + return compositeFrom(`withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 968dd102..0716931a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -8,6 +8,15 @@ import find from '#find'; import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; +import { + from as compositeFrom, + exposeDependency, + withReverseReferenceList, + withResolvedContribs, + withResolvedReference, + withResolvedReferenceList, +} from '#composite'; + import { isAdditionalFileList, isBoolean, @@ -27,7 +36,6 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; -import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -194,20 +202,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. resolvedReferenceList({list, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ - Thing.composite.withResolvedReferenceList({ + return compositeFrom(`Thing.common.resolvedReferenceList`, [ + withResolvedReferenceList({ list, data, find, notFoundMode: 'filter', }), - Thing.composite.exposeDependency('#resolvedReferenceList'), + exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. resolvedReference({ref, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReference`, [ - Thing.composite.withResolvedReference({ref, data, find}), - Thing.composite.exposeDependency('#resolvedReference'), + return compositeFrom(`Thing.common.resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency('#resolvedReference'), ]); }, @@ -227,13 +235,13 @@ export default class Thing extends CacheableObject { // reference list is somehow messed up, or artistData isn't being provided // properly.) dynamicContribs(contribsByRefProperty) { - return Thing.composite.from(`Thing.common.dynamicContribs`, [ - Thing.composite.withResolvedContribs({ + return compositeFrom(`Thing.common.dynamicContribs`, [ + withResolvedContribs({ from: contribsByRefProperty, to: '#contribs', }), - Thing.composite.exposeDependency('#contribs'), + exposeDependency('#contribs'), ]); }, @@ -257,9 +265,9 @@ export default class Thing extends CacheableObject { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. reverseReferenceList({data, list}) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, list}), - Thing.composite.exposeDependency('#reverseReferenceList'), + return compositeFrom(`Thing.common.reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency('#reverseReferenceList'), ]); }, @@ -323,6 +331,4 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - - static composite = composite; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 7d7e8a68..c5e6ff34 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -4,6 +4,19 @@ import {color} from '#cli'; import find from '#find'; import {empty} from '#sugar'; +import { + from as compositeFrom, + earlyExitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, + withResolvedContribs, + withResolvedReference, + withResultOfAvailabilityCheck, + withReverseReferenceList, +} from '#composite'; + import Thing from './thing.js'; export class Track extends Thing { @@ -43,9 +56,9 @@ export class Track extends Thing { sampledTracksByRef: Thing.common.referenceList(Track), artTagsByRef: Thing.common.referenceList(ArtTag), - color: Thing.composite.from(`Track.color`, [ - Thing.composite.exposeUpdateValueOrContinue(), - Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}), + color: compositeFrom(`Track.color`, [ + exposeUpdateValueOrContinue(), + withContainingTrackSection({earlyExitIfNotFound: false}), { dependencies: ['#trackSection'], @@ -59,8 +72,8 @@ export class Track extends Thing { : continuation()), }, - Track.composite.withAlbumProperty('color'), - Thing.composite.exposeDependency('#album.color', { + withAlbumProperty('color'), + exposeDependency('#album.color', { update: {validate: isColor}, }), ]), @@ -75,21 +88,21 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the extension // of the album's main artwork. It does inherit trackCoverArtFileExtension, // if present on the album. - coverArtFileExtension: Thing.composite.from(`Track.coverArtFileExtension`, [ + coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [ // No cover art file extension if the track doesn't have unique artwork // in the first place. - Track.composite.withHasUniqueCoverArt(), - Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + withHasUniqueCoverArt(), + earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. - Thing.composite.exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - Track.composite.withAlbumProperty('trackCoverArtFileExtension'), - Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + withAlbumProperty('trackCoverArtFileExtension'), + exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), // Fallback to 'jpg'. - Thing.composite.exposeConstant('jpg', { + exposeConstant('jpg', { update: {validate: isFileExtension}, }), ]), @@ -98,14 +111,14 @@ export class Track extends Thing { // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: Thing.composite.from(`Track.coverArtDate`, [ - Track.composite.withHasUniqueCoverArt(), - Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + coverArtDate: compositeFrom(`Track.coverArtDate`, [ + withHasUniqueCoverArt(), + earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), - Thing.composite.exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue(), - Track.composite.withAlbumProperty('trackArtDate'), - Thing.composite.exposeDependency('#album.trackArtDate', { + withAlbumProperty('trackArtDate'), + exposeDependency('#album.trackArtDate', { update: {validate: isDate}, }), ]), @@ -132,9 +145,9 @@ export class Track extends Thing { commentatorArtists: Thing.common.commentatorArtists(), - album: Thing.composite.from(`Track.album`, [ - Track.composite.withAlbum(), - Thing.composite.exposeDependency('#album'), + album: compositeFrom(`Track.album`, [ + withAlbum(), + exposeDependency('#album'), ]), // Note - this is an internal property used only to help identify a track. @@ -150,10 +163,10 @@ export class Track extends Thing { find: find.album, }), - date: Thing.composite.from(`Track.date`, [ - Thing.composite.exposeDependencyOrContinue('dateFirstReleased'), - Track.composite.withAlbumProperty('date'), - Thing.composite.exposeDependency('#album.date'), + date: compositeFrom(`Track.date`, [ + exposeDependencyOrContinue('dateFirstReleased'), + withAlbumProperty('date'), + exposeDependency('#album.date'), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -163,19 +176,19 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [ - Track.composite.withHasUniqueCoverArt(), - Thing.composite.exposeDependency('#hasUniqueCoverArt'), + hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exposeDependency('#hasUniqueCoverArt'), ]), - originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [ - Track.composite.withOriginalRelease(), - Thing.composite.exposeDependency('#originalRelease'), + originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ + withOriginalRelease(), + exposeDependency('#originalRelease'), ]), - otherReleases: Thing.composite.from(`Track.otherReleases`, [ - Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}), - Track.composite.withOriginalRelease({selfIfOriginal: true}), + otherReleases: compositeFrom(`Track.otherReleases`, [ + earlyExitWithoutDependency('trackData', {mode: 'empty'}), + withOriginalRelease({selfIfOriginal: true}), { flags: {expose: true}, @@ -197,10 +210,10 @@ export class Track extends Thing { }, ]), - artistContribs: Thing.composite.from(`Track.artistContribs`, [ - Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}), + artistContribs: compositeFrom(`Track.artistContribs`, [ + inheritFromOriginalRelease({property: 'artistContribs'}), - Thing.composite.withResolvedContribs({ + withResolvedContribs({ from: 'artistContribsByRef', to: '#artistContribs', }), @@ -213,19 +226,19 @@ export class Track extends Thing { : contribsFromTrack), }, - Track.composite.withAlbumProperty('artistContribs'), - Thing.composite.exposeDependency('#album.artistContribs'), + withAlbumProperty('artistContribs'), + exposeDependency('#album.artistContribs'), ]), - contributorContribs: Thing.composite.from(`Track.contributorContribs`, [ - Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}), + contributorContribs: compositeFrom(`Track.contributorContribs`, [ + inheritFromOriginalRelease({property: 'contributorContribs'}), Thing.common.dynamicContribs('contributorContribsByRef'), ]), // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [ + coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ { dependencies: ['disableUniqueCoverArt'], compute: ({disableUniqueCoverArt}, continuation) => @@ -234,7 +247,7 @@ export class Track extends Thing { : continuation()), }, - Thing.composite.withResolvedContribs({ + withResolvedContribs({ from: 'coverArtistContribsByRef', to: '#coverArtistContribs', }), @@ -247,12 +260,12 @@ export class Track extends Thing { : contribsFromTrack), }, - Track.composite.withAlbumProperty('trackCoverArtistContribs'), - Thing.composite.exposeDependency('#album.trackCoverArtistContribs'), + withAlbumProperty('trackCoverArtistContribs'), + exposeDependency('#album.trackCoverArtistContribs'), ]), - referencedTracks: Thing.composite.from(`Track.referencedTracks`, [ - Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}), + referencedTracks: compositeFrom(`Track.referencedTracks`, [ + inheritFromOriginalRelease({property: 'referencedTracks'}), Thing.common.resolvedReferenceList({ list: 'referencedTracksByRef', data: 'trackData', @@ -260,8 +273,8 @@ export class Track extends Thing { }), ]), - sampledTracks: Thing.composite.from(`Track.sampledTracks`, [ - Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}), + sampledTracks: compositeFrom(`Track.sampledTracks`, [ + inheritFromOriginalRelease({property: 'sampledTracks'}), Thing.common.resolvedReferenceList({ list: 'sampledTracksByRef', data: 'trackData', @@ -283,10 +296,10 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: Track.composite.trackReverseReferenceList('referencedTracks'), + referencedByTracks: trackReverseReferenceList('referencedTracks'), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'), + sampledByTracks: trackReverseReferenceList('sampledTracks'), featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', @@ -294,309 +307,310 @@ export class Track extends Thing { }), }); - static composite = { - // Early exits with a value inherited from the original release, if - // this track is a rerelease, and otherwise continues with no further - // dependencies provided. If allowOverride is true, then the continuation - // will also be called if the original release exposed the requested - // property as null. - inheritFromOriginalRelease({ - property: originalProperty, - allowOverride = false, - }) { - return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [ - Track.composite.withOriginalRelease(), - - { - dependencies: ['#originalRelease'], - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); - - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); - - return continuation.exit(value); - }, - }, - ]); - }, + [inspect.custom](depth) { + const parts = []; - // Gets the track's album. Unless earlyExitIfNotFound is overridden false, - // this will early exit with null in two cases - albumData being missing, - // or not including an album whose .tracks array includes this track. - withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) { - return Thing.composite.from(`Track.composite.withAlbum`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: 'albumData', - mode: 'empty', - to: '#albumDataAvailability', - }), + parts.push(Thing.prototype[inspect.custom].apply(this)); - { - dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, + if (this.originalReleaseTrackByRef) { + parts.unshift(`${color.yellow('[rerelease]')} `); + } - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: null}))), - }, + let album; + if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { + const albumName = album.name; + const albumIndex = album.tracks.indexOf(this); + const trackNum = + (albumIndex === -1 + ? '#?' + : `#${albumIndex + 1}`); + parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + } - { - dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => - continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), - }), - }, + return parts.join(''); + } +} - { - dependencies: ['#album'], - options: {earlyExitIfNotFound}, - mapContinuation: {to}, - compute: ({ - '#album': album, - '#options': {earlyExitIfNotFound}, - }, continuation) => - (album - ? continuation.raise({to: album}) - : (earlyExitIfNotFound - ? continuation.exit(null) - : continuation.raise({to: album}))), - }, - ]); +// Early exits with a value inherited from the original release, if +// this track is a rerelease, and otherwise continues with no further +// dependencies provided. If allowOverride is true, then the continuation +// will also be called if the original release exposed the requested +// property as null. +function inheritFromOriginalRelease({ + property: originalProperty, + allowOverride = false, +}) { + return compositeFrom(`inheritFromOriginalRelease`, [ + withOriginalRelease(), + + { + dependencies: ['#originalRelease'], + compute({'#originalRelease': originalRelease}, continuation) { + if (!originalRelease) return continuation.raise(); + + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation.raise(); + + return continuation.exit(value); + }, }, + ]); +} - // Gets a single property from this track's album, providing it as the same - // property name prefixed with '#album.' (by default). If the track's album - // isn't available, and earlyExitIfNotFound hasn't been set, the property - // will be provided as null. - withAlbumProperty(property, { - to = '#album.' + property, - earlyExitIfNotFound = false, - } = {}) { - return Thing.composite.from(`Track.composite.withAlbumProperty`, [ - Track.composite.withAlbum({earlyExitIfNotFound}), - - { - dependencies: ['#album'], - options: {property}, - mapContinuation: {to}, +// Gets the track's album. Unless earlyExitIfNotFound is overridden false, +// this will early exit with null in two cases - albumData being missing, +// or not including an album whose .tracks array includes this track. +function withAlbum({ + to = '#album', + earlyExitIfNotFound = true, +} = {}) { + return compositeFrom(`withAlbum`, [ + withResultOfAvailabilityCheck({ + fromDependency: 'albumData', + mode: 'empty', + to: '#albumDataAvailability', + }), - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), - }, - ]); + { + dependencies: ['#albumDataAvailability'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + + compute: ({ + '#albumDataAvailability': albumDataAvailability, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (albumDataAvailability + ? continuation() + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: null}))), }, - // Gets the listed properties from this track's album, providing them as - // dependencies (by default) with '#album.' prefixed before each property - // name. If the track's album isn't available, and earlyExitIfNotFound - // hasn't been set, the same dependency names will be provided as null. - withAlbumProperties({ - properties, - prefix = '#album', - earlyExitIfNotFound = false, - }) { - return Thing.composite.from(`Track.composite.withAlbumProperties`, [ - Track.composite.withAlbum({earlyExitIfNotFound}), - - { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } - } - - return continuation.raise(raise); - }, - }, - ]); + { + dependencies: ['this', 'albumData'], + compute: ({this: track, albumData}, continuation) => + continuation({ + '#album': + albumData.find(album => album.tracks.includes(track)), + }), }, - // Gets the track section containing this track from its album's track list. - // Unless earlyExitIfNotFound is overridden false, this will early exit if - // the album can't be found or if none of its trackSections includes the - // track for some reason. - withContainingTrackSection({ - to = '#trackSection', - earlyExitIfNotFound = true, - } = {}) { - return Thing.composite.from(`Track.composite.withContainingTrackSection`, [ - Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}), - - { - dependencies: ['this', '#album.trackSections'], - mapContinuation: {to}, - - compute({ - this: track, - '#album.trackSections': trackSections, - }, continuation) { - if (!trackSections) { - return continuation.raise({to: null}); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { - return continuation.exit(null); - } else { - return continuation.raise({to: null}); - } - }, - }, - ]); + { + dependencies: ['#album'], + options: {earlyExitIfNotFound}, + mapContinuation: {to}, + compute: ({ + '#album': album, + '#options': {earlyExitIfNotFound}, + }, continuation) => + (album + ? continuation.raise({to: album}) + : (earlyExitIfNotFound + ? continuation.exit(null) + : continuation.raise({to: album}))), }, + ]); +} - // Just includes the original release of this track as a dependency. - // If this track isn't a rerelease, then it'll provide null, unless the - // {selfIfOriginal} option is set, in which case it'll provide this track - // itself. Note that this will early exit if the original release is - // specified by reference and that reference doesn't resolve to anything. - // Outputs to '#originalRelease' by default. - withOriginalRelease({ - to = '#originalRelease', - selfIfOriginal = false, - } = {}) { - return Thing.composite.from(`Track.composite.withOriginalRelease`, [ - Thing.composite.withResolvedReference({ - ref: 'originalReleaseTrackByRef', - data: 'trackData', - to: '#originalRelease', - find: find.track, - earlyExitIfNotFound: true, - }), - - { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {to}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ - to: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, - ]); +// Gets a single property from this track's album, providing it as the same +// property name prefixed with '#album.' (by default). If the track's album +// isn't available, and earlyExitIfNotFound hasn't been set, the property +// will be provided as null. +function withAlbumProperty(property, { + to = '#album.' + property, + earlyExitIfNotFound = false, +} = {}) { + return compositeFrom(`withAlbumProperty`, [ + withAlbum({earlyExitIfNotFound}), + + { + dependencies: ['#album'], + options: {property}, + mapContinuation: {to}, + + compute: ({ + '#album': album, + '#options': {property}, + }, continuation) => + (album + ? continuation.raise({to: album[property]}) + : continuation.raise({to: null})), }, + ]); +} - // The algorithm for checking if a track has unique cover art is used in a - // couple places, so it's defined in full as a compositional step. - withHasUniqueCoverArt({ - to = '#hasUniqueCoverArt', - } = {}) { - return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [ - { - dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? continuation.raise({to: false}) - : continuation()), - }, +// Gets the listed properties from this track's album, providing them as +// dependencies (by default) with '#album.' prefixed before each property +// name. If the track's album isn't available, and earlyExitIfNotFound +// hasn't been set, the same dependency names will be provided as null. +function withAlbumProperties({ + properties, + prefix = '#album', + earlyExitIfNotFound = false, +}) { + return compositeFrom(`withAlbumProperties`, [ + withAlbum({earlyExitIfNotFound}), + + { + dependencies: ['#album'], + options: {properties, prefix}, + + compute({ + '#album': album, + '#options': {properties, prefix}, + }, continuation) { + const raise = {}; + + if (album) { + for (const property of properties) { + raise[prefix + '.' + property] = album[property]; + } + } else { + for (const property of properties) { + raise[prefix + '.' + property] = null; + } + } + + return continuation.raise(raise); + }, + }, + ]); +} - Thing.composite.withResolvedContribs({ - from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', - }), +// Gets the track section containing this track from its album's track list. +// Unless earlyExitIfNotFound is overridden false, this will early exit if +// the album can't be found or if none of its trackSections includes the +// track for some reason. +function withContainingTrackSection({ + to = '#trackSection', + earlyExitIfNotFound = true, +} = {}) { + return compositeFrom(`withContainingTrackSection`, [ + withAlbumProperty('trackSections', {earlyExitIfNotFound}), + + { + dependencies: ['this', '#album.trackSections'], + mapContinuation: {to}, + + compute({ + this: track, + '#album.trackSections': trackSections, + }, continuation) { + if (!trackSections) { + return continuation.raise({to: null}); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raise({to: trackSection}); + } else if (earlyExitIfNotFound) { + return continuation.exit(null); + } else { + return continuation.raise({to: null}); + } + }, + }, + ]); +} - { - dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raise({to: true})), - }, +// Just includes the original release of this track as a dependency. +// If this track isn't a rerelease, then it'll provide null, unless the +// {selfIfOriginal} option is set, in which case it'll provide this track +// itself. Note that this will early exit if the original release is +// specified by reference and that reference doesn't resolve to anything. +// Outputs to '#originalRelease' by default. +function withOriginalRelease({ + to = '#originalRelease', + selfIfOriginal = false, +} = {}) { + return compositeFrom(`withOriginalRelease`, [ + withResolvedReference({ + ref: 'originalReleaseTrackByRef', + data: 'trackData', + to: '#originalRelease', + find: find.track, + earlyExitIfNotFound: true, + }), - Track.composite.withAlbumProperty('trackCoverArtistContribs'), + { + dependencies: ['this', '#originalRelease'], + options: {selfIfOriginal}, + mapContinuation: {to}, + compute: ({ + this: track, + '#originalRelease': originalRelease, + '#options': {selfIfOriginal}, + }, continuation) => + continuation.raise({ + to: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + ]); +} - { - dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), - }, - ]); +// The algorithm for checking if a track has unique cover art is used in a +// couple places, so it's defined in full as a compositional step. +function withHasUniqueCoverArt({ + to = '#hasUniqueCoverArt', +} = {}) { + return compositeFrom(`withHasUniqueCoverArt`, [ + { + dependencies: ['disableUniqueCoverArt'], + mapContinuation: {to}, + compute: ({disableUniqueCoverArt}, continuation) => + (disableUniqueCoverArt + ? continuation.raise({to: false}) + : continuation()), }, - trackReverseReferenceList(refListProperty) { - return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [ - Thing.composite.withReverseReferenceList({ - data: 'trackData', - list: refListProperty, - }), + withResolvedContribs({ + from: 'coverArtistContribsByRef', + to: '#coverArtistContribs', + }), - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({'#reverseReferenceList': reverseReferenceList}) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ]); + { + dependencies: ['#coverArtistContribs'], + mapContinuation: {to}, + compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raise({to: true})), }, - }; - - [inspect.custom](depth) { - const parts = []; - parts.push(Thing.prototype[inspect.custom].apply(this)); + withAlbumProperty('trackCoverArtistContribs'), - if (this.originalReleaseTrackByRef) { - parts.unshift(`${color.yellow('[rerelease]')} `); - } + { + dependencies: ['#album.trackCoverArtistContribs'], + mapContinuation: {to}, + compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => + (empty(contribsFromAlbum) + ? continuation.raise({to: false}) + : continuation.raise({to: true})), + }, + ]); +} - let album; - if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) { - const albumName = album.name; - const albumIndex = album.tracks.indexOf(this); - const trackNum = - (albumIndex === -1 - ? '#?' - : `#${albumIndex + 1}`); - parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); - } +function trackReverseReferenceList(refListProperty) { + return compositeFrom(`trackReverseReferenceList`, [ + withReverseReferenceList({ + data: 'trackData', + list: refListProperty, + }), - return parts.join(''); - } + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({'#reverseReferenceList': reverseReferenceList}) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ]); } -- cgit 1.3.0-6-gf8a5 From c86de8a2be3867c14ca92c8e6799fd9b325305ec Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 09:55:29 -0300 Subject: data: move composite utilities related to wiki data into thing.js --- src/data/things/composite.js | 167 ------------------------------------------- src/data/things/thing.js | 166 ++++++++++++++++++++++++++++++++++++++++-- src/data/things/track.js | 9 +-- 3 files changed, 165 insertions(+), 177 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bcc52a2a..7cba1e97 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,18 +1,13 @@ import {inspect} from 'node:util'; import {color} from '#cli'; -import find from '#find'; -import {filterMultipleArrays} from '#wiki-data'; import { empty, filterProperties, openAggregate, - stitchArrays, } from '#sugar'; -import Thing from './thing.js'; - // Composes multiple compositional "steps" and a "base" to form a property // descriptor out of modular building blocks. This is an extension to the // more general-purpose CacheableObject property descriptor syntax, and @@ -794,8 +789,6 @@ export function debug(fn) { return value; } -// -- Compositional steps for compositions to nest -- - // Provides dependencies exactly as they are (or null if not defined) to the // continuation. Although this can *technically* be used to alias existing // dependencies to some other name within the middle of a composition, it's @@ -834,8 +827,6 @@ function _export(mapping) { }; } -// -- Compositional steps for top-level property descriptors -- - // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate @@ -1113,161 +1104,3 @@ export function raiseWithoutUpdateValue({ }, ]); } - -// -- Compositional steps for processing data -- - -// Resolves the contribsByRef contained in the provided dependency, -// providing (named by the second argument) the result. "Resolving" -// means mapping the "who" reference of each contribution to an artist -// object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, to}) { - return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency(from, { - mode: 'empty', - map: {to}, - raise: {to: []}, - }), - - { - mapDependencies: {from}, - compute: ({from}, continuation) => - continuation({ - '#whoByRef': from.map(({who}) => who), - '#what': from.map(({what}) => what), - }), - }, - - withResolvedReferenceList({ - list: '#whoByRef', - data: 'artistData', - to: '#who', - find: find.artist, - notFoundMode: 'null', - }), - - { - dependencies: ['#who', '#what'], - mapContinuation: {to}, - compute({'#who': who, '#what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - to: stitchArrays({who, what}), - }); - }, - }, - ]); -} - -// Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if earlyExitIfNotFound is set to true, -// if the find function doesn't match anything for the reference. -// Otherwise, the data object is provided on the output dependency; -// or null, if the reference doesn't match anything or itself was null -// to begin with. -export function withResolvedReference({ - ref, - data, - find: findFunction, - to = '#resolvedReference', - earlyExitIfNotFound = false, -}) { - return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - earlyExitWithoutDependency(data), - - { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, - - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, - }, - ]); -} - -// Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export function withResolvedReferenceList({ - list, - data, - find: findFunction, - to = '#resolvedReferenceList', - notFoundMode = 'filter', -}) { - if (!['filter', 'exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); - } - - return compositeFrom(`withResolvedReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - - raiseWithoutDependency(list, { - map: {to}, - raise: {to: []}, - mode: 'empty', - }), - - { - options: {findFunction, notFoundMode}, - mapDependencies: {list, data}, - mapContinuation: {matches: to}, - - compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { - let matches = - list.map(ref => findFunction(ref, data, {mode: 'quiet'})); - - if (!matches.includes(null)) { - return continuation.raise({matches}); - } - - switch (notFoundMode) { - case 'filter': - matches = matches.filter(value => value !== null); - return continuation.raise({matches}); - - case 'exit': - return continuation.exit([]); - - case 'null': - return continuation.raise({matches}); - } - }, - }, - ]); -} - -// Check out the info on Thing.common.reverseReferenceList! -// This is its composable form. -export function withReverseReferenceList({ - data, - list: refListProperty, - to = '#reverseReferenceList', -}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {to}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ]); -} diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0716931a..1077a652 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,16 +5,14 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; -import {getKebabCase} from '#wiki-data'; +import {empty, stitchArrays} from '#sugar'; +import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, + earlyExitWithoutDependency, exposeDependency, - withReverseReferenceList, - withResolvedContribs, - withResolvedReference, - withResolvedReferenceList, + raiseWithoutDependency, } from '#composite'; import { @@ -332,3 +330,159 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export function withResolvedContribs({from, to}) { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { + mapDependencies: {from}, + compute: ({from}, continuation) => + continuation({ + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), + }), + }, + + withResolvedReferenceList({ + list: '#whoByRef', + data: 'artistData', + to: '#who', + find: find.artist, + notFoundMode: 'null', + }), + + { + dependencies: ['#who', '#what'], + mapContinuation: {to}, + compute({'#who': who, '#what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + to: stitchArrays({who, what}), + }); + }, + }, + ]); +} + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if earlyExitIfNotFound is set to true, +// if the find function doesn't match anything for the reference. +// Otherwise, the data object is provided on the output dependency; +// or null, if the reference doesn't match anything or itself was null +// to begin with. +export function withResolvedReference({ + ref, + data, + find: findFunction, + to = '#resolvedReference', + earlyExitIfNotFound = false, +}) { + return compositeFrom(`withResolvedReference`, [ + raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + earlyExitWithoutDependency(data), + + { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, + + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, + }, + ]); +} + +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export function withResolvedReferenceList({ + list, + data, + find: findFunction, + to = '#resolvedReferenceList', + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(list, { + map: {to}, + raise: {to: []}, + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {list, data}, + mapContinuation: {matches: to}, + + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { + let matches = + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + + if (!matches.includes(null)) { + return continuation.raise({matches}); + } + + switch (notFoundMode) { + case 'filter': + matches = matches.filter(value => value !== null); + return continuation.raise({matches}); + + case 'exit': + return continuation.exit([]); + + case 'null': + return continuation.raise({matches}); + } + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + list: refListProperty, + to = '#reverseReferenceList', +}) { + return compositeFrom(`Thing.common.reverseReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); +} diff --git a/src/data/things/track.js b/src/data/things/track.js index c5e6ff34..0d7803bd 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,13 +11,14 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, - withResolvedContribs, - withResolvedReference, withResultOfAvailabilityCheck, - withReverseReferenceList, } from '#composite'; -import Thing from './thing.js'; +import Thing, { + withResolvedContribs, + withResolvedReference, + withReverseReferenceList, +} from './thing.js'; export class Track extends Thing { static [Thing.referenceType] = 'track'; -- cgit 1.3.0-6-gf8a5 From 3437936e6127192d30a308b68731cd4aa33555e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:10:44 -0300 Subject: data: earlyExit -> exit in misc. utility names --- src/data/things/composite.js | 20 ++++++++++---------- src/data/things/thing.js | 9 ++++----- src/data/things/track.js | 8 ++++---- 3 files changed, 18 insertions(+), 19 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7cba1e97..84a98290 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1004,13 +1004,13 @@ export function exposeUpdateValueOrContinue({ } // Early exits if an availability check has failed. -// This is for internal use only - use `earlyExitWithoutDependency` or -// `earlyExitWIthoutUpdateValue` instead. -export function earlyExitIfAvailabilityCheckFailed({ +// This is for internal use only - use `exitWithoutDependency` or +// `exitWithoutUpdateValue` instead. +export function exitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`exitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1029,25 +1029,25 @@ export function earlyExitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutDependency(dependency, { +export function exitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } // Early exits if this property's update value isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutUpdateValue({ +export function exitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1077a652..5d407153 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -10,7 +10,7 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, - earlyExitWithoutDependency, + exitWithoutDependency, exposeDependency, raiseWithoutDependency, } from '#composite'; @@ -389,7 +389,7 @@ export function withResolvedReference({ }) { return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - earlyExitWithoutDependency(data), + exitWithoutDependency(data), { options: {findFunction, earlyExitIfNotFound}, @@ -426,8 +426,7 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - + exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { map: {to}, raise: {to: []}, @@ -471,7 +470,7 @@ export function withReverseReferenceList({ to = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), + exitWithoutDependency(data, {value: []}), { dependencies: ['this'], diff --git a/src/data/things/track.js b/src/data/things/track.js index 0d7803bd..a7f96e42 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -6,7 +6,7 @@ import {empty} from '#sugar'; import { from as compositeFrom, - earlyExitWithoutDependency, + exitWithoutDependency, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -93,7 +93,7 @@ export class Track extends Thing { // No cover art file extension if the track doesn't have unique artwork // in the first place. withHasUniqueCoverArt(), - earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), @@ -114,7 +114,7 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: compositeFrom(`Track.coverArtDate`, [ withHasUniqueCoverArt(), - earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), exposeUpdateValueOrContinue(), @@ -188,7 +188,7 @@ export class Track extends Thing { ]), otherReleases: compositeFrom(`Track.otherReleases`, [ - earlyExitWithoutDependency('trackData', {mode: 'empty'}), + exitWithoutDependency('trackData', {mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), { -- cgit 1.3.0-6-gf8a5 From b076c87e435bbe2403122158ee03e4934c220c6c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:32:41 -0300 Subject: data: earlyExitIfNotFound -> notFoundMode --- src/data/things/thing.js | 21 +++++++++------- src/data/things/track.js | 65 +++++++++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 38 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d407153..93f19799 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -375,31 +375,34 @@ export function withResolvedContribs({from, to}) { // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if earlyExitIfNotFound is set to true, -// if the find function doesn't match anything for the reference. -// Otherwise, the data object is provided on the output dependency; -// or null, if the reference doesn't match anything or itself was null -// to begin with. +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. export function withResolvedReference({ ref, data, find: findFunction, to = '#resolvedReference', - earlyExitIfNotFound = false, + notFoundMode = 'null', }) { + if (!['exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be exit or null`); + } + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), exitWithoutDependency(data), { - options: {findFunction, earlyExitIfNotFound}, + options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { + if (match === null && notFoundMode === 'exit') { return continuation.exit(null); } diff --git a/src/data/things/track.js b/src/data/things/track.js index a7f96e42..9e1942e3 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,7 @@ export class Track extends Thing { color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), - withContainingTrackSection({earlyExitIfNotFound: false}), + withContainingTrackSection(), { dependencies: ['#trackSection'], @@ -358,12 +358,13 @@ function inheritFromOriginalRelease({ ]); } -// Gets the track's album. Unless earlyExitIfNotFound is overridden false, -// this will early exit with null in two cases - albumData being missing, -// or not including an album whose .tracks array includes this track. +// Gets the track's album. This will early exit if albumData is missing. +// By default, if there's no album whose list of tracks includes this track, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. function withAlbum({ to = '#album', - earlyExitIfNotFound = true, + notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbum`, [ withResultOfAvailabilityCheck({ @@ -374,16 +375,16 @@ function withAlbum({ { dependencies: ['#albumDataAvailability'], - options: {earlyExitIfNotFound}, + options: {notFoundMode}, mapContinuation: {to}, compute: ({ '#albumDataAvailability': albumDataAvailability, - '#options': {earlyExitIfNotFound}, + '#options': {notFoundMode}, }, continuation) => (albumDataAvailability ? continuation() - : (earlyExitIfNotFound + : (notFoundMode === 'exit' ? continuation.exit(null) : continuation.raise({to: null}))), }, @@ -392,38 +393,38 @@ function withAlbum({ dependencies: ['this', 'albumData'], compute: ({this: track, albumData}, continuation) => continuation({ - '#album': - albumData.find(album => album.tracks.includes(track)), + '#album': albumData.find(album => album.tracks.includes(track)), }), }, { dependencies: ['#album'], - options: {earlyExitIfNotFound}, + options: {notFoundMode}, mapContinuation: {to}, + compute: ({ '#album': album, - '#options': {earlyExitIfNotFound}, + '#options': {notFoundMode}, }, continuation) => (album ? continuation.raise({to: album}) - : (earlyExitIfNotFound + : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: album}))), + : continuation.raise({to: null}))), }, ]); } // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album -// isn't available, and earlyExitIfNotFound hasn't been set, the property -// will be provided as null. +// isn't available, then by default, the property will be provided as null; +// set {notFoundMode: 'exit'} to early exit instead. function withAlbumProperty(property, { to = '#album.' + property, - earlyExitIfNotFound = false, + notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbumProperty`, [ - withAlbum({earlyExitIfNotFound}), + withAlbum({notFoundMode}), { dependencies: ['#album'], @@ -443,15 +444,16 @@ function withAlbumProperty(property, { // Gets the listed properties from this track's album, providing them as // dependencies (by default) with '#album.' prefixed before each property -// name. If the track's album isn't available, and earlyExitIfNotFound -// hasn't been set, the same dependency names will be provided as null. +// name. If the track's album isn't available, then by default, the same +// dependency names will be provided as null; set {notFoundMode: 'exit'} +// to early exit instead. function withAlbumProperties({ properties, prefix = '#album', - earlyExitIfNotFound = false, + notFoundMode = 'null', }) { return compositeFrom(`withAlbumProperties`, [ - withAlbum({earlyExitIfNotFound}), + withAlbum({notFoundMode}), { dependencies: ['#album'], @@ -480,23 +482,28 @@ function withAlbumProperties({ } // Gets the track section containing this track from its album's track list. -// Unless earlyExitIfNotFound is overridden false, this will early exit if -// the album can't be found or if none of its trackSections includes the -// track for some reason. +// If notFoundMode is set to 'exit', this will early exit if the album can't be +// found or if none of its trackSections includes the track for some reason. function withContainingTrackSection({ to = '#trackSection', - earlyExitIfNotFound = true, + notFoundMode = 'null', } = {}) { + if (!['exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be exit or null`); + } + return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty('trackSections', {earlyExitIfNotFound}), + withAlbumProperty('trackSections', {notFoundMode}), { dependencies: ['this', '#album.trackSections'], + options: {notFoundMode}, mapContinuation: {to}, compute({ this: track, '#album.trackSections': trackSections, + '#options': {notFoundMode}, }, continuation) { if (!trackSections) { return continuation.raise({to: null}); @@ -507,7 +514,7 @@ function withContainingTrackSection({ if (trackSection) { return continuation.raise({to: trackSection}); - } else if (earlyExitIfNotFound) { + } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { return continuation.raise({to: null}); @@ -533,7 +540,7 @@ function withOriginalRelease({ data: 'trackData', to: '#originalRelease', find: find.track, - earlyExitIfNotFound: true, + notFoundMode: 'exit', }), { -- cgit 1.3.0-6-gf8a5 From fcdc788a3b9efe308518ccdce89f8db0dd5618f6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:36:32 -0300 Subject: data: composite docs update --- src/data/things/composite.js | 31 +++++++++++++++---------------- src/data/things/thing.js | 2 +- src/data/things/track.js | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 84a98290..b9cd6bfb 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -104,7 +104,7 @@ import { // on a provided dependency name, and then providing a result in another // also-provided dependency name: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -126,10 +126,11 @@ import { // // And how you might work that into a composition: // -// static Track[Thing.getPropertyDescriptors].coverArtists = -// Thing.composite.from([ -// Track.composite.doSomethingWhichMightEarlyExit(), -// Thing.composite.withResolvedContribs({ +// Track.coverArtists = +// compositeFrom([ +// doSomethingWhichMightEarlyExit(), +// +// withResolvedContribs({ // from: 'coverArtistContribsByRef', // to: '#coverArtistContribs', // }), @@ -138,9 +139,8 @@ import { // flags: {expose: true}, // expose: { // dependencies: ['#coverArtistContribs'], -// compute({'#coverArtistContribs': coverArtistContribs}) { -// return coverArtistContribs.map(({who}) => who); -// }, +// compute: ({'#coverArtistContribs': coverArtistContribs}) => +// coverArtistContribs.map(({who}) => who), // }, // }, // ]); @@ -169,7 +169,7 @@ import { // Consider the `withResolvedContribs` example adjusted to make use of // two of these options below: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -194,7 +194,7 @@ import { // With a little destructuring and restructuring JavaScript sugar, the // above can be simplified some more: // -// static Thing.composite.withResolvedContribs = ({from, to}) => ({ +// withResolvedContribs = ({from, to}) => ({ // flags: {expose: true, compose: true}, // expose: { // dependencies: ['artistData'], @@ -281,7 +281,7 @@ import { // // In order to allow for this while helping to ensure internal dependencies // remain neatly isolated from the composition which nests your bundle, -// the Thing.composite.from() function will accept and adapt to a base that +// the compositeFrom() function will accept and adapt to a base that // specifies the {compose: true} flag, just like the steps preceding it. // // The continuation function that gets provided to the base will be mildly @@ -341,8 +341,7 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // -export {compositeFrom as from}; -function compositeFrom(firstArg, secondArg) { +export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -373,7 +372,7 @@ function compositeFrom(firstArg, secondArg) { const aggregate = openAggregate({ message: - `Errors preparing Thing.composite.from() composition` + + `Errors preparing composition` + (annotation ? ` (${annotation})` : ''), }); @@ -780,9 +779,9 @@ function compositeFrom(firstArg, secondArg) { // t.same(thing.someProp, value) // // With debugging: -// t.same(Thing.composite.debug(() => thing.someProp), value) +// t.same(debugComposite(() => thing.someProp), value) // -export function debug(fn) { +export function debugComposite(fn) { compositeFrom.debug = true; const value = fn(); compositeFrom.debug = false; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 93f19799..9b564ee9 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,7 +9,7 @@ import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { - from as compositeFrom, + compositeFrom, exitWithoutDependency, exposeDependency, raiseWithoutDependency, diff --git a/src/data/things/track.js b/src/data/things/track.js index 9e1942e3..1818e003 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -5,7 +5,7 @@ import find from '#find'; import {empty} from '#sugar'; import { - from as compositeFrom, + compositeFrom, exitWithoutDependency, exposeConstant, exposeDependency, -- cgit 1.3.0-6-gf8a5 From ba04498715423c165cdb254676cc211c48b7c8ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:38:16 -0300 Subject: data: remove unused export() raising utility --- src/data/things/composite.js | 38 -------------------------------------- 1 file changed, 38 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b9cd6bfb..f59e7d75 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -788,44 +788,6 @@ export function debugComposite(fn) { return value; } -// Provides dependencies exactly as they are (or null if not defined) to the -// continuation. Although this can *technically* be used to alias existing -// dependencies to some other name within the middle of a composition, it's -// intended to be used only as a composition's base - doing so makes the -// composition as a whole suitable as a step in some other composition, -// providing the listed (internal) dependencies to later steps just like -// other compositional steps. -export {_export as export}; -function _export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `export`, - flags: {expose: true, compose: true}, - - expose: { - options: {mappingEntries}, - dependencies: Object.values(mapping), - - compute({'#options': {mappingEntries}, ...dependencies}, continuation) { - const exports = {}; - - // Note: This is slightly different behavior from filterProperties, - // as defined in sugar.js, which doesn't fall back to null for - // properties which don't exist on the original object. - for (const [exportKey, dependencyKey] of mappingEntries) { - exports[exportKey] = - (Object.hasOwn(dependencies, dependencyKey) - ? dependencies[dependencyKey] - : null); - } - - return continuation.raise(exports); - } - }, - }; -} - // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate -- cgit 1.3.0-6-gf8a5 From 4541b2aa65a2f5ccfb7f9a13d5605311fd8ef801 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 11:37:58 -0300 Subject: data: composite "to" -> "into" --- src/data/things/composite.js | 28 ++++++++++---------- src/data/things/thing.js | 34 ++++++++++++------------ src/data/things/track.js | 62 ++++++++++++++++++++++---------------------- 3 files changed, 62 insertions(+), 62 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f59e7d75..976f7804 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -106,7 +106,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -132,7 +132,7 @@ import { // // withResolvedContribs({ // from: 'coverArtistContribsByRef', -// to: '#coverArtistContribs', +// into: '#coverArtistContribs', // }), // // { @@ -171,7 +171,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -199,11 +199,11 @@ import { // expose: { // dependencies: ['artistData'], // mapDependencies: {from}, -// mapContinuation: {to}, +// mapContinuation: {into}, // compute({artistData, from: contribsByRef}, continuation) { // if (!artistData) return null; // return continuation({ -// to: (..resolve contributions one way or another..), +// into: (..resolve contributions one way or another..), // }); // }, // }, @@ -505,8 +505,8 @@ export function compositeFrom(firstArg, secondArg) { : {}); if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; + for (const [into, from] of Object.entries(mapDependencies)) { + filteredDependencies[into] = availableDependencies[from] ?? null; } } @@ -524,8 +524,8 @@ export function compositeFrom(firstArg, secondArg) { const assignDependencies = {}; - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; + for (const [from, into] of Object.entries(mapContinuation)) { + assignDependencies[into] = continuationAssignment[from] ?? null; } return assignDependencies; @@ -861,7 +861,7 @@ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, mode = 'null', - to = '#availability', + into = '#availability', }) { if (!['null', 'empty', 'falsy'].includes(mode)) { throw new TypeError(`Expected mode to be null, empty, or falsy`); @@ -890,10 +890,10 @@ export function withResultOfAvailabilityCheck({ flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), + continuation({into: checkAvailability(from, mode)}), }, }; } else { @@ -901,10 +901,10 @@ export function withResultOfAvailabilityCheck({ annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), + continuation(value, {into: checkAvailability(value, mode)}), }, }; } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9b564ee9..16003b00 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -236,7 +236,7 @@ export default class Thing extends CacheableObject { return compositeFrom(`Thing.common.dynamicContribs`, [ withResolvedContribs({ from: contribsByRefProperty, - to: '#contribs', + into: '#contribs', }), exposeDependency('#contribs'), @@ -335,12 +335,12 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, to}) { +export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency(from, { mode: 'empty', - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, }), { @@ -355,18 +355,18 @@ export function withResolvedContribs({from, to}) { withResolvedReferenceList({ list: '#whoByRef', data: 'artistData', - to: '#who', + into: '#who', find: find.artist, notFoundMode: 'null', }), { dependencies: ['#who', '#what'], - mapContinuation: {to}, + mapContinuation: {into}, compute({'#who': who, '#what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - to: stitchArrays({who, what}), + into: stitchArrays({who, what}), }); }, }, @@ -383,7 +383,7 @@ export function withResolvedReference({ ref, data, find: findFunction, - to = '#resolvedReference', + into = '#resolvedReference', notFoundMode = 'null', }) { if (!['exit', 'null'].includes(notFoundMode)) { @@ -391,13 +391,13 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), exitWithoutDependency(data), { options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, - mapContinuation: {match: to}, + mapContinuation: {match: into}, compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); @@ -421,7 +421,7 @@ export function withResolvedReferenceList({ list, data, find: findFunction, - to = '#resolvedReferenceList', + into = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { @@ -431,15 +431,15 @@ export function withResolvedReferenceList({ return compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, mode: 'empty', }), { options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: to}, + mapContinuation: {matches: into}, compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = @@ -470,7 +470,7 @@ export function withResolvedReferenceList({ export function withReverseReferenceList({ data, list: refListProperty, - to = '#reverseReferenceList', + into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ exitWithoutDependency(data, {value: []}), @@ -478,12 +478,12 @@ export function withReverseReferenceList({ { dependencies: ['this'], mapDependencies: {data}, - mapContinuation: {to}, + mapContinuation: {into}, options: {refListProperty}, compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), + into: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, ]); diff --git a/src/data/things/track.js b/src/data/things/track.js index 1818e003..1adfe71a 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -216,7 +216,7 @@ export class Track extends Thing { withResolvedContribs({ from: 'artistContribsByRef', - to: '#artistContribs', + into: '#artistContribs', }), { @@ -250,7 +250,7 @@ export class Track extends Thing { withResolvedContribs({ from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', + into: '#coverArtistContribs', }), { @@ -363,20 +363,20 @@ function inheritFromOriginalRelease({ // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. function withAlbum({ - to = '#album', + into = '#album', notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbum`, [ withResultOfAvailabilityCheck({ fromDependency: 'albumData', mode: 'empty', - to: '#albumDataAvailability', + into: '#albumDataAvailability', }), { dependencies: ['#albumDataAvailability'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#albumDataAvailability': albumDataAvailability, @@ -386,7 +386,7 @@ function withAlbum({ ? continuation() : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: null}))), + : continuation.raise({into: null}))), }, { @@ -400,17 +400,17 @@ function withAlbum({ { dependencies: ['#album'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#album': album, '#options': {notFoundMode}, }, continuation) => (album - ? continuation.raise({to: album}) + ? continuation.raise({into: album}) : (notFoundMode === 'exit' ? continuation.exit(null) - : continuation.raise({to: null}))), + : continuation.raise({into: null}))), }, ]); } @@ -420,7 +420,7 @@ function withAlbum({ // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. function withAlbumProperty(property, { - to = '#album.' + property, + into = '#album.' + property, notFoundMode = 'null', } = {}) { return compositeFrom(`withAlbumProperty`, [ @@ -429,15 +429,15 @@ function withAlbumProperty(property, { { dependencies: ['#album'], options: {property}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ '#album': album, '#options': {property}, }, continuation) => (album - ? continuation.raise({to: album[property]}) - : continuation.raise({to: null})), + ? continuation.raise({into: album[property]}) + : continuation.raise({into: null})), }, ]); } @@ -485,7 +485,7 @@ function withAlbumProperties({ // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. function withContainingTrackSection({ - to = '#trackSection', + into = '#trackSection', notFoundMode = 'null', } = {}) { if (!['exit', 'null'].includes(notFoundMode)) { @@ -498,7 +498,7 @@ function withContainingTrackSection({ { dependencies: ['this', '#album.trackSections'], options: {notFoundMode}, - mapContinuation: {to}, + mapContinuation: {into}, compute({ this: track, @@ -506,18 +506,18 @@ function withContainingTrackSection({ '#options': {notFoundMode}, }, continuation) { if (!trackSections) { - return continuation.raise({to: null}); + return continuation.raise({into: null}); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation.raise({to: trackSection}); + return continuation.raise({into: trackSection}); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation.raise({to: null}); + return continuation.raise({into: null}); } }, }, @@ -531,14 +531,14 @@ function withContainingTrackSection({ // specified by reference and that reference doesn't resolve to anything. // Outputs to '#originalRelease' by default. function withOriginalRelease({ - to = '#originalRelease', + into = '#originalRelease', selfIfOriginal = false, } = {}) { return compositeFrom(`withOriginalRelease`, [ withResolvedReference({ ref: 'originalReleaseTrackByRef', data: 'trackData', - to: '#originalRelease', + into: '#originalRelease', find: find.track, notFoundMode: 'exit', }), @@ -546,14 +546,14 @@ function withOriginalRelease({ { dependencies: ['this', '#originalRelease'], options: {selfIfOriginal}, - mapContinuation: {to}, + mapContinuation: {into}, compute: ({ this: track, '#originalRelease': originalRelease, '#options': {selfIfOriginal}, }, continuation) => continuation.raise({ - to: + into: (originalRelease ?? (selfIfOriginal ? track @@ -566,41 +566,41 @@ function withOriginalRelease({ // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. function withHasUniqueCoverArt({ - to = '#hasUniqueCoverArt', + into = '#hasUniqueCoverArt', } = {}) { return compositeFrom(`withHasUniqueCoverArt`, [ { dependencies: ['disableUniqueCoverArt'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({disableUniqueCoverArt}, continuation) => (disableUniqueCoverArt - ? continuation.raise({to: false}) + ? continuation.raise({into: false}) : continuation()), }, withResolvedContribs({ from: 'coverArtistContribsByRef', - to: '#coverArtistContribs', + into: '#coverArtistContribs', }), { dependencies: ['#coverArtistContribs'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => (empty(contribsFromTrack) ? continuation() - : continuation.raise({to: true})), + : continuation.raise({into: true})), }, withAlbumProperty('trackCoverArtistContribs'), { dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {to}, + mapContinuation: {into}, compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => (empty(contribsFromAlbum) - ? continuation.raise({to: false}) - : continuation.raise({to: true})), + ? continuation.raise({into: false}) + : continuation.raise({into: true})), }, ]); } -- cgit 1.3.0-6-gf8a5 From 8b379954c9d74f0d47ac32ef395627353940c728 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 11:56:05 -0300 Subject: data: use key/value-style for all compositional utility args --- src/data/things/composite.js | 25 ++++++++++------- src/data/things/thing.js | 39 ++++++++++++++++++-------- src/data/things/track.js | 65 ++++++++++++++++++++++++++------------------ 3 files changed, 81 insertions(+), 48 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 976f7804..5b6de901 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -801,9 +801,10 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency(dependency, { +export function exposeDependency({ + dependency, update = false, -} = {}) { +}) { return { annotation: `exposeDependency`, flags: {expose: true, update: !!update}, @@ -826,9 +827,10 @@ export function exposeDependency(dependency, { // exit with some other value, with the exposeConstant base serving as the // fallback default value. Like exposeDependency, set {update} to true or // an object to indicate that the property as a whole updates. -export function exposeConstant(value, { +export function exposeConstant({ + value, update = false, -} = {}) { +}) { return { annotation: `exposeConstant`, flags: {expose: true, update: !!update}, @@ -912,9 +914,10 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue(dependency, { +export function exposeDependencyOrContinue({ + dependency, mode = 'null', -} = {}) { +}) { return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, @@ -990,10 +993,11 @@ export function exitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency(dependency, { +export function exitWithoutDependency({ + dependency, mode = 'null', value = null, -} = {}) { +}) { return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), exitIfAvailabilityCheckFailed({value}), @@ -1014,11 +1018,12 @@ export function exitWithoutUpdateValue({ // Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency(dependency, { +export function raiseWithoutDependency({ + dependency, mode = 'null', map = {}, raise = {}, -} = {}) { +}) { return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16003b00..98dec3c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -205,7 +205,8 @@ export default class Thing extends CacheableObject { list, data, find, notFoundMode: 'filter', }), - exposeDependency('#resolvedReferenceList'), + + exposeDependency({dependency: '#resolvedReferenceList'}), ]); }, @@ -213,7 +214,7 @@ export default class Thing extends CacheableObject { resolvedReference({ref, data, find}) { return compositeFrom(`Thing.common.resolvedReference`, [ withResolvedReference({ref, data, find}), - exposeDependency('#resolvedReference'), + exposeDependency({dependency: '#resolvedReference'}), ]); }, @@ -239,7 +240,7 @@ export default class Thing extends CacheableObject { into: '#contribs', }), - exposeDependency('#contribs'), + exposeDependency({dependency: '#contribs'}), ]); }, @@ -265,7 +266,7 @@ export default class Thing extends CacheableObject { reverseReferenceList({data, list}) { return compositeFrom(`Thing.common.reverseReferenceList`, [ withReverseReferenceList({data, list}), - exposeDependency('#reverseReferenceList'), + exposeDependency({dependency: '#reverseReferenceList'}), ]); }, @@ -337,7 +338,8 @@ export default class Thing extends CacheableObject { // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency(from, { + raiseWithoutDependency({ + dependency: from, mode: 'empty', map: {into}, raise: {into: []}, @@ -391,8 +393,15 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), - exitWithoutDependency(data), + raiseWithoutDependency({ + dependency: ref, + map: {into}, + raise: {into: null}, + }), + + exitWithoutDependency({ + dependency: data, + }), { options: {findFunction, notFoundMode}, @@ -429,11 +438,16 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency(data, {value: []}), - raiseWithoutDependency(list, { + exitWithoutDependency({ + dependency: data, + value: [], + }), + + raiseWithoutDependency({ + dependency: list, + mode: 'empty', map: {into}, raise: {into: []}, - mode: 'empty', }), { @@ -473,7 +487,10 @@ export function withReverseReferenceList({ into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - exitWithoutDependency(data, {value: []}), + exitWithoutDependency({ + dependency: data, + value: [], + }), { dependencies: ['this'], diff --git a/src/data/things/track.js b/src/data/things/track.js index 1adfe71a..7dde88db 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -73,8 +73,10 @@ export class Track extends Thing { : continuation()), }, - withAlbumProperty('color'), - exposeDependency('#album.color', { + withAlbumProperty({property: 'color'}), + + exposeDependency({ + dependency: '#album.color', update: {validate: isColor}, }), ]), @@ -93,17 +95,18 @@ export class Track extends Thing { // No cover art file extension if the track doesn't have unique artwork // in the first place. withHasUniqueCoverArt(), - exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - withAlbumProperty('trackCoverArtFileExtension'), - exposeDependencyOrContinue('#album.trackCoverArtFileExtension'), + withAlbumProperty({property: 'trackCoverArtFileExtension'}), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), // Fallback to 'jpg'. - exposeConstant('jpg', { + exposeConstant({ + value: 'jpg', update: {validate: isFileExtension}, }), ]), @@ -114,12 +117,13 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: compositeFrom(`Track.coverArtDate`, [ withHasUniqueCoverArt(), - exitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}), + exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), exposeUpdateValueOrContinue(), - withAlbumProperty('trackArtDate'), - exposeDependency('#album.trackArtDate', { + withAlbumProperty({property: 'trackArtDate'}), + exposeDependency({ + dependency: '#album.trackArtDate', update: {validate: isDate}, }), ]), @@ -148,7 +152,7 @@ export class Track extends Thing { album: compositeFrom(`Track.album`, [ withAlbum(), - exposeDependency('#album'), + exposeDependency({dependency: '#album'}), ]), // Note - this is an internal property used only to help identify a track. @@ -165,9 +169,9 @@ export class Track extends Thing { }), date: compositeFrom(`Track.date`, [ - exposeDependencyOrContinue('dateFirstReleased'), - withAlbumProperty('date'), - exposeDependency('#album.date'), + exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), + withAlbumProperty({property: 'date'}), + exposeDependency({dependency: '#album.date'}), ]), // Whether or not the track has "unique" cover artwork - a cover which is @@ -179,16 +183,16 @@ export class Track extends Thing { // album.) hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ withHasUniqueCoverArt(), - exposeDependency('#hasUniqueCoverArt'), + exposeDependency({dependency: '#hasUniqueCoverArt'}), ]), originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ withOriginalRelease(), - exposeDependency('#originalRelease'), + exposeDependency({dependency: '#originalRelease'}), ]), otherReleases: compositeFrom(`Track.otherReleases`, [ - exitWithoutDependency('trackData', {mode: 'empty'}), + exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), { @@ -227,8 +231,8 @@ export class Track extends Thing { : contribsFromTrack), }, - withAlbumProperty('artistContribs'), - exposeDependency('#album.artistContribs'), + withAlbumProperty({property: 'artistContribs'}), + exposeDependency({dependency: '#album.artistContribs'}), ]), contributorContribs: compositeFrom(`Track.contributorContribs`, [ @@ -261,8 +265,8 @@ export class Track extends Thing { : contribsFromTrack), }, - withAlbumProperty('trackCoverArtistContribs'), - exposeDependency('#album.trackCoverArtistContribs'), + withAlbumProperty({property: 'trackCoverArtistContribs'}), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ]), referencedTracks: compositeFrom(`Track.referencedTracks`, [ @@ -297,10 +301,14 @@ export class Track extends Thing { // counting the number of times a track has been referenced, for use in // the "Tracks - by Times Referenced" listing page (or other data // processing). - referencedByTracks: trackReverseReferenceList('referencedTracks'), + referencedByTracks: trackReverseReferenceList({ + property: 'referencedTracks', + }), // For the same reasoning, exclude re-releases from sampled tracks too. - sampledByTracks: trackReverseReferenceList('sampledTracks'), + sampledByTracks: trackReverseReferenceList({ + property: 'sampledTracks', + }), featuredInFlashes: Thing.common.reverseReferenceList({ data: 'flashData', @@ -419,10 +427,11 @@ function withAlbum({ // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withAlbumProperty(property, { +function withAlbumProperty({ + property, into = '#album.' + property, notFoundMode = 'null', -} = {}) { +}) { return compositeFrom(`withAlbumProperty`, [ withAlbum({notFoundMode}), @@ -493,7 +502,7 @@ function withContainingTrackSection({ } return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty('trackSections', {notFoundMode}), + withAlbumProperty({property: 'trackSections', notFoundMode}), { dependencies: ['this', '#album.trackSections'], @@ -592,7 +601,7 @@ function withHasUniqueCoverArt({ : continuation.raise({into: true})), }, - withAlbumProperty('trackCoverArtistContribs'), + withAlbumProperty({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], @@ -605,7 +614,9 @@ function withHasUniqueCoverArt({ ]); } -function trackReverseReferenceList(refListProperty) { +function trackReverseReferenceList({ + property: refListProperty, +}) { return compositeFrom(`trackReverseReferenceList`, [ withReverseReferenceList({ data: 'trackData', -- cgit 1.3.0-6-gf8a5 From 6889c764caef5542ba9ad8362acf6e8b7b879ea9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 12:06:06 -0300 Subject: data, infra: import validators directly --- src/data/things/album.js | 16 +++------------- src/data/things/art-tag.js | 5 +---- src/data/things/artist.js | 12 ++---------- src/data/things/flash.js | 27 ++++++++++----------------- src/data/things/group.js | 8 ++------ src/data/things/homepage-layout.js | 36 ++++++++++++------------------------ src/data/things/index.js | 3 +-- src/data/things/language.js | 8 +++----- src/data/things/static-page.js | 8 +++----- src/data/things/track.js | 15 ++------------- src/data/things/wiki-info.js | 11 ++--------- 11 files changed, 41 insertions(+), 108 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 81f04f70..da018856 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,23 +1,13 @@ -import {empty} from '#sugar'; import find from '#find'; +import {empty} from '#sugar'; +import {isDate, isDimensions, isTrackSectionList} from '#validators'; import Thing from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; - static [Thing.getPropertyDescriptors] = ({ - ArtTag, - Artist, - Group, - Track, - - validators: { - isDate, - isDimensions, - isTrackSectionList, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Album'), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index bb36e09e..5d7d0cbf 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -5,10 +5,7 @@ import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; - static [Thing.getPropertyDescriptors] = ({ - Album, - Track, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Art Tag'), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index b2383057..93a1b51b 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,20 +1,12 @@ import find from '#find'; +import {isName, validateArrayItems} from '#validators'; import Thing from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; - static [Thing.getPropertyDescriptors] = ({ - Album, - Flash, - Track, - - validators: { - isName, - validateArrayItems, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ // Update & expose name: Thing.common.name('Unnamed Artist'), diff --git a/src/data/things/flash.js b/src/data/things/flash.js index baef23d8..ce2e7fac 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -1,22 +1,19 @@ import find from '#find'; +import { + isColor, + isDirectory, + isNumber, + isString, + oneOf, +} from '#validators'; + import Thing from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; - static [Thing.getPropertyDescriptors] = ({ - Artist, - Track, - FlashAct, - - validators: { - isDirectory, - isNumber, - isString, - oneOf, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ // Update & expose name: Thing.common.name('Unnamed Flash'), @@ -111,11 +108,7 @@ export class Flash extends Thing { } export class FlashAct extends Thing { - static [Thing.getPropertyDescriptors] = ({ - validators: { - isColor, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: Thing.common.name('Unnamed Flash Act'), diff --git a/src/data/things/group.js b/src/data/things/group.js index d04fcf56..6c712847 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -5,9 +5,7 @@ import Thing from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; - static [Thing.getPropertyDescriptors] = ({ - Album, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album}) => ({ // Update & expose name: Thing.common.name('Unnamed Group'), @@ -76,9 +74,7 @@ export class Group extends Thing { } export class GroupCategory extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Group, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Group Category'), diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index c478bc41..59656b41 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,17 +1,18 @@ import find from '#find'; +import { + is, + isCountingNumber, + isString, + isStringNonEmpty, + validateArrayItems, + validateInstanceOf, +} from '#validators'; + import Thing from './thing.js'; export class HomepageLayout extends Thing { - static [Thing.getPropertyDescriptors] = ({ - HomepageLayoutRow, - - validators: { - isStringNonEmpty, - validateArrayItems, - validateInstanceOf, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose sidebarContent: Thing.common.simpleString(), @@ -32,10 +33,7 @@ export class HomepageLayout extends Thing { } export class HomepageLayoutRow extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Album, - Group, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Homepage Row'), @@ -63,17 +61,7 @@ export class HomepageLayoutRow extends Thing { } export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { - static [Thing.getPropertyDescriptors] = (opts, { - Album, - Group, - - validators: { - is, - isCountingNumber, - isString, - validateArrayItems, - }, - } = opts) => ({ + static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), // Update & expose diff --git a/src/data/things/index.js b/src/data/things/index.js index 2d4f77d7..3b73a772 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -4,7 +4,6 @@ import {fileURLToPath} from 'node:url'; import {logError} from '#cli'; import * as serialize from '#serialize'; import {openAggregate, showAggregate} from '#sugar'; -import * as validators from '#validators'; import Thing from './thing.js'; @@ -121,7 +120,7 @@ function descriptorAggregateHelper({ } function evaluatePropertyDescriptors() { - const opts = {...allClasses, validators}; + const opts = {...allClasses}; return descriptorAggregateHelper({ message: `Errors evaluating Thing class property descriptors`, diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..0638afa2 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,11 +1,9 @@ +import {isLanguageCode} from '#validators'; + import Thing from './thing.js'; export class Language extends Thing { - static [Thing.getPropertyDescriptors] = ({ - validators: { - isLanguageCode, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose // General language code. This is used to identify the language distinctly diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 3d8d474c..ae0ca420 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,13 +1,11 @@ +import {isName} from '#validators'; + import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; - static [Thing.getPropertyDescriptors] = ({ - validators: { - isName, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: Thing.common.name('Unnamed Static Page'), diff --git a/src/data/things/track.js b/src/data/things/track.js index 7dde88db..10b966a7 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -3,6 +3,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; import {empty} from '#sugar'; +import {isColor, isDate, isDuration, isFileExtension} from '#validators'; import { compositeFrom, @@ -23,19 +24,7 @@ import Thing, { export class Track extends Thing { static [Thing.referenceType] = 'track'; - static [Thing.getPropertyDescriptors] = ({ - Album, - ArtTag, - Artist, - Flash, - - validators: { - isColor, - isDate, - isDuration, - isFileExtension, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ // Update & expose name: Thing.common.name('Unnamed Track'), diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index d6790c55..0ccef5ed 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,17 +1,10 @@ import find from '#find'; +import {isLanguageCode, isName, isURL} from '#validators'; import Thing from './thing.js'; export class WikiInfo extends Thing { - static [Thing.getPropertyDescriptors] = ({ - Group, - - validators: { - isLanguageCode, - isName, - isURL, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose name: Thing.common.name('Unnamed Wiki'), -- cgit 1.3.0-6-gf8a5 From eb00f2993a1aaaba171ad6c918656552f80bb748 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 12:38:34 -0300 Subject: data: import Thing.common utilities directly Also rename 'color' (from #cli) to 'colors'. --- src/data/things/album.js | 108 ++++--- src/data/things/art-tag.js | 20 +- src/data/things/artist.js | 35 ++- src/data/things/cacheable-object.js | 4 +- src/data/things/composite.js | 22 +- src/data/things/flash.js | 50 ++-- src/data/things/group.js | 37 ++- src/data/things/homepage-layout.js | 29 +- src/data/things/language.js | 12 +- src/data/things/news-entry.js | 15 +- src/data/things/static-page.js | 14 +- src/data/things/thing.js | 554 +++++++++++++++++++----------------- src/data/things/track.js | 83 +++--- src/data/things/validators.js | 6 +- src/data/things/wiki-info.js | 34 ++- src/data/yaml.js | 42 +-- 16 files changed, 599 insertions(+), 466 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index da018856..9cf58641 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -2,7 +2,25 @@ import find from '#find'; import {empty} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; -import Thing from './thing.js'; +import Thing, { + additionalFiles, + commentary, + color, + commentatorArtists, + contribsByRef, + contribsPresent, + directory, + dynamicContribs, + fileExtension, + flag, + name, + resolvedReferenceList, + referenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -10,14 +28,14 @@ export class Album extends Thing { static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Album'), - color: Thing.common.color(), - directory: Thing.common.directory(), - urls: Thing.common.urls(), + name: name('Unnamed Album'), + color: color(), + directory: directory(), + urls: urls(), - date: Thing.common.simpleDate(), - trackArtDate: Thing.common.simpleDate(), - dateAddedToWiki: Thing.common.simpleDate(), + date: simpleDate(), + trackArtDate: simpleDate(), + dateAddedToWiki: simpleDate(), coverArtDate: { flags: {update: true, expose: true}, @@ -36,14 +54,14 @@ export class Album extends Thing { }, }, - artistContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), - trackCoverArtistContribsByRef: Thing.common.contribsByRef(), - wallpaperArtistContribsByRef: Thing.common.contribsByRef(), - bannerArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), + trackCoverArtistContribsByRef: contribsByRef(), + wallpaperArtistContribsByRef: contribsByRef(), + bannerArtistContribsByRef: contribsByRef(), - groupsByRef: Thing.common.referenceList(Group), - artTagsByRef: Thing.common.referenceList(ArtTag), + groupsByRef: referenceList(Group), + artTagsByRef: referenceList(ArtTag), trackSections: { flags: {update: true, expose: true}, @@ -81,58 +99,58 @@ export class Album extends Thing { }, }, - coverArtFileExtension: Thing.common.fileExtension('jpg'), - trackCoverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), + trackCoverArtFileExtension: fileExtension('jpg'), - wallpaperStyle: Thing.common.simpleString(), - wallpaperFileExtension: Thing.common.fileExtension('jpg'), + wallpaperStyle: simpleString(), + wallpaperFileExtension: fileExtension('jpg'), - bannerStyle: Thing.common.simpleString(), - bannerFileExtension: Thing.common.fileExtension('jpg'), + bannerStyle: simpleString(), + bannerFileExtension: fileExtension('jpg'), bannerDimensions: { flags: {update: true, expose: true}, update: {validate: isDimensions}, }, - hasTrackNumbers: Thing.common.flag(true), - isListedOnHomepage: Thing.common.flag(true), - isListedInGalleries: Thing.common.flag(true), + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), - commentary: Thing.common.commentary(), - additionalFiles: Thing.common.additionalFiles(), + commentary: commentary(), + additionalFiles: additionalFiles(), // Update only - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - groupData: Thing.common.wikiData(Group), - trackData: Thing.common.wikiData(Track), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + groupData: wikiData(Group), + trackData: wikiData(Track), // Expose only - artistContribs: Thing.common.dynamicContribs('artistContribsByRef'), - coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'), - trackCoverArtistContribs: Thing.common.dynamicContribs('trackCoverArtistContribsByRef'), - wallpaperArtistContribs: Thing.common.dynamicContribs('wallpaperArtistContribsByRef'), - bannerArtistContribs: Thing.common.dynamicContribs('bannerArtistContribsByRef'), + artistContribs: dynamicContribs('artistContribsByRef'), + coverArtistContribs: dynamicContribs('coverArtistContribsByRef'), + trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'), + wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'), + bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'), - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, }), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, }), - hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), - hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), - hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'), + hasCoverArt: contribsPresent('coverArtistContribsByRef'), + hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'), + hasBannerArt: contribsPresent('bannerArtistContribsByRef'), tracks: { flags: {expose: true}, @@ -192,9 +210,9 @@ export class Album extends Thing { export class TrackSectionHelper extends Thing { static [Thing.getPropertyDescriptors] = () => ({ - name: Thing.common.name('Unnamed Track Group'), - color: Thing.common.color(), - dateOriginallyReleased: Thing.common.simpleDate(), - isDefaultTrackGroup: Thing.common.flag(false), + name: name('Unnamed Track Group'), + color: color(), + dateOriginallyReleased: simpleDate(), + isDefaultTrackGroup: flag(false), }) } diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 5d7d0cbf..3d65b578 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,6 +1,12 @@ import {sortAlbumsTracksChronologically} from '#wiki-data'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + flag, + name, + wikiData, +} from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; @@ -8,10 +14,10 @@ export class ArtTag extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Art Tag'), - directory: Thing.common.directory(), - color: Thing.common.color(), - isContentWarning: Thing.common.flag(false), + name: name('Unnamed Art Tag'), + directory: directory(), + color: color(), + isContentWarning: flag(false), nameShort: { flags: {update: true, expose: true}, @@ -25,8 +31,8 @@ export class ArtTag extends Thing { // Update only - albumData: Thing.common.wikiData(Album), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 93a1b51b..2676591a 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,7 +1,16 @@ import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + fileExtension, + flag, + name, + simpleString, + singleReference, + urls, + wikiData, +} from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; @@ -9,13 +18,13 @@ export class Artist extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Artist'), - directory: Thing.common.directory(), - urls: Thing.common.urls(), - contextNotes: Thing.common.simpleString(), + name: name('Unnamed Artist'), + directory: directory(), + urls: urls(), + contextNotes: simpleString(), - hasAvatar: Thing.common.flag(false), - avatarFileExtension: Thing.common.fileExtension('jpg'), + hasAvatar: flag(false), + avatarFileExtension: fileExtension('jpg'), aliasNames: { flags: {update: true, expose: true}, @@ -23,15 +32,15 @@ export class Artist extends Thing { expose: {transform: (names) => names ?? []}, }, - isAlias: Thing.common.flag(), - aliasedArtistRef: Thing.common.singleReference(Artist), + isAlias: flag(), + aliasedArtistRef: singleReference(Artist), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 62c23d13..92a46d66 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -76,7 +76,7 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -183,7 +183,7 @@ export default class CacheableObject { } } catch (error) { error.message = [ - `Property ${color.green(property)}`, + `Property ${colors.green(property)}`, `(${inspect(this[property])} -> ${inspect(newValue)}):`, error.message ].join(' '); diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 5b6de901..fd52aa0f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import { empty, @@ -346,8 +346,8 @@ export function compositeFrom(firstArg, secondArg) { if (compositeFrom.debug === true) { const label = (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); + ? colors.dim(`[composite: ${annotation}]`) + : colors.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { console.log(label, ...result.map(value => @@ -594,9 +594,9 @@ export function compositeFrom(firstArg, secondArg) { const availableDependencies = {...initialDependencies}; if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { - debug(() => color.bright(`begin composition - not transforming`)); + debug(() => colors.bright(`begin composition - not transforming`)); } for (let i = 0; i < steps.length; i++) { @@ -641,7 +641,7 @@ export function compositeFrom(firstArg, secondArg) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => color.bright(`end composition - exit (inferred)`)); + debug(() => colors.bright(`end composition - exit (inferred)`)); return result; } @@ -652,7 +652,7 @@ export function compositeFrom(firstArg, secondArg) { const {providedValue} = continuationStorage; debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + debug(() => colors.bright(`end composition - exit (explicit)`)); if (baseComposes) { return continuationIfApplicable.exit(providedValue); @@ -708,17 +708,17 @@ export function compositeFrom(firstArg, secondArg) { case 'raise': debug(() => (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); + ? colors.bright(`end composition - raise (base: explicit)`) + : colors.bright(`end composition - raise`))); return continuationIfApplicable(...continuationArgs); case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); + debug(() => colors.bright(`end composition - raiseAbove`)); return continuationIfApplicable.raise(...continuationArgs); case 'continuation': if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raise (inferred)`)); return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, continuingWithDependencies); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index ce2e7fac..4e640dac 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -8,7 +8,19 @@ import { oneOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + dynamicContribs, + color, + contribsByRef, + fileExtension, + name, + referenceList, + resolvedReferenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; @@ -16,7 +28,7 @@ export class Flash extends Thing { static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ // Update & expose - name: Thing.common.name('Unnamed Flash'), + name: name('Unnamed Flash'), directory: { flags: {update: true, expose: true}, @@ -44,27 +56,27 @@ export class Flash extends Thing { }, }, - date: Thing.common.simpleDate(), + date: simpleDate(), - coverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), - contributorContribsByRef: Thing.common.contribsByRef(), + contributorContribsByRef: contribsByRef(), - featuredTracksByRef: Thing.common.referenceList(Track), + featuredTracksByRef: referenceList(Track), - urls: Thing.common.urls(), + urls: urls(), // Update only - artistData: Thing.common.wikiData(Artist), - trackData: Thing.common.wikiData(Track), - flashActData: Thing.common.wikiData(FlashAct), + artistData: wikiData(Artist), + trackData: wikiData(Track), + flashActData: wikiData(FlashAct), // Expose only - contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), + contributorContribs: dynamicContribs('contributorContribsByRef'), - featuredTracks: Thing.common.resolvedReferenceList({ + featuredTracks: resolvedReferenceList({ list: 'featuredTracksByRef', data: 'trackData', find: find.track, @@ -111,10 +123,10 @@ export class FlashAct extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Flash Act'), - color: Thing.common.color(), - anchor: Thing.common.simpleString(), - jump: Thing.common.simpleString(), + name: name('Unnamed Flash Act'), + color: color(), + anchor: simpleString(), + jump: simpleString(), jumpColor: { flags: {update: true, expose: true}, @@ -126,15 +138,15 @@ export class FlashAct extends Thing { } }, - flashesByRef: Thing.common.referenceList(Flash), + flashesByRef: referenceList(Flash), // Update only - flashData: Thing.common.wikiData(Flash), + flashData: wikiData(Flash), // Expose only - flashes: Thing.common.resolvedReferenceList({ + flashes: resolvedReferenceList({ list: 'flashesByRef', data: 'flashData', find: find.flash, diff --git a/src/data/things/group.js b/src/data/things/group.js index 6c712847..873c6d88 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,6 +1,15 @@ import find from '#find'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + name, + referenceList, + resolvedReferenceList, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; @@ -8,23 +17,23 @@ export class Group extends Thing { static [Thing.getPropertyDescriptors] = ({Album}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group'), - directory: Thing.common.directory(), + name: name('Unnamed Group'), + directory: directory(), - description: Thing.common.simpleString(), + description: simpleString(), - urls: Thing.common.urls(), + urls: urls(), - featuredAlbumsByRef: Thing.common.referenceList(Album), + featuredAlbumsByRef: referenceList(Album), // Update only - albumData: Thing.common.wikiData(Album), - groupCategoryData: Thing.common.wikiData(GroupCategory), + albumData: wikiData(Album), + groupCategoryData: wikiData(GroupCategory), // Expose only - featuredAlbums: Thing.common.resolvedReferenceList({ + featuredAlbums: resolvedReferenceList({ list: 'featuredAlbumsByRef', data: 'albumData', find: find.album, @@ -77,18 +86,18 @@ export class GroupCategory extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group Category'), - color: Thing.common.color(), + name: name('Unnamed Group Category'), + color: color(), - groupsByRef: Thing.common.referenceList(Group), + groupsByRef: referenceList(Group), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 59656b41..ab6f4cff 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -9,13 +9,22 @@ import { validateInstanceOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + simpleString, + singleReference, + wikiData, +} from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose - sidebarContent: Thing.common.simpleString(), + sidebarContent: simpleString(), navbarLinks: { flags: {update: true, expose: true}, @@ -36,7 +45,7 @@ export class HomepageLayoutRow extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Homepage Row'), + name: name('Unnamed Homepage Row'), type: { flags: {update: true, expose: true}, @@ -48,15 +57,15 @@ export class HomepageLayoutRow extends Thing { }, }, - color: Thing.common.color(), + color: color(), // Update only // These aren't necessarily used by every HomepageLayoutRow subclass, but // for convenience of providing this data, every row accepts all wiki data // arrays depended upon by any subclass's behavior. - albumData: Thing.common.wikiData(Album), - groupData: Thing.common.wikiData(Group), + albumData: wikiData(Album), + groupData: wikiData(Group), }); } @@ -92,8 +101,8 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroupByRef: Thing.common.singleReference(Group), - sourceAlbumsByRef: Thing.common.referenceList(Album), + sourceGroupByRef: singleReference(Group), + sourceAlbumsByRef: referenceList(Album), countAlbumsFromGroup: { flags: {update: true, expose: true}, @@ -107,13 +116,13 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { // Expose only - sourceGroup: Thing.common.resolvedReference({ + sourceGroup: resolvedReference({ ref: 'sourceGroupByRef', data: 'groupData', find: find.group, }), - sourceAlbums: Thing.common.resolvedReferenceList({ + sourceAlbums: resolvedReferenceList({ list: 'sourceAlbumsByRef', data: 'albumData', find: find.album, diff --git a/src/data/things/language.js b/src/data/things/language.js index 0638afa2..c98495dc 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,6 +1,10 @@ import {isLanguageCode} from '#validators'; -import Thing from './thing.js'; +import Thing, { + externalFunction, + flag, + simpleString, +} from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -16,7 +20,7 @@ export class Language extends Thing { // Human-readable name. This should be the language's own native name, not // localized to any other language. - name: Thing.common.simpleString(), + name: simpleString(), // Language code specific to JavaScript's Internationalization (Intl) API. // Usually this will be the same as the language's general code, but it @@ -38,7 +42,7 @@ export class Language extends Thing { // with languages that are currently in development and not ready for // formal release, or which are just kept hidden as "experimental zones" // for wiki development or content testing. - hidden: Thing.common.flag(false), + hidden: flag(false), // Mapping of translation keys to values (strings). Generally, don't // access this object directly - use methods instead. @@ -66,7 +70,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: externalFunction(), // Expose only diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 43911410..6984874e 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,4 +1,9 @@ -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleDate, + simpleString, +} from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; @@ -6,11 +11,11 @@ export class NewsEntry extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed News Entry'), - directory: Thing.common.directory(), - date: Thing.common.simpleDate(), + name: name('Unnamed News Entry'), + directory: directory(), + date: simpleDate(), - content: Thing.common.simpleString(), + content: simpleString(), // Expose only diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index ae0ca420..0133e0b6 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,6 +1,10 @@ import {isName} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleString, +} from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; @@ -8,7 +12,7 @@ export class StaticPage extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Static Page'), + name: name('Unnamed Static Page'), nameShort: { flags: {update: true, expose: true}, @@ -20,8 +24,8 @@ export class StaticPage extends Thing { }, }, - directory: Thing.common.directory(), - content: Thing.common.simpleString(), - stylesheet: Thing.common.simpleString(), + directory: directory(), + content: simpleString(), + stylesheet: simpleString(), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 98dec3c3..19f00b3e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -3,7 +3,7 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; @@ -41,297 +41,329 @@ export default class Thing extends CacheableObject { static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); - // Regularly reused property descriptors, for ease of access and generally - // duplicating less code across wiki data types. These are specialized utility - // functions, so check each for how its own arguments behave! - static common = { - name: (defaultName) => ({ - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }), + // Default custom inspect function, which may be overridden by Thing + // subclasses. This will be used when displaying aggregate errors and other + // command-line logging - it's the place to provide information useful in + // identifying the Thing being presented. + [inspect.custom]() { + const cname = this.constructor.name; - color: () => ({ - flags: {update: true, expose: true}, - update: {validate: isColor}, - }), + return ( + (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) + + (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '') + ); + } - directory: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }), + static getReference(thing) { + if (!thing.constructor[Thing.referenceType]) { + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + } - urls: () => ({ - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }), + if (!thing.directory) { + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + } - // A file extension! Or the default, if provided when calling this. - fileExtension: (defaultFileExtension = null) => ({ - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }), + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; + } +} + +// Property descriptor templates +// +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! + +export function name(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} + +export function color() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} - // Straightforward flag descriptor for a variety of property purposes. - // Provide a default value, true or false! - flag: (defaultValue = false) => { - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; +export function directory() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, }, + }; +} - // General date type, used as the descriptor for a bunch of properties. - // This isn't dynamic though - it won't inherit from a date stored on - // another object, for example. - simpleDate: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDate}, - }), +export function urls() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: (value) => value ?? []}, + }; +} - // General string type. This should probably generally be avoided in favor - // of more specific validation, but using it makes it easy to find where we - // might want to improve later, and it's a useful shorthand meanwhile. - simpleString: () => ({ - flags: {update: true, expose: true}, - update: {validate: isString}, - }), +// A file extension! Or the default, if provided when calling this. +export function fileExtension(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} - // External function. These should only be used as dependencies for other - // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }), +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! +export function flag(defaultValue = false) { + // TODO: ^ Are you actually kidding me + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } - // Super simple "contributions by reference" list, used for a variety of - // properties (Artists, Cover Artists, etc). This is the property which is - // externally provided, in the form: - // - // [ - // {who: 'Artist Name', what: 'Viola'}, - // {who: 'artist:john-cena', what: null}, - // ... - // ] - // - // ...processed from YAML, spreadsheet, or any other kind of input. - contribsByRef: () => ({ - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }), + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} - // Artist commentary! Generally present on tracks and albums. - commentary: () => ({ - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }), +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. +export function simpleDate() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} - // This is a somewhat more involved data structure - it's for additional - // or "bonus" files associated with albums or tracks (or anything else). - // It's got this form: - // - // [ - // {title: 'Booklet', files: ['Booklet.pdf']}, - // { - // title: 'Wallpaper', - // description: 'Cool Wallpaper!', - // files: ['1440x900.png', '1920x1080.png'] - // }, - // {title: 'Alternate Covers', description: null, files: [...]}, - // ... - // ] - // - additionalFiles: () => ({ - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }), +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. +export function simpleString() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} - // A reference list! Keep in mind this is for general references to wiki - // objects of (usually) other Thing subclasses, not specifically leitmotif - // references in tracks (although that property uses referenceList too!). - // - // The underlying function validateReferenceList expects a string like - // 'artist' or 'track', but this utility keeps from having to hard-code the - // string in multiple places by referencing the value saved on the class - // instead. - referenceList: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; - }, +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. +export function externalFunction() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} - // Corresponding function for a single reference. - singleReference: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; - }, +// Super simple "contributions by reference" list, used for a variety of +// properties (Artists, Cover Artists, etc). This is the property which is +// externally provided, in the form: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...processed from YAML, spreadsheet, or any other kind of input. +export function contribsByRef() { + return { + flags: {update: true, expose: true}, + update: {validate: isContributionList}, + }; +} - // Corresponding dynamic property to referenceList, which takes the values - // in the provided property and searches the specified wiki data for - // matching actual Thing-subclass objects. - resolvedReferenceList({list, data, find}) { - return compositeFrom(`Thing.common.resolvedReferenceList`, [ - withResolvedReferenceList({ - list, data, find, - notFoundMode: 'filter', - }), +// Artist commentary! Generally present on tracks and albums. +export function commentary() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} - exposeDependency({dependency: '#resolvedReferenceList'}), - ]); +// This is a somewhat more involved data structure - it's for additional +// or "bonus" files associated with albums or tracks (or anything else). +// It's got this form: +// +// [ +// {title: 'Booklet', files: ['Booklet.pdf']}, +// { +// title: 'Wallpaper', +// description: 'Cool Wallpaper!', +// files: ['1440x900.png', '1920x1080.png'] +// }, +// {title: 'Alternate Covers', description: null, files: [...]}, +// ... +// ] +// +export function additionalFiles() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], }, + }; +} - // Corresponding function for a single reference. - resolvedReference({ref, data, find}) { - return compositeFrom(`Thing.common.resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); - }, +// A reference list! Keep in mind this is for general references to wiki +// objects of (usually) other Thing subclasses, not specifically leitmotif +// references in tracks (although that property uses referenceList too!). +// +// The underlying function validateReferenceList expects a string like +// 'artist' or 'track', but this utility keeps from having to hard-code the +// string in multiple places by referencing the value saved on the class +// instead. +export function referenceList(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Corresponding dynamic property to contribsByRef, which takes the values - // in the provided property and searches the object's artistData for - // matching actual Artist objects. The computed structure has the same form - // as contribsByRef, but with Artist objects instead of string references: - // - // [ - // {who: (an Artist), what: 'Viola'}, - // {who: (an Artist), what: null}, - // ... - // ] - // - // Contributions whose "who" values don't match anything in artistData are - // filtered out. (So if the list is all empty, chances are that either the - // reference list is somehow messed up, or artistData isn't being provided - // properly.) - dynamicContribs(contribsByRefProperty) { - return compositeFrom(`Thing.common.dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), + return { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList(referenceType)}, + }; +} - exposeDependency({dependency: '#contribs'}), - ]); - }, +// Corresponding function for a single reference. +export function singleReference(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Nice 'n simple shorthand for an exposed-only flag which is true when any - // contributions are present in the specified property. - contribsPresent: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + return { + flags: {update: true, expose: true}, + update: {validate: validateReference(referenceType)}, + }; +} + +// Corresponding dynamic property to referenceList, which takes the values +// in the provided property and searches the specified wiki data for +// matching actual Thing-subclass objects. +export function resolvedReferenceList({list, data, find}) { + return compositeFrom(`resolvedReferenceList`, [ + withResolvedReferenceList({ + list, data, find, + notFoundMode: 'filter', }), - // Neat little shortcut for "reversing" the reference lists stored on other - // things - for example, tracks specify a "referenced tracks" property, and - // you would use this to compute a corresponding "referenced *by* tracks" - // property. Naturally, the passed ref list property is of the things in the - // wiki data provided, not the requesting Thing itself. - reverseReferenceList({data, list}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); - }, + exposeDependency({dependency: '#resolvedReferenceList'}), + ]); +} - // General purpose wiki data constructor, for properties like artistData, - // trackData, etc. - wikiData: (thingClass) => ({ - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }), +// Corresponding function for a single reference. +export function resolvedReference({ref, data, find}) { + return compositeFrom(`resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency({dependency: '#resolvedReference'}), + ]); +} - // This one's kinda tricky: it parses artist "references" from the - // commentary content, and finds the matching artist for each reference. - // This is mostly useful for credits and listings on artist pages. - commentatorArtists: () => ({ - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], - }, +// Corresponding dynamic property to contribsByRef, which takes the values +// in the provided property and searches the object's artistData for +// matching actual Artist objects. The computed structure has the same form +// as contribsByRef, but with Artist objects instead of string references: +// +// [ +// {who: (an Artist), what: 'Viola'}, +// {who: (an Artist), what: null}, +// ... +// ] +// +// Contributions whose "who" values don't match anything in artistData are +// filtered out. (So if the list is all empty, chances are that either the +// reference list is somehow messed up, or artistData isn't being provided +// properly.) +export function dynamicContribs(contribsByRefProperty) { + return compositeFrom(`dynamicContribs`, [ + withResolvedContribs({ + from: contribsByRefProperty, + into: '#contribs', }), - }; - - // Default custom inspect function, which may be overridden by Thing - // subclasses. This will be used when displaying aggregate errors and other - // command-line logging - it's the place to provide information useful in - // identifying the Thing being presented. - [inspect.custom]() { - const cname = this.constructor.name; - return ( - (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) + - (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '') - ); - } + exposeDependency({dependency: '#contribs'}), + ]); +} - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. +export function contribsPresent(contribsByRefProperty) { + return { + flags: {expose: true}, + expose: { + dependencies: [contribsByRefProperty], + compute({ + [contribsByRefProperty]: contribsByRef, + }) { + return !empty(contribsByRef); + }, } + }; +} - if (!thing.directory) { - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - } +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. +export function reverseReferenceList({data, list}) { + return compositeFrom(`reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency({dependency: '#reverseReferenceList'}), + ]); +} - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. +export function wikiData(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; } +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. +export function commentatorArtists(){ + return { + flags: {expose: true}, + + expose: { + dependencies: ['artistData', 'commentary'], + + compute: ({artistData, commentary}) => + artistData && commentary + ? Array.from( + new Set( + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g) + ).map(({groups: {who}}) => + find.artist(who, artistData, {mode: 'quiet'}) + ) + ) + ) + : [], + }, + }; +} + +// Compositional utilities + // 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 @@ -479,14 +511,14 @@ export function withResolvedReferenceList({ ]); } -// Check out the info on Thing.common.reverseReferenceList! +// Check out the info on reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ data, list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ + return compositeFrom(`withReverseReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], diff --git a/src/data/things/track.js b/src/data/things/track.js index 10b966a7..41c92092 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty} from '#sugar'; import {isColor, isDate, isDuration, isFileExtension} from '#validators'; @@ -16,6 +16,23 @@ import { } from '#composite'; import Thing, { + additionalFiles, + commentary, + commentatorArtists, + contribsByRef, + directory, + dynamicContribs, + flag, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + reverseReferenceList, + simpleDate, + singleReference, + simpleString, + urls, + wikiData, withResolvedContribs, withResolvedReference, withReverseReferenceList, @@ -27,24 +44,24 @@ export class Track extends Thing { static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ // Update & expose - name: Thing.common.name('Unnamed Track'), - directory: Thing.common.directory(), + name: name('Unnamed Track'), + directory: directory(), duration: { flags: {update: true, expose: true}, update: {validate: isDuration}, }, - urls: Thing.common.urls(), - dateFirstReleased: Thing.common.simpleDate(), + urls: urls(), + dateFirstReleased: simpleDate(), - artistContribsByRef: Thing.common.contribsByRef(), - contributorContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + contributorContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), - referencedTracksByRef: Thing.common.referenceList(Track), - sampledTracksByRef: Thing.common.referenceList(Track), - artTagsByRef: Thing.common.referenceList(ArtTag), + referencedTracksByRef: referenceList(Track), + sampledTracksByRef: referenceList(Track), + artTagsByRef: referenceList(ArtTag), color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), @@ -74,7 +91,7 @@ export class Track extends Thing { // This flag should only be used in select circumstances, i.e. to override // an album's trackCoverArtists. This flag supercedes that property, as well // as the track's own coverArtists. - disableUniqueCoverArt: Thing.common.flag(), + disableUniqueCoverArt: flag(), // File extension for track's corresponding media file. This represents the // track's unique cover artwork, if any, and does not inherit the extension @@ -117,27 +134,27 @@ export class Track extends Thing { }), ]), - originalReleaseTrackByRef: Thing.common.singleReference(Track), + originalReleaseTrackByRef: singleReference(Track), - dataSourceAlbumByRef: Thing.common.singleReference(Album), + dataSourceAlbumByRef: singleReference(Album), - commentary: Thing.common.commentary(), - lyrics: Thing.common.simpleString(), - additionalFiles: Thing.common.additionalFiles(), - sheetMusicFiles: Thing.common.additionalFiles(), - midiProjectFiles: Thing.common.additionalFiles(), + commentary: commentary(), + lyrics: simpleString(), + additionalFiles: additionalFiles(), + sheetMusicFiles: additionalFiles(), + midiProjectFiles: additionalFiles(), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), album: compositeFrom(`Track.album`, [ withAlbum(), @@ -151,7 +168,7 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.resolvedReference({ + dataSourceAlbum: resolvedReference({ ref: 'dataSourceAlbumByRef', data: 'albumData', find: find.album, @@ -226,7 +243,7 @@ export class Track extends Thing { contributorContribs: compositeFrom(`Track.contributorContribs`, [ inheritFromOriginalRelease({property: 'contributorContribs'}), - Thing.common.dynamicContribs('contributorContribsByRef'), + dynamicContribs('contributorContribsByRef'), ]), // Cover artists aren't inherited from the original release, since it @@ -260,7 +277,7 @@ export class Track extends Thing { referencedTracks: compositeFrom(`Track.referencedTracks`, [ inheritFromOriginalRelease({property: 'referencedTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'referencedTracksByRef', data: 'trackData', find: find.track, @@ -269,14 +286,14 @@ export class Track extends Thing { sampledTracks: compositeFrom(`Track.sampledTracks`, [ inheritFromOriginalRelease({property: 'sampledTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'sampledTracksByRef', data: 'trackData', find: find.track, }), ]), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, @@ -299,7 +316,7 @@ export class Track extends Thing { property: 'sampledTracks', }), - featuredInFlashes: Thing.common.reverseReferenceList({ + featuredInFlashes: reverseReferenceList({ data: 'flashData', list: 'featuredTracks', }), @@ -311,7 +328,7 @@ export class Track extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); if (this.originalReleaseTrackByRef) { - parts.unshift(`${color.yellow('[rerelease]')} `); + parts.unshift(`${colors.yellow('[rerelease]')} `); } let album; @@ -322,7 +339,7 @@ export class Track extends Thing { (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`); - parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + parts.push(` (${colors.yellow(trackNum)} in ${colors.green(albumName)})`); } return parts.join(''); diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 5748eacf..4c8f683b 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,6 +1,6 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; import {withAggregate} from '#sugar'; function inspect(value) { @@ -174,7 +174,7 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${color.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${colors.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; throw error; } }; @@ -264,7 +264,7 @@ export function validateProperties(spec) { try { specValidator(value); } catch (error) { - error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`; + error.message = `(key: ${colors.green(specKey)}, value: ${inspect(value)}) ${error.message}`; throw error; } }); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 0ccef5ed..416b6c4e 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,13 +1,21 @@ import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + flag, + name, + referenceList, + resolvedReferenceList, + simpleString, + wikiData, +} from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Wiki'), + name: name('Unnamed Wiki'), // Displayed in nav bar. nameShort: { @@ -20,12 +28,12 @@ export class WikiInfo extends Thing { }, }, - color: Thing.common.color(), + color: color(), // One-line description used for tag. - description: Thing.common.simpleString(), + description: simpleString(), - footerContent: Thing.common.simpleString(), + footerContent: simpleString(), defaultLanguage: { flags: {update: true, expose: true}, @@ -37,22 +45,22 @@ export class WikiInfo extends Thing { update: {validate: isURL}, }, - divideTrackListsByGroupsByRef: Thing.common.referenceList(Group), + divideTrackListsByGroupsByRef: referenceList(Group), // Feature toggles - enableFlashesAndGames: Thing.common.flag(false), - enableListings: Thing.common.flag(false), - enableNews: Thing.common.flag(false), - enableArtTagUI: Thing.common.flag(false), - enableGroupUI: Thing.common.flag(false), + enableFlashesAndGames: flag(false), + enableListings: flag(false), + enableNews: flag(false), + enableArtTagUI: flag(false), + enableGroupUI: flag(false), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - divideTrackListsByGroups: Thing.common.resolvedReferenceList({ + divideTrackListsByGroups: resolvedReferenceList({ list: 'divideTrackListsByGroupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/yaml.js b/src/data/yaml.js index 2ad2d41d..c0aad943 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -7,7 +7,7 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; -import {color, ENABLE_COLOR, logInfo, logWarn} from '#cli'; +import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; import T from '#things'; @@ -137,7 +137,7 @@ function makeProcessDocument( const name = document[nameField]; error.message = name ? `(name: ${inspect(name)}) ${error.message}` - : `(${color.dim(`no name found`)}) ${error.message}`; + : `(${colors.dim(`no name found`)}) ${error.message}`; throw error; } }; @@ -195,7 +195,7 @@ function makeProcessDocument( const thing = Reflect.construct(thingClass, []); - withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => { + withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => { for (const [property, value] of Object.entries(sourceProperties)) { call(() => (thing[property] = value)); } @@ -228,7 +228,7 @@ makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extend makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const combinePart = `Don't combine ${fieldNames.map(field => color.red(field)).join(', ')}`; + const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; const messagePart = (typeof message === 'function' @@ -1009,7 +1009,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } catch (error) { error.message += (error.message.includes('\n') ? '\n' : ' ') + - `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`; + `(file: ${colors.bright(colors.blue(path.relative(dataPath, x.file)))})`; throw error; } }; @@ -1032,7 +1032,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { // just without the callbacks. Thank you. const filterBlankDocuments = documents => { const aggregate = openAggregate({ - message: `Found blank documents - check for extra '${color.cyan(`---`)}'`, + message: `Found blank documents - check for extra '${colors.cyan(`---`)}'`, }); const filteredDocuments = @@ -1076,10 +1076,10 @@ export async function loadAndProcessDataDocuments({dataPath}) { if (count === 1) { const range = `#${start + 1}`; - parts.push(`${count} document (${color.yellow(range)}), `); + parts.push(`${count} document (${colors.yellow(range)}), `); } else { const range = `#${start + 1}-${end + 1}`; - parts.push(`${count} documents (${color.yellow(range)}), `); + parts.push(`${count} documents (${colors.yellow(range)}), `); } if (previous === null) { @@ -1089,7 +1089,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } else { const previousDescription = Object.entries(previous).at(0).join(': '); const nextDescription = Object.entries(next).at(0).join(': '); - parts.push(`between "${color.cyan(previousDescription)}" and "${color.cyan(nextDescription)}"`); + parts.push(`between "${colors.cyan(previousDescription)}" and "${colors.cyan(nextDescription)}"`); } aggregate.push(new Error(parts.join(''))); @@ -1395,7 +1395,7 @@ export function filterDuplicateDirectories(wikiData) { const aggregate = openAggregate({message: `Duplicate directories found`}); for (const thingDataProp of deduplicateSpec) { const thingData = wikiData[thingDataProp]; - aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({call}) => { + aggregate.nest({message: `Duplicate directories found in ${colors.green('wikiData.' + thingDataProp)}`}, ({call}) => { const directoryPlaces = Object.create(null); const duplicateDirectories = []; @@ -1421,7 +1421,7 @@ export function filterDuplicateDirectories(wikiData) { const places = directoryPlaces[directory]; call(() => { throw new Error( - `Duplicate directory ${color.green(directory)}:\n` + + `Duplicate directory ${colors.green(directory)}:\n` + places.map((thing) => ` - ` + inspect(thing)).join('\n') ); }); @@ -1516,7 +1516,7 @@ export function filterReferenceErrors(wikiData) { for (const [thingDataProp, providedProcessDocumentFn, propSpec] of referenceSpec) { const thingData = getNestedProp(wikiData, thingDataProp); - aggregate.nest({message: `Reference errors in ${color.green('wikiData.' + thingDataProp)}`}, ({nest}) => { + aggregate.nest({message: `Reference errors in ${colors.green('wikiData.' + thingDataProp)}`}, ({nest}) => { const things = Array.isArray(thingData) ? thingData : [thingData]; for (const thing of things) { @@ -1535,7 +1535,7 @@ export function filterReferenceErrors(wikiData) { const value = thing[property]; if (value === undefined) { - push(new TypeError(`Property ${color.red(property)} isn't valid for ${color.green(thing.constructor.name)}`)); + push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); continue; } @@ -1553,7 +1553,7 @@ export function filterReferenceErrors(wikiData) { // No need to check if the original exists here. Aliases are automatically // created from a field on the original, so the original certainly exists. const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'}); - throw new Error(`Reference ${color.red(contribRef.who)} is to an alias, should be ${color.green(original.name)}`); + throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); } return boundFind.artist(contribRef.who); @@ -1578,12 +1578,12 @@ export function filterReferenceErrors(wikiData) { const shouldBeMessage = (originalByName - ? color.green(original.name) + ? colors.green(original.name) : original - ? color.green('track:' + original.directory) - : color.green(track.originalReleaseTrackByRef)); + ? colors.green('track:' + original.directory) + : colors.green(track.originalReleaseTrackByRef)); - throw new Error(`Reference ${color.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); + throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); } return track; @@ -1614,13 +1614,13 @@ export function filterReferenceErrors(wikiData) { const fieldPropertyMessage = (processDocumentFn?.propertyFieldMapping?.[property] - ? ` in field ${color.green(processDocumentFn.propertyFieldMapping[property])}` - : ` in property ${color.green(property)}`); + ? ` in field ${colors.green(processDocumentFn.propertyFieldMapping[property])}` + : ` in property ${colors.green(property)}`); const findFnMessage = (findFnKey.startsWith('_') ? `` - : ` (${color.green('find.' + findFnKey)})`); + : ` (${colors.green('find.' + findFnKey)})`); const errorMessage = (Array.isArray(value) -- cgit 1.3.0-6-gf8a5 From d33effa272c3388640974648fe2888a284c6701c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 14:50:41 -0300 Subject: data: withAlbum: perform proper availability check on album --- src/data/things/composite.js | 5 +++-- src/data/things/track.js | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fd52aa0f..29f5770c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -853,8 +853,9 @@ export function exposeConstant({ // consider using instead. Customize {mode} to select one of these modes, // or leave unset and default to 'null': // -// * 'null': Check that the value isn't null. +// * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. +// This will outright error for undefined. // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! @@ -879,7 +880,7 @@ export function withResultOfAvailabilityCheck({ const checkAvailability = (value, mode) => { switch (mode) { - case 'null': return value !== null; + case 'null': return value !== null && value !== undefined; case 'empty': return !empty(value); case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; diff --git a/src/data/things/track.js b/src/data/things/track.js index 41c92092..fcfd39c7 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -411,21 +411,34 @@ function withAlbum({ }), }, + withResultOfAvailabilityCheck({ + fromDependency: '#album', + mode: 'null', + into: '#albumAvailability', + }), + { - dependencies: ['#album'], + dependencies: ['#albumAvailability'], options: {notFoundMode}, mapContinuation: {into}, compute: ({ - '#album': album, + '#albumAvailability': albumAvailability, '#options': {notFoundMode}, }, continuation) => - (album - ? continuation.raise({into: album}) + (albumAvailability + ? continuation() : (notFoundMode === 'exit' ? continuation.exit(null) : continuation.raise({into: null}))), }, + + { + dependencies: ['#album'], + mapContinuation: {into}, + compute: ({'#album': album}, continuation) => + continuation({into: album}), + }, ]); } -- cgit 1.3.0-6-gf8a5 From 9db4b91c66f8b9b98d098bfe446e29f5b3caee53 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 14:53:25 -0300 Subject: data: withResolvedContribs: use default "into" --- src/data/things/thing.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f00b3e..9d8b2ea2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -286,12 +286,8 @@ export function resolvedReference({ref, data, find}) { // properly.) export function dynamicContribs(contribsByRefProperty) { return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), - - exposeDependency({dependency: '#contribs'}), + withResolvedContribs({from: contribsByRefProperty}), + exposeDependency({dependency: '#resolvedContribs'}), ]); } @@ -368,7 +364,10 @@ export function commentatorArtists(){ // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, into}) { +export function withResolvedContribs({ + from, + into = '#resolvedContribs', +}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency({ dependency: from, -- cgit 1.3.0-6-gf8a5 From a24a72339f6e6e416a797d869fe9c4d9057fcac0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 17:23:54 -0300 Subject: data: custom _homepageSourceGroup reference validation function --- src/data/yaml.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index c0aad943..8aca3299 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1490,7 +1490,7 @@ export function filterReferenceErrors(wikiData) { }], ['homepageLayout.rows', undefined, { - sourceGroupByRef: 'group', + sourceGroupByRef: '_homepageSourceGroup', sourceAlbumsByRef: 'album', }], @@ -1560,6 +1560,16 @@ export function filterReferenceErrors(wikiData) { }; break; + case '_homepageSourceGroup': + findFn = groupRef => { + if (groupRef === 'new-additions' || groupRef === 'new-releases') { + return true; + } + + return boundFind.group(groupRef); + }; + break; + case '_trackNotRerelease': findFn = trackRef => { const track = find.track(trackRef, wikiData.trackData, {mode: 'error'}); -- cgit 1.3.0-6-gf8a5 From bbccaf51222cb4bed73466164496f5bc1030292c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 17:30:54 -0300 Subject: data: roll paired "byRef" and "dynamic" properties into one --- src/data/things/album.js | 75 ++++++++---------- src/data/things/artist.js | 19 ++--- src/data/things/cacheable-object.js | 26 ++++--- src/data/things/composite.js | 15 ++++ src/data/things/flash.js | 34 +++------ src/data/things/group.js | 27 +++---- src/data/things/homepage-layout.js | 60 ++++++++++----- src/data/things/thing.js | 147 ++++++++++++++++++------------------ src/data/things/track.js | 132 ++++++++++++++------------------ src/data/things/validators.js | 2 +- src/data/things/wiki-info.js | 15 ++-- src/data/yaml.js | 133 ++++++++++++++++---------------- 12 files changed, 335 insertions(+), 350 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 9cf58641..88308182 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -7,14 +7,12 @@ import Thing, { commentary, color, commentatorArtists, - contribsByRef, contribsPresent, + contributionList, directory, - dynamicContribs, fileExtension, flag, name, - resolvedReferenceList, referenceList, simpleDate, simpleString, @@ -43,25 +41,31 @@ export class Album extends Thing { update: {validate: isDate}, expose: { - dependencies: ['date', 'coverArtistContribsByRef'], - transform: (coverArtDate, { - coverArtistContribsByRef, - date, - }) => - (!empty(coverArtistContribsByRef) + dependencies: ['date', 'coverArtistContribs'], + transform: (coverArtDate, {coverArtistContribs, date}) => + (!empty(coverArtistContribs) ? coverArtDate ?? date ?? null : null), }, }, - artistContribsByRef: contribsByRef(), - coverArtistContribsByRef: contribsByRef(), - trackCoverArtistContribsByRef: contribsByRef(), - wallpaperArtistContribsByRef: contribsByRef(), - bannerArtistContribsByRef: contribsByRef(), + artistContribs: contributionList(), + coverArtistContribs: contributionList(), + trackCoverArtistContribs: contributionList(), + wallpaperArtistContribs: contributionList(), + bannerArtistContribs: contributionList(), - groupsByRef: referenceList(Group), - artTagsByRef: referenceList(ArtTag), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), + + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), trackSections: { flags: {update: true, expose: true}, @@ -84,13 +88,12 @@ export class Album extends Thing { isDefaultTrackSection: section.isDefaultTrackSection ?? false, startIndex: ( - startIndex += section.tracksByRef.length, - startIndex - section.tracksByRef.length + startIndex += section.tracks.length, + startIndex - section.tracks.length ), - tracksByRef: section.tracksByRef ?? [], tracks: - (trackData && section.tracksByRef + (trackData && section.tracks ?.map(ref => find.track(ref, trackData, {mode: 'quiet'})) .filter(Boolean)) ?? [], @@ -128,29 +131,11 @@ export class Album extends Thing { // Expose only - artistContribs: dynamicContribs('artistContribsByRef'), - coverArtistContribs: dynamicContribs('coverArtistContribsByRef'), - trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'), - wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'), - bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'), - commentatorArtists: commentatorArtists(), - groups: resolvedReferenceList({ - list: 'groupsByRef', - data: 'groupData', - find: find.group, - }), - - artTags: resolvedReferenceList({ - list: 'artTagsByRef', - data: 'artTagData', - find: find.artTag, - }), - - hasCoverArt: contribsPresent('coverArtistContribsByRef'), - hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'), - hasBannerArt: contribsPresent('bannerArtistContribsByRef'), + hasCoverArt: contribsPresent('coverArtistContribs'), + hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), + hasBannerArt: contribsPresent('bannerArtistContribs'), tracks: { flags: {expose: true}, @@ -158,12 +143,12 @@ export class Album extends Thing { expose: { dependencies: ['trackSections', 'trackData'], compute: ({trackSections, trackData}) => - trackSections && trackData + (trackSections && trackData ? trackSections - .flatMap((section) => section.tracksByRef ?? []) - .map((ref) => find.track(ref, trackData, {mode: 'quiet'})) + .flatMap(section => section.tracks ?? []) + .map(ref => find.track(ref, trackData, {mode: 'quiet'})) .filter(Boolean) - : [], + : []), }, }, }); diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 2676591a..7a9dbd3c 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -33,7 +33,12 @@ export class Artist extends Thing { }, isAlias: flag(), - aliasedArtistRef: singleReference(Artist), + + aliasedArtist: singleReference({ + class: Artist, + find: find.artist, + data: 'artistData', + }), // Update only @@ -44,18 +49,6 @@ export class Artist extends Thing { // Expose only - aliasedArtist: { - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'aliasedArtistRef'], - compute: ({artistData, aliasedArtistRef}) => - aliasedArtistRef && artistData - ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'}) - : null, - }, - }, - tracksAsArtist: Artist.filterByContrib('trackData', 'artistContribs'), tracksAsContributor: diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 92a46d66..4bc3668d 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -86,16 +86,14 @@ export default class CacheableObject { #propertyUpdateValues = Object.create(null); #propertyUpdateCacheInvalidators = Object.create(null); - /* - // Note the constructor doesn't take an initial data source. Due to a quirk - // of JavaScript, private members can't be accessed before the superclass's - // constructor is finished processing - so if we call the overridden - // update() function from inside this constructor, it will error when - // writing to private members. Pretty bad! - // - // That means initial data must be provided by following up with update() - // after constructing the new instance of the Thing (sub)class. - */ + // Note the constructor doesn't take an initial data source. Due to a quirk + // of JavaScript, private members can't be accessed before the superclass's + // constructor is finished processing - so if we call the overridden + // update() function from inside this constructor, it will error when + // writing to private members. Pretty bad! + // + // That means initial data must be provided by following up with update() + // after constructing the new instance of the Thing (sub)class. constructor() { this.#defineProperties(); @@ -352,4 +350,12 @@ export default class CacheableObject { console.log(` - ${line}`); } } + + static getUpdateValue(object, key) { + if (!Object.hasOwn(object, key)) { + return undefined; + } + + return object.#propertyUpdateValues[key] ?? null; + } } diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 29f5770c..96abf4af 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1071,3 +1071,18 @@ export function raiseWithoutUpdateValue({ }, ]); } + +export function withUpdateValueAsDependency({ + into = '#updateValue', +} = {}) { + return { + annotation: `withUpdateValueAsDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapContinuation: {into}, + transform: (value, continuation) => + continuation(value, {into: value}), + }, + }; +} diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 4e640dac..eb16d29e 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,13 +9,11 @@ import { } from '#validators'; import Thing, { - dynamicContribs, color, - contribsByRef, + contributionList, fileExtension, name, referenceList, - resolvedReferenceList, simpleDate, simpleString, urls, @@ -60,9 +58,13 @@ export class Flash extends Thing { coverArtFileExtension: fileExtension('jpg'), - contributorContribsByRef: contribsByRef(), + contributorContribs: contributionList(), - featuredTracksByRef: referenceList(Track), + featuredTracks: referenceList({ + class: Track, + find: find.track, + data: 'trackData', + }), urls: urls(), @@ -74,14 +76,6 @@ export class Flash extends Thing { // Expose only - contributorContribs: dynamicContribs('contributorContribsByRef'), - - featuredTracks: resolvedReferenceList({ - list: 'featuredTracksByRef', - data: 'trackData', - find: find.track, - }), - act: { flags: {expose: true}, @@ -138,18 +132,14 @@ export class FlashAct extends Thing { } }, - flashesByRef: referenceList(Flash), + flashes: referenceList({ + class: Flash, + data: 'flashData', + find: find.flash, + }), // Update only flashData: wikiData(Flash), - - // Expose only - - flashes: resolvedReferenceList({ - list: 'flashesByRef', - data: 'flashData', - find: find.flash, - }), }) } diff --git a/src/data/things/group.js b/src/data/things/group.js index 873c6d88..f53fa48e 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -5,7 +5,6 @@ import Thing, { directory, name, referenceList, - resolvedReferenceList, simpleString, urls, wikiData, @@ -24,7 +23,11 @@ export class Group extends Thing { urls: urls(), - featuredAlbumsByRef: referenceList(Album), + featuredAlbums: referenceList({ + class: Album, + find: find.album, + data: 'albumData', + }), // Update only @@ -33,12 +36,6 @@ export class Group extends Thing { // Expose only - featuredAlbums: resolvedReferenceList({ - list: 'featuredAlbumsByRef', - data: 'albumData', - find: find.album, - }), - descriptionShort: { flags: {expose: true}, @@ -89,18 +86,14 @@ export class GroupCategory extends Thing { name: name('Unnamed Group Category'), color: color(), - groupsByRef: referenceList(Group), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), // Update only groupData: wikiData(Group), - - // Expose only - - groups: resolvedReferenceList({ - list: 'groupsByRef', - data: 'groupData', - find: find.group, - }), }); } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index ab6f4cff..b509c1e2 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,23 +1,29 @@ import find from '#find'; +import { + compositeFrom, + exposeDependency, + withUpdateValueAsDependency, +} from '#composite'; + import { is, isCountingNumber, isString, isStringNonEmpty, + oneOf, validateArrayItems, validateInstanceOf, + validateReference, } from '#validators'; import Thing, { color, name, referenceList, - resolvedReference, - resolvedReferenceList, simpleString, - singleReference, wikiData, + withResolvedReference, } from './thing.js'; export class HomepageLayout extends Thing { @@ -101,8 +107,38 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroupByRef: singleReference(Group), - sourceAlbumsByRef: referenceList(Album), + sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ + { + transform: (value, continuation) => + (value === 'new-releases' || value === 'new-additions' + ? value + : continuation(value)), + }, + + withUpdateValueAsDependency(), + + withResolvedReference({ + ref: '#updateValue', + data: 'groupData', + find: find.group, + }), + + exposeDependency({ + dependency: '#resolvedReference', + update: { + validate: + oneOf( + is('new-releases', 'new-additions'), + validateReference(Group[Thing.referenceType])), + }, + }), + ]), + + sourceAlbums: referenceList({ + class: Album, + find: find.album, + data: 'albumData', + }), countAlbumsFromGroup: { flags: {update: true, expose: true}, @@ -113,19 +149,5 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { flags: {update: true, expose: true}, update: {validate: validateArrayItems(isString)}, }, - - // Expose only - - sourceGroup: resolvedReference({ - ref: 'sourceGroupByRef', - data: 'groupData', - find: find.group, - }), - - sourceAlbums: resolvedReferenceList({ - list: 'sourceAlbumsByRef', - data: 'albumData', - find: find.album, - }), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9d8b2ea2..91ad96af 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -11,8 +11,11 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { compositeFrom, exitWithoutDependency, + exposeConstant, exposeDependency, + exposeDependencyOrContinue, raiseWithoutDependency, + withUpdateValueAsDependency, } from '#composite'; import { @@ -162,22 +165,31 @@ export function externalFunction() { }; } -// Super simple "contributions by reference" list, used for a variety of -// properties (Artists, Cover Artists, etc). This is the property which is -// externally provided, in the form: +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: // -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] // -// ...processed from YAML, spreadsheet, or any other kind of input. -export function contribsByRef() { - return { - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }; +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// +export function contributionList() { + return compositeFrom(`contributionList`, [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({ + value: [], + update: {validate: isContributionList}, + }), + ]); } // Artist commentary! Generally present on tracks and albums. @@ -222,88 +234,77 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. -export function referenceList(thingClass) { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); +export function referenceList({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; -} - -// Corresponding function for a single reference. -export function singleReference(thingClass) { const {[Thing.referenceType]: referenceType} = thingClass; if (!referenceType) { throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; -} + return compositeFrom(`referenceList`, [ + withUpdateValueAsDependency(), -// Corresponding dynamic property to referenceList, which takes the values -// in the provided property and searches the specified wiki data for -// matching actual Thing-subclass objects. -export function resolvedReferenceList({list, data, find}) { - return compositeFrom(`resolvedReferenceList`, [ withResolvedReferenceList({ - list, data, find, + data, find, + list: '#updateValue', notFoundMode: 'filter', }), - exposeDependency({dependency: '#resolvedReferenceList'}), + exposeDependency({ + dependency: '#resolvedReferenceList', + update: { + validate: validateReferenceList(referenceType), + }, + }), ]); } // Corresponding function for a single reference. -export function resolvedReference({ref, data, find}) { - return compositeFrom(`resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); -} +export function singleReference({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); + } -// Corresponding dynamic property to contribsByRef, which takes the values -// in the provided property and searches the object's artistData for -// matching actual Artist objects. The computed structure has the same form -// as contribsByRef, but with Artist objects instead of string references: -// -// [ -// {who: (an Artist), what: 'Viola'}, -// {who: (an Artist), what: null}, -// ... -// ] -// -// Contributions whose "who" values don't match anything in artistData are -// filtered out. (So if the list is all empty, chances are that either the -// reference list is somehow messed up, or artistData isn't being provided -// properly.) -export function dynamicContribs(contribsByRefProperty) { - return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({from: contribsByRefProperty}), - exposeDependency({dependency: '#resolvedContribs'}), + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } + + return compositeFrom(`singleReference`, [ + withUpdateValueAsDependency(), + + withResolvedReference({ref: '#updateValue', data, find}), + + exposeDependency({ + dependency: '#resolvedReference', + update: { + validate: validateReference(referenceType), + }, + }), ]); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsByRefProperty) { +export function contribsPresent(contribsProperty) { return { flags: {expose: true}, expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + dependencies: [contribsProperty], + compute: ({[contribsProperty]: contribs}) => + !empty(contribs), + }, }; } @@ -380,13 +381,13 @@ export function withResolvedContribs({ mapDependencies: {from}, compute: ({from}, continuation) => continuation({ - '#whoByRef': from.map(({who}) => who), + '#artistRefs': from.map(({who}) => who), '#what': from.map(({what}) => what), }), }, withResolvedReferenceList({ - list: '#whoByRef', + list: '#artistRefs', data: 'artistData', into: '#who', find: find.artist, diff --git a/src/data/things/track.js b/src/data/things/track.js index fcfd39c7..8263d399 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -3,7 +3,6 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; import {empty} from '#sugar'; -import {isColor, isDate, isDuration, isFileExtension} from '#validators'; import { compositeFrom, @@ -13,20 +12,28 @@ import { exposeDependencyOrContinue, exposeUpdateValueOrContinue, withResultOfAvailabilityCheck, + withUpdateValueAsDependency, } from '#composite'; +import { + isColor, + isContributionList, + isDate, + isDuration, + isFileExtension, +} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + import Thing, { additionalFiles, commentary, commentatorArtists, - contribsByRef, + contributionList, directory, - dynamicContribs, flag, name, referenceList, - resolvedReference, - resolvedReferenceList, reverseReferenceList, simpleDate, singleReference, @@ -55,13 +62,11 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - artistContribsByRef: contribsByRef(), - contributorContribsByRef: contribsByRef(), - coverArtistContribsByRef: contribsByRef(), - - referencedTracksByRef: referenceList(Track), - sampledTracksByRef: referenceList(Track), - artTagsByRef: referenceList(ArtTag), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), @@ -134,9 +139,24 @@ export class Track extends Thing { }), ]), - originalReleaseTrackByRef: singleReference(Track), + originalReleaseTrack: singleReference({ + class: Track, + find: find.track, + data: 'trackData', + }), - dataSourceAlbumByRef: singleReference(Album), + // Note - this is an internal property used only to help identify a track. + // It should not be assumed in general that the album and dataSourceAlbum match + // (i.e. a track may dynamically be moved from one album to another, at + // which point dataSourceAlbum refers to where it was originally from, and is + // not generally relevant information). It's also not guaranteed that + // dataSourceAlbum is available (depending on the Track creator to optionally + // provide this property's update value). + dataSourceAlbum: singleReference({ + class: Album, + find: find.album, + data: 'albumData', + }), commentary: commentary(), lyrics: simpleString(), @@ -161,19 +181,6 @@ export class Track extends Thing { exposeDependency({dependency: '#album'}), ]), - // Note - this is an internal property used only to help identify a track. - // It should not be assumed in general that the album and dataSourceAlbum match - // (i.e. a track may dynamically be moved from one album to another, at - // which point dataSourceAlbum refers to where it was originally from, and is - // not generally relevant information). It's also not guaranteed that - // dataSourceAlbum is available (depending on the Track creator to optionally - // provide dataSourceAlbumByRef). - dataSourceAlbum: resolvedReference({ - ref: 'dataSourceAlbumByRef', - data: 'albumData', - find: find.album, - }), - date: compositeFrom(`Track.date`, [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), withAlbumProperty({property: 'date'}), @@ -192,11 +199,6 @@ export class Track extends Thing { exposeDependency({dependency: '#hasUniqueCoverArt'}), ]), - originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [ - withOriginalRelease(), - exposeDependency({dependency: '#originalRelease'}), - ]), - otherReleases: compositeFrom(`Track.otherReleases`, [ exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), @@ -224,26 +226,20 @@ export class Track extends Thing { artistContribs: compositeFrom(`Track.artistContribs`, [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs({ - from: 'artistContribsByRef', - into: '#artistContribs', - }), - - { - dependencies: ['#artistContribs'], - compute: ({'#artistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), + exposeDependencyOrContinue({dependency: '#artistContribs'}), withAlbumProperty({property: 'artistContribs'}), - exposeDependency({dependency: '#album.artistContribs'}), + exposeDependency({ + dependency: '#album.artistContribs', + update: {validate: isContributionList}, + }), ]), contributorContribs: compositeFrom(`Track.contributorContribs`, [ inheritFromOriginalRelease({property: 'contributorContribs'}), - dynamicContribs('contributorContribsByRef'), + contributionList(), ]), // Cover artists aren't inherited from the original release, since it @@ -258,47 +254,35 @@ export class Track extends Thing { : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribsByRef', - into: '#coverArtistContribs', - }), - - { - dependencies: ['#coverArtistContribs'], - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => - (empty(contribsFromTrack) - ? continuation() - : contribsFromTrack), - }, + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), + exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withAlbumProperty({property: 'trackCoverArtistContribs'}), - exposeDependency({dependency: '#album.trackCoverArtistContribs'}), + exposeDependency({ + dependency: '#album.trackCoverArtistContribs', + update: {validate: isContributionList}, + }), ]), referencedTracks: compositeFrom(`Track.referencedTracks`, [ inheritFromOriginalRelease({property: 'referencedTracks'}), - resolvedReferenceList({ - list: 'referencedTracksByRef', - data: 'trackData', + referenceList({ + class: Track, find: find.track, + data: 'trackData', }), ]), sampledTracks: compositeFrom(`Track.sampledTracks`, [ inheritFromOriginalRelease({property: 'sampledTracks'}), - resolvedReferenceList({ - list: 'sampledTracksByRef', - data: 'trackData', + referenceList({ + class: Track, find: find.track, + data: 'trackData', }), ]), - artTags: resolvedReferenceList({ - list: 'artTagsByRef', - data: 'artTagData', - find: find.artTag, - }), - // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. @@ -327,7 +311,7 @@ export class Track extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); - if (this.originalReleaseTrackByRef) { + if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) { parts.unshift(`${colors.yellow('[rerelease]')} `); } @@ -564,7 +548,7 @@ function withOriginalRelease({ } = {}) { return compositeFrom(`withOriginalRelease`, [ withResolvedReference({ - ref: 'originalReleaseTrackByRef', + ref: 'originalReleaseTrack', data: 'trackData', into: '#originalRelease', find: find.track, @@ -607,7 +591,7 @@ function withHasUniqueCoverArt({ }, withResolvedContribs({ - from: 'coverArtistContribsByRef', + from: 'coverArtistContribs', into: '#coverArtistContribs', }), diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 4c8f683b..f0d1d9fd 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -308,7 +308,7 @@ export const isTrackSection = validateProperties({ color: optional(isColor), dateOriginallyReleased: optional(isDate), isDefaultTrackSection: optional(isBoolean), - tracksByRef: optional(validateReferenceList('track')), + tracks: optional(validateReferenceList('track')), }); export const isTrackSectionList = validateArrayItems(isTrackSection); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 416b6c4e..7c2de324 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -6,7 +6,6 @@ import Thing, { flag, name, referenceList, - resolvedReferenceList, simpleString, wikiData, } from './thing.js'; @@ -45,7 +44,11 @@ export class WikiInfo extends Thing { update: {validate: isURL}, }, - divideTrackListsByGroupsByRef: referenceList(Group), + divideTrackListsByGroups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), // Feature toggles enableFlashesAndGames: flag(false), @@ -57,13 +60,5 @@ export class WikiInfo extends Thing { // Update only groupData: wikiData(Group), - - // Expose only - - divideTrackListsByGroups: resolvedReferenceList({ - list: 'divideTrackListsByGroupsByRef', - data: 'groupData', - find: find.group, - }), }); } diff --git a/src/data/yaml.js b/src/data/yaml.js index 8aca3299..e1e5803d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -10,7 +10,7 @@ import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; -import T from '#things'; +import T, {CacheableObject, Thing} from '#things'; import { conditionallySuppressError, @@ -278,11 +278,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, { coverArtFileExtension: 'Cover Art File Extension', trackCoverArtFileExtension: 'Track Art File Extension', - wallpaperArtistContribsByRef: 'Wallpaper Artists', + wallpaperArtistContribs: 'Wallpaper Artists', wallpaperStyle: 'Wallpaper Style', wallpaperFileExtension: 'Wallpaper File Extension', - bannerArtistContribsByRef: 'Banner Artists', + bannerArtistContribs: 'Banner Artists', bannerStyle: 'Banner Style', bannerFileExtension: 'Banner File Extension', bannerDimensions: 'Banner Dimensions', @@ -290,11 +290,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, { commentary: 'Commentary', additionalFiles: 'Additional Files', - artistContribsByRef: 'Artists', - coverArtistContribsByRef: 'Cover Artists', - trackCoverArtistContribsByRef: 'Default Track Cover Artists', - groupsByRef: 'Groups', - artTagsByRef: 'Art Tags', + artistContribs: 'Artists', + coverArtistContribs: 'Cover Artists', + trackCoverArtistContribs: 'Default Track Cover Artists', + groups: 'Groups', + artTags: 'Art Tags', }, }); @@ -348,13 +348,13 @@ export const processTrackDocument = makeProcessDocument(T.Track, { sheetMusicFiles: 'Sheet Music Files', midiProjectFiles: 'MIDI Project Files', - originalReleaseTrackByRef: 'Originally Released As', - referencedTracksByRef: 'Referenced Tracks', - sampledTracksByRef: 'Sampled Tracks', - artistContribsByRef: 'Artists', - contributorContribsByRef: 'Contributors', - coverArtistContribsByRef: 'Cover Artists', - artTagsByRef: 'Art Tags', + originalReleaseTrack: 'Originally Released As', + referencedTracks: 'Referenced Tracks', + sampledTracks: 'Sampled Tracks', + artistContribs: 'Artists', + contributorContribs: 'Contributors', + coverArtistContribs: 'Cover Artists', + artTags: 'Art Tags', }, invalidFieldCombinations: [ @@ -424,8 +424,8 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { date: 'Date', coverArtFileExtension: 'Cover Art File Extension', - featuredTracksByRef: 'Featured Tracks', - contributorContribsByRef: 'Contributors', + featuredTracks: 'Featured Tracks', + contributorContribs: 'Contributors', }, }); @@ -470,7 +470,7 @@ export const processGroupDocument = makeProcessDocument(T.Group, { description: 'Description', urls: 'URLs', - featuredAlbumsByRef: 'Featured Albums', + featuredAlbums: 'Featured Albums', }, }); @@ -501,7 +501,7 @@ export const processWikiInfoDocument = makeProcessDocument(T.WikiInfo, { footerContent: 'Footer Content', defaultLanguage: 'Default Language', canonicalBase: 'Canonical Base', - divideTrackListsByGroupsByRef: 'Divide Track Lists By Groups', + divideTrackListsByGroups: 'Divide Track Lists By Groups', enableFlashesAndGames: 'Enable Flashes & Games', enableListings: 'Enable Listings', enableNews: 'Enable News', @@ -536,9 +536,9 @@ export const homepageLayoutRowTypeProcessMapping = { albums: makeProcessHomepageLayoutRowDocument(T.HomepageLayoutAlbumsRow, { propertyFieldMapping: { displayStyle: 'Display Style', - sourceGroupByRef: 'Group', + sourceGroup: 'Group', countAlbumsFromGroup: 'Count', - sourceAlbumsByRef: 'Albums', + sourceAlbums: 'Albums', actionLinks: 'Actions', }, }), @@ -771,13 +771,13 @@ export const dataSteps = [ let currentTrackSection = { name: `Default Track Section`, isDefaultTrackSection: true, - tracksByRef: [], + tracks: [], }; - const albumRef = T.Thing.getReference(album); + const albumRef = Thing.getReference(album); const closeCurrentTrackSection = () => { - if (!empty(currentTrackSection.tracksByRef)) { + if (!empty(currentTrackSection.tracks)) { trackSections.push(currentTrackSection); } }; @@ -791,7 +791,7 @@ export const dataSteps = [ color: entry.color, dateOriginallyReleased: entry.dateOriginallyReleased, isDefaultTrackSection: false, - tracksByRef: [], + tracks: [], }; continue; @@ -799,9 +799,9 @@ export const dataSteps = [ trackData.push(entry); - entry.dataSourceAlbumByRef = albumRef; + entry.dataSourceAlbum = albumRef; - currentTrackSection.tracksByRef.push(T.Thing.getReference(entry)); + currentTrackSection.tracks.push(Thing.getReference(entry)); } closeCurrentTrackSection(); @@ -825,12 +825,12 @@ export const dataSteps = [ const artistData = results; const artistAliasData = results.flatMap((artist) => { - const origRef = T.Thing.getReference(artist); + const origRef = Thing.getReference(artist); return artist.aliasNames?.map((name) => { const alias = new T.Artist(); alias.name = name; alias.isAlias = true; - alias.aliasedArtistRef = origRef; + alias.aliasedArtist = origRef; alias.artistData = artistData; return alias; }) ?? []; @@ -854,7 +854,7 @@ export const dataSteps = [ save(results) { let flashAct; - let flashesByRef = []; + let flashRefs = []; if (results[0] && !(results[0] instanceof T.FlashAct)) { throw new Error(`Expected an act at top of flash data file`); @@ -863,18 +863,18 @@ export const dataSteps = [ for (const thing of results) { if (thing instanceof T.FlashAct) { if (flashAct) { - Object.assign(flashAct, {flashesByRef}); + Object.assign(flashAct, {flashes: flashRefs}); } flashAct = thing; - flashesByRef = []; + flashRefs = []; } else { - flashesByRef.push(T.Thing.getReference(thing)); + flashRefs.push(Thing.getReference(thing)); } } if (flashAct) { - Object.assign(flashAct, {flashesByRef}); + Object.assign(flashAct, {flashes: flashRefs}); } const flashData = results.filter((x) => x instanceof T.Flash); @@ -897,7 +897,7 @@ export const dataSteps = [ save(results) { let groupCategory; - let groupsByRef = []; + let groupRefs = []; if (results[0] && !(results[0] instanceof T.GroupCategory)) { throw new Error(`Expected a category at top of group data file`); @@ -906,18 +906,18 @@ export const dataSteps = [ for (const thing of results) { if (thing instanceof T.GroupCategory) { if (groupCategory) { - Object.assign(groupCategory, {groupsByRef}); + Object.assign(groupCategory, {groups: groupRefs}); } groupCategory = thing; - groupsByRef = []; + groupRefs = []; } else { - groupsByRef.push(T.Thing.getReference(thing)); + groupRefs.push(Thing.getReference(thing)); } } if (groupCategory) { - Object.assign(groupCategory, {groupsByRef}); + Object.assign(groupCategory, {groups: groupRefs}); } const groupData = results.filter((x) => x instanceof T.Group); @@ -1462,45 +1462,45 @@ export function filterDuplicateDirectories(wikiData) { export function filterReferenceErrors(wikiData) { const referenceSpec = [ ['wikiInfo', processWikiInfoDocument, { - divideTrackListsByGroupsByRef: 'group', + divideTrackListsByGroups: 'group', }], ['albumData', processAlbumDocument, { - artistContribsByRef: '_contrib', - coverArtistContribsByRef: '_contrib', - trackCoverArtistContribsByRef: '_contrib', - wallpaperArtistContribsByRef: '_contrib', - bannerArtistContribsByRef: '_contrib', - groupsByRef: 'group', - artTagsByRef: 'artTag', + artistContribs: '_contrib', + coverArtistContribs: '_contrib', + trackCoverArtistContribs: '_contrib', + wallpaperArtistContribs: '_contrib', + bannerArtistContribs: '_contrib', + groups: 'group', + artTags: 'artTag', }], ['trackData', processTrackDocument, { - artistContribsByRef: '_contrib', - contributorContribsByRef: '_contrib', - coverArtistContribsByRef: '_contrib', - referencedTracksByRef: '_trackNotRerelease', - sampledTracksByRef: '_trackNotRerelease', - artTagsByRef: 'artTag', - originalReleaseTrackByRef: '_trackNotRerelease', + artistContribs: '_contrib', + contributorContribs: '_contrib', + coverArtistContribs: '_contrib', + referencedTracks: '_trackNotRerelease', + sampledTracks: '_trackNotRerelease', + artTags: 'artTag', + originalReleaseTrack: '_trackNotRerelease', }], ['groupCategoryData', processGroupCategoryDocument, { - groupsByRef: 'group', + groups: 'group', }], ['homepageLayout.rows', undefined, { - sourceGroupByRef: '_homepageSourceGroup', - sourceAlbumsByRef: 'album', + sourceGroup: '_homepageSourceGroup', + sourceAlbums: 'album', }], ['flashData', processFlashDocument, { - contributorContribsByRef: '_contrib', - featuredTracksByRef: 'track', + contributorContribs: '_contrib', + featuredTracks: 'track', }], ['flashActData', processFlashActDocument, { - flashesByRef: 'flash', + flashes: 'flash', }], ]; @@ -1532,7 +1532,7 @@ export function filterReferenceErrors(wikiData) { nest({message: `Reference errors in ${inspect(thing)}`}, ({push, filter}) => { for (const [property, findFnKey] of Object.entries(propSpec)) { - const value = thing[property]; + const value = CacheableObject.getUpdateValue(thing, property); if (value === undefined) { push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); @@ -1552,7 +1552,7 @@ export function filterReferenceErrors(wikiData) { if (alias) { // No need to check if the original exists here. Aliases are automatically // created from a field on the original, so the original certainly exists. - const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'}); + const original = alias.aliasedArtist; throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); } @@ -1573,12 +1573,13 @@ export function filterReferenceErrors(wikiData) { case '_trackNotRerelease': findFn = trackRef => { const track = find.track(trackRef, wikiData.trackData, {mode: 'error'}); + const originalRef = track && CacheableObject.getUpdateValue(track, 'originalReleaseTrack'); - if (track?.originalReleaseTrackByRef) { + if (originalRef) { // It's possible for the original to not actually exist, in this case. // It should still be reported since the 'Originally Released As' field // was present. - const original = find.track(track.originalReleaseTrackByRef, wikiData.trackData, {mode: 'quiet'}); + const original = find.track(originalRef, wikiData.trackData, {mode: 'quiet'}); // Prefer references by name, but only if it's unambiguous. const originalByName = @@ -1591,7 +1592,7 @@ export function filterReferenceErrors(wikiData) { ? colors.green(original.name) : original ? colors.green('track:' + original.directory) - : colors.green(track.originalReleaseTrackByRef)); + : colors.green(originalRef)); throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); } @@ -1606,7 +1607,7 @@ export function filterReferenceErrors(wikiData) { } const suppress = fn => conditionallySuppressError(error => { - if (property === 'sampledTracksByRef') { + if (property === 'sampledTracks') { // Suppress "didn't match anything" errors in particular, just for samples. // In hsmusic-data we have a lot of "stub" sample data which don't have // corresponding tracks yet, so it won't be useful to report such reference -- cgit 1.3.0-6-gf8a5 From ee46a4f78f1bfc8348834fbd3349849148f178a8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 10:54:58 -0300 Subject: data: Album.coverArt{Date,FileExtension}: depend on contribs --- src/data/things/album.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 88308182..3726a463 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -2,6 +2,13 @@ import find from '#find'; import {empty} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; +import { + compositeFrom, + exitWithoutDependency, + exposeDependency, + exposeUpdateValueOrContinue, +} from '#composite'; + import Thing, { additionalFiles, commentary, @@ -18,6 +25,7 @@ import Thing, { simpleString, urls, wikiData, + withResolvedContribs, } from './thing.js'; export class Album extends Thing { @@ -35,19 +43,16 @@ export class Album extends Thing { trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), - coverArtDate: { - flags: {update: true, expose: true}, + coverArtDate: compositeFrom(`Album.coverArtDate`, [ + withResolvedContribs({from: 'coverArtistContribs'}), + exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), - update: {validate: isDate}, - - expose: { - dependencies: ['date', 'coverArtistContribs'], - transform: (coverArtDate, {coverArtistContribs, date}) => - (!empty(coverArtistContribs) - ? coverArtDate ?? date ?? null - : null), - }, - }, + exposeUpdateValueOrContinue(), + exposeDependency({ + dependency: 'date', + update: {validate: isDate}, + }), + ]), artistContribs: contributionList(), coverArtistContribs: contributionList(), @@ -102,7 +107,12 @@ export class Album extends Thing { }, }, - coverArtFileExtension: fileExtension('jpg'), + coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ + withResolvedContribs({from: 'coverArtistContribs'}), + exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), + fileExtension('jpg'), + ]), + trackCoverArtFileExtension: fileExtension('jpg'), wallpaperStyle: simpleString(), -- cgit 1.3.0-6-gf8a5 From 3ebe98d51d94a3e5277d65b2a4d2b5b433449214 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 11:29:53 -0300 Subject: data: withResolvedReferenceList: handle undefined matches --- src/data/things/thing.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 91ad96af..79d8ae0e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -491,19 +491,20 @@ export function withResolvedReferenceList({ let matches = list.map(ref => findFunction(ref, data, {mode: 'quiet'})); - if (!matches.includes(null)) { + if (matches.every(match => match)) { return continuation.raise({matches}); } switch (notFoundMode) { case 'filter': - matches = matches.filter(value => value !== null); + matches = matches.filter(match => match); return continuation.raise({matches}); case 'exit': return continuation.exit([]); case 'null': + matches = matches.map(match => match ?? null); return continuation.raise({matches}); } }, -- cgit 1.3.0-6-gf8a5 From bf0be010c9d9b860ad42762fc2e373130c7535eb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 11:32:50 -0300 Subject: data: update Album.tracks --- src/data/things/album.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 3726a463..76e0f638 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -26,6 +26,7 @@ import Thing, { urls, wikiData, withResolvedContribs, + withResolvedReferenceList, } from './thing.js'; export class Album extends Thing { @@ -147,20 +148,30 @@ export class Album extends Thing { hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), hasBannerArt: contribsPresent('bannerArtistContribs'), - tracks: { - flags: {expose: true}, + tracks: compositeFrom(`Album.tracks`, [ + exitWithoutDependency({ + dependency: 'trackSections', + mode: 'empty', + value: [], + }), - expose: { + { dependencies: ['trackSections', 'trackData'], - compute: ({trackSections, trackData}) => - (trackSections && trackData - ? trackSections - .flatMap(section => section.tracks ?? []) - .map(ref => find.track(ref, trackData, {mode: 'quiet'})) - .filter(Boolean) - : []), + compute: ({trackSections, trackData}, continuation) => + continuation({ + '#trackRefs': trackSections + .flatMap(section => section.tracks ?? []), + }), }, - }, + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + find: find.track, + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ]), }); static [Thing.getSerializeDescriptors] = ({ -- cgit 1.3.0-6-gf8a5 From 65260d7fc2790ece0c13820ba18bc821163f164e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 12:19:49 -0300 Subject: data: new withFlattenedArray, withUnflattenedArray utilities --- src/data/things/composite.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 96abf4af..1f6482f6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1072,6 +1072,8 @@ export function raiseWithoutUpdateValue({ ]); } +// Turns an updating property's update value into a dependency, so it can be +// conveniently passed to other functions. export function withUpdateValueAsDependency({ into = '#updateValue', } = {}) { @@ -1086,3 +1088,76 @@ export function withUpdateValueAsDependency({ }, }; } + +// Flattens an array with one level of nested arrays, providing as dependencies +// both the flattened array as well as the original starting indices of each +// successive source array. +export function withFlattenedArray({ + from, + into = '#flattenedArray', + intoIndices = '#flattenedIndices', +}) { + return { + annotation: `withFlattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from}, + mapContinuation: {into, intoIndices}, + + compute({from: sourceArray}, continuation) { + const into = sourceArray.flat(); + const intoIndices = []; + + let lastEndIndex = 0; + for (const {length} of sourceArray) { + intoIndices.push(lastEndIndex); + lastEndIndex += length; + } + + return continuation({into, intoIndices}); + }, + }, + }; +} + +// After mapping the contents of a flattened array in-place (being careful to +// retain the original indices by replacing unmatched results with null instead +// of filtering them out), this function allows for recombining them. It will +// filter out null and undefined items by default (pass {filter: false} to +// disable this). +export function withUnflattenedArray({ + from, + fromIndices = '#flattenedIndices', + into = '#unflattenedArray', + filter = true, +}) { + return { + annotation: `withUnflattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from, fromIndices}, + mapContinuation: {into}, + compute({from, fromIndices}, continuation) { + const arrays = []; + + for (let i = 0; i < fromIndices.length; i++) { + const startIndex = fromIndices[i]; + const endIndex = + (i === fromIndices.length - 1 + ? from.length + : fromIndices[i + 1]); + + const values = from.slice(startIndex, endIndex); + arrays.push( + (filter + ? values.filter(value => value !== null && value !== undefined) + : values)); + } + + return continuation({into: arrays}); + }, + }, + }; +} -- cgit 1.3.0-6-gf8a5 From 4ed5649e83e344615eb0e710c7a942d0dea8fa22 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 12:35:22 -0300 Subject: data: update Album.trackSections --- src/data/things/album.js | 121 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 37 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 76e0f638..01f52c2d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,12 +1,15 @@ import find from '#find'; -import {empty} from '#sugar'; +import {stitchArrays} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; import { compositeFrom, exitWithoutDependency, + exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + withFlattenedArray, + withUnflattenedArray, } from '#composite'; import Thing, { @@ -73,40 +76,87 @@ export class Album extends Thing { data: 'artTagData', }), - trackSections: { - flags: {update: true, expose: true}, + trackSections: compositeFrom(`Album.trackSections`, [ + exitWithoutDependency({dependency: 'trackData', value: []}), + exitWithoutUpdateValue({value: [], mode: 'empty'}), - update: { - validate: isTrackSectionList, + { + transform: (trackSections, continuation) => + continuation(trackSections, { + '#sectionTrackRefs': + trackSections.map(section => section.tracks), + + '#sectionDateOriginallyReleased': + trackSections + .map(({dateOriginallyReleased}) => dateOriginallyReleased ?? null), + + '#sectionIsDefaultTrackSection': + trackSections + .map(({isDefaultTrackSection}) => isDefaultTrackSection ?? false), + }), }, - expose: { - dependencies: ['color', 'trackData'], - transform(trackSections, { - color: albumColor, - trackData, - }) { - let startIndex = 0; - return trackSections?.map(section => ({ - name: section.name ?? null, - color: section.color ?? albumColor ?? null, - dateOriginallyReleased: section.dateOriginallyReleased ?? null, - isDefaultTrackSection: section.isDefaultTrackSection ?? false, - - startIndex: ( - startIndex += section.tracks.length, - startIndex - section.tracks.length - ), - - tracks: - (trackData && section.tracks - ?.map(ref => find.track(ref, trackData, {mode: 'quiet'})) - .filter(Boolean)) ?? - [], - })); + { + dependencies: ['color'], + transform: (trackSections, {color: albumColor}, continuation) => + continuation(trackSections, { + '#sectionColor': + trackSections + .map(({color: sectionColor}) => sectionColor ?? albumColor), + }), + }, + + withFlattenedArray({ + from: '#sectionTrackRefs', + into: '#trackRefs', + intoIndices: '#sectionStartIndex', + }), + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + mode: 'null', + find: find.track, + into: '#tracks', + }), + + withUnflattenedArray({ + from: '#tracks', + fromIndices: '#sectionStartIndex', + into: '#sectionTracks', + }), + + { + flags: {update: true, expose: true}, + + update: {validate: isTrackSectionList}, + + expose: { + dependencies: [ + '#sectionTracks', + '#sectionColor', + '#sectionDateOriginallyReleased', + '#sectionIsDefaultTrackSection', + '#sectionStartIndex', + ], + + transform: (trackSections, { + '#sectionTracks': tracks, + '#sectionColor': color, + '#sectionDateOriginallyReleased': dateOriginallyReleased, + '#sectionIsDefaultTrackSection': isDefaultTrackSection, + '#sectionStartIndex': startIndex, + }) => + stitchArrays({ + tracks, + color, + dateOriginallyReleased, + isDefaultTrackSection, + startIndex, + }), }, }, - }, + ]), coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ withResolvedContribs({from: 'coverArtistContribs'}), @@ -149,15 +199,12 @@ export class Album extends Thing { hasBannerArt: contribsPresent('bannerArtistContribs'), tracks: compositeFrom(`Album.tracks`, [ - exitWithoutDependency({ - dependency: 'trackSections', - mode: 'empty', - value: [], - }), + exitWithoutDependency({dependency: 'trackData', value: []}), + exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), { - dependencies: ['trackSections', 'trackData'], - compute: ({trackSections, trackData}, continuation) => + dependencies: ['trackSections'], + compute: ({trackSections}, continuation) => continuation({ '#trackRefs': trackSections .flatMap(section => section.tracks ?? []), -- cgit 1.3.0-6-gf8a5 From 15b0b5422a3de8da52e14666909418405bdb8c39 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:09:16 -0300 Subject: data: update commentatorArtists --- src/data/things/thing.js | 55 +++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 22 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..9e7f940f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { @@ -334,29 +334,40 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists(){ - return { - flags: {expose: true}, +export function commentatorArtists() { + return compositeFrom(`commentatorArtists`, [ + exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - }; + + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: find.artist, + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, + }, + ]); } // Compositional utilities -- cgit 1.3.0-6-gf8a5 From f39164ed44fe5c86f1f1911514d38a5549e51f92 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:17:05 -0300 Subject: data: rearrange track properties --- src/data/things/track.js | 153 +++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 78 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 8263d399..0cd39dca 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -62,12 +62,6 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - artTags: referenceList({ - class: ArtTag, - find: find.artTag, - data: 'artTagData', - }), - color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), withContainingTrackSection(), @@ -139,90 +133,28 @@ export class Track extends Thing { }), ]), + commentary: commentary(), + lyrics: simpleString(), + + additionalFiles: additionalFiles(), + sheetMusicFiles: additionalFiles(), + midiProjectFiles: additionalFiles(), + originalReleaseTrack: singleReference({ class: Track, find: find.track, data: 'trackData', }), - // Note - this is an internal property used only to help identify a track. - // It should not be assumed in general that the album and dataSourceAlbum match - // (i.e. a track may dynamically be moved from one album to another, at - // which point dataSourceAlbum refers to where it was originally from, and is - // not generally relevant information). It's also not guaranteed that - // dataSourceAlbum is available (depending on the Track creator to optionally - // provide this property's update value). + // Internal use only - for directly identifying an album inside a track's + // util.inspect display, if it isn't indirectly available (by way of being + // included in an album's track list). dataSourceAlbum: singleReference({ class: Album, find: find.album, data: 'albumData', }), - commentary: commentary(), - lyrics: simpleString(), - additionalFiles: additionalFiles(), - sheetMusicFiles: additionalFiles(), - midiProjectFiles: additionalFiles(), - - // Update only - - albumData: wikiData(Album), - artistData: wikiData(Artist), - artTagData: wikiData(ArtTag), - flashData: wikiData(Flash), - trackData: wikiData(Track), - - // Expose only - - commentatorArtists: commentatorArtists(), - - album: compositeFrom(`Track.album`, [ - withAlbum(), - exposeDependency({dependency: '#album'}), - ]), - - date: compositeFrom(`Track.date`, [ - exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withAlbumProperty({property: 'date'}), - exposeDependency({dependency: '#album.date'}), - ]), - - // Whether or not the track has "unique" cover artwork - a cover which is - // specifically associated with this track in particular, rather than with - // the track's album as a whole. This is typically used to select between - // displaying the track artwork and a fallback, such as the album artwork - // or a placeholder. (This property is named hasUniqueCoverArt instead of - // the usual hasCoverArt to emphasize that it does not inherit from the - // album.) - hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ - withHasUniqueCoverArt(), - exposeDependency({dependency: '#hasUniqueCoverArt'}), - ]), - - otherReleases: compositeFrom(`Track.otherReleases`, [ - exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), - withOriginalRelease({selfIfOriginal: true}), - - { - flags: {expose: true}, - expose: { - dependencies: ['this', 'trackData', '#originalRelease'], - compute: ({ - this: thisTrack, - trackData, - '#originalRelease': originalRelease, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, - }, - ]), - artistContribs: compositeFrom(`Track.artistContribs`, [ inheritFromOriginalRelease({property: 'artistContribs'}), @@ -283,6 +215,71 @@ export class Track extends Thing { }), ]), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), + + // Update only + + albumData: wikiData(Album), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + flashData: wikiData(Flash), + trackData: wikiData(Track), + + // Expose only + + commentatorArtists: commentatorArtists(), + + album: compositeFrom(`Track.album`, [ + withAlbum(), + exposeDependency({dependency: '#album'}), + ]), + + date: compositeFrom(`Track.date`, [ + exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), + withAlbumProperty({property: 'date'}), + exposeDependency({dependency: '#album.date'}), + ]), + + // Whether or not the track has "unique" cover artwork - a cover which is + // specifically associated with this track in particular, rather than with + // the track's album as a whole. This is typically used to select between + // displaying the track artwork and a fallback, such as the album artwork + // or a placeholder. (This property is named hasUniqueCoverArt instead of + // the usual hasCoverArt to emphasize that it does not inherit from the + // album.) + hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exposeDependency({dependency: '#hasUniqueCoverArt'}), + ]), + + otherReleases: compositeFrom(`Track.otherReleases`, [ + exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), + withOriginalRelease({selfIfOriginal: true}), + + { + flags: {expose: true}, + expose: { + dependencies: ['this', 'trackData', '#originalRelease'], + compute: ({ + this: thisTrack, + trackData, + '#originalRelease': originalRelease, + }) => + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }, + }, + ]), + // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't // generally relevant from the perspective of the tracks being referenced. -- cgit 1.3.0-6-gf8a5 From e01b73d286fbb11ac8ded59b4c23738dff195171 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:22:10 -0300 Subject: data: dimensions utility --- src/data/things/album.js | 8 +++----- src/data/things/thing.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 01f52c2d..5e281f06 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,6 +1,6 @@ import find from '#find'; import {stitchArrays} from '#sugar'; -import {isDate, isDimensions, isTrackSectionList} from '#validators'; +import {isDate, isTrackSectionList} from '#validators'; import { compositeFrom, @@ -19,6 +19,7 @@ import Thing, { commentatorArtists, contribsPresent, contributionList, + dimensions, directory, fileExtension, flag, @@ -171,10 +172,7 @@ export class Album extends Thing { bannerStyle: simpleString(), bannerFileExtension: fileExtension('jpg'), - bannerDimensions: { - flags: {update: true, expose: true}, - update: {validate: isDimensions}, - }, + bannerDimensions: dimensions(), hasTrackNumbers: flag(true), isListedOnHomepage: flag(true), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9e7f940f..0484b589 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,6 +25,7 @@ import { isColor, isContributionList, isDate, + isDimensions, isDirectory, isFileExtension, isName, @@ -122,6 +123,15 @@ export function fileExtension(defaultFileExtension = null) { }; } +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. +export function dimensions() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { -- cgit 1.3.0-6-gf8a5 From cd3e2ae7384d82f0f2758beb0ae38ce0fe9f5e09 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:25:22 -0300 Subject: data: duration utility --- src/data/things/thing.js | 10 ++++++++++ src/data/things/track.js | 8 ++------ 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0484b589..169fc1ca 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { isDate, isDimensions, isDirectory, + isDuration, isFileExtension, isName, isString, @@ -132,6 +133,15 @@ export function dimensions() { }; } +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. +export function duration() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { diff --git a/src/data/things/track.js b/src/data/things/track.js index 0cd39dca..53798cda 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -19,7 +19,6 @@ import { isColor, isContributionList, isDate, - isDuration, isFileExtension, } from '#validators'; @@ -31,6 +30,7 @@ import Thing, { commentatorArtists, contributionList, directory, + duration, flag, name, referenceList, @@ -54,11 +54,7 @@ export class Track extends Thing { name: name('Unnamed Track'), directory: directory(), - duration: { - flags: {update: true, expose: true}, - update: {validate: isDuration}, - }, - + duration: duration(), urls: urls(), dateFirstReleased: simpleDate(), -- cgit 1.3.0-6-gf8a5 From 6fe22802d8220b983a488f4efee1834bacbdb166 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 17:20:48 -0300 Subject: data: cleaner withResolvedReferenceList notFoundMode implementation --- src/data/things/thing.js | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 169fc1ca..96ac9b12 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -514,29 +514,45 @@ export function withResolvedReferenceList({ }), { - options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: into}, + options: {findFunction}, - compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { - let matches = - list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, - if (matches.every(match => match)) { - return continuation.raise({matches}); - } + { + dependencies: ['#matches'], + mapContinuation: {into}, - switch (notFoundMode) { - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({matches}); + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({into: matches}) + : continuation()), + }, + + { + dependencies: ['#matches'], + options: {notFoundMode}, + mapContinuation: {into}, + compute({ + '#matches': matches, + '#options': {notFoundMode}, + }, continuation) { + switch (notFoundMode) { case 'exit': return continuation.exit([]); + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({into: matches}); + case 'null': matches = matches.map(match => match ?? null); - return continuation.raise({matches}); + return continuation.raise({into: matches}); } }, }, -- cgit 1.3.0-6-gf8a5 From c82784ebb4e5141bfe97664f3252303b3e833863 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:13:44 -0300 Subject: data: withPropertyFrom{Object,List}, fillMissingListItems utils --- src/data/things/album.js | 66 +++++++++------------- src/data/things/composite.js | 127 +++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 11 +--- src/data/things/track.js | 16 +----- 4 files changed, 159 insertions(+), 61 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 5e281f06..288caa04 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -8,8 +8,11 @@ import { exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + fillMissingListItems, withFlattenedArray, + withPropertyFromList, withUnflattenedArray, + withUpdateValueAsDependency, } from '#composite'; import Thing, { @@ -81,50 +84,35 @@ export class Album extends Thing { exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), - { - transform: (trackSections, continuation) => - continuation(trackSections, { - '#sectionTrackRefs': - trackSections.map(section => section.tracks), - - '#sectionDateOriginallyReleased': - trackSections - .map(({dateOriginallyReleased}) => dateOriginallyReleased ?? null), - - '#sectionIsDefaultTrackSection': - trackSections - .map(({isDefaultTrackSection}) => isDefaultTrackSection ?? false), - }), - }, + withUpdateValueAsDependency({into: '#sections'}), - { - dependencies: ['color'], - transform: (trackSections, {color: albumColor}, continuation) => - continuation(trackSections, { - '#sectionColor': - trackSections - .map(({color: sectionColor}) => sectionColor ?? albumColor), - }), - }, + withPropertyFromList({list: '#sections', property: 'tracks', into: '#sections.trackRefs'}), + withPropertyFromList({list: '#sections', property: 'dateOriginallyReleased'}), + withPropertyFromList({list: '#sections', property: 'isDefaultTrackSection'}), + withPropertyFromList({list: '#sections', property: 'color'}), + + fillMissingListItems({list: '#sections.trackRefs', value: []}), + fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), + fillMissingListItems({list: '#sections.color', dependency: 'color'}), withFlattenedArray({ - from: '#sectionTrackRefs', + from: '#sections.trackRefs', into: '#trackRefs', - intoIndices: '#sectionStartIndex', + intoIndices: '#sections.startIndex', }), withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - mode: 'null', + notFoundMode: 'null', find: find.track, into: '#tracks', }), withUnflattenedArray({ from: '#tracks', - fromIndices: '#sectionStartIndex', - into: '#sectionTracks', + fromIndices: '#sections.startIndex', + into: '#sections.tracks', }), { @@ -134,19 +122,19 @@ export class Album extends Thing { expose: { dependencies: [ - '#sectionTracks', - '#sectionColor', - '#sectionDateOriginallyReleased', - '#sectionIsDefaultTrackSection', - '#sectionStartIndex', + '#sections.tracks', + '#sections.color', + '#sections.dateOriginallyReleased', + '#sections.isDefaultTrackSection', + '#sections.startIndex', ], transform: (trackSections, { - '#sectionTracks': tracks, - '#sectionColor': color, - '#sectionDateOriginallyReleased': dateOriginallyReleased, - '#sectionIsDefaultTrackSection': isDefaultTrackSection, - '#sectionStartIndex': startIndex, + '#sections.tracks': tracks, + '#sections.color': color, + '#sections.dateOriginallyReleased': dateOriginallyReleased, + '#sections.isDefaultTrackSection': isDefaultTrackSection, + '#sections.startIndex': startIndex, }) => stitchArrays({ tracks, diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1f6482f6..a5adc3e9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1089,6 +1089,133 @@ export function withUpdateValueAsDependency({ }; } +// Gets a property of some object (in a dependency) and provides that value. +// If the object itself is null, the provided dependency will also always be +// null. By default, it'll also be null if the object doesn't have the listed +// property (or its value is undefined/null); provide a value on {missing} to +// default to something else here. +export function withPropertyFromObject({ + object, + property, + into = null, +}) { + into ??= + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`); + + return { + annotation: `withPropertyFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + mapContinuation: {into}, + options: {property}, + + compute({object, '#options': {property}}, continuation) { + if (object === null || object === undefined) return continuation({into: null}); + if (!Object.hasOwn(object, property)) return continuation({into: null}); + return continuation({into: object[property] ?? null}); + }, + }, + }; +} + +// Gets a property from each of a list of objects (in a dependency) and +// provides the results. This doesn't alter any list indices, so positions +// which were null in the original list are kept null here. Objects which don't +// have the specified property are also included in-place as null, by default; +// provide a value on {missing} to default to something else here. +export function withPropertyFromList({ + list, + property, + into = null, + missing = null, +}) { + into ??= + (list.startsWith('#') + ? `${list}.${property}` + : `#${list}.${property}`); + + return { + annotation: `withPropertyFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {property}, + + compute({list, '#options': {property}}, continuation) { + if (list === undefined || empty(list)) { + return continuation({into: []}); + } + + return continuation({ + into: + list.map(item => { + if (item === null || item === undefined) return null; + if (!Object.hasOwn(item, property)) return missing; + return item[property] ?? missing; + }), + }); + }, + }, + }; +} + +// Replaces items of a list, which are null or undefined, with some fallback +// value, either a constant (set {value}) or from a dependency ({dependency}). +// By default, this replaces the passed dependency. +export function fillMissingListItems({ + list, + value, + dependency, + into = list, +}) { + if (value !== undefined && dependency !== undefined) { + throw new TypeError(`Don't provide both value and dependency`); + } + + if (value === undefined && dependency === undefined) { + throw new TypeError(`Missing value or dependency`); + } + + if (dependency) { + return { + annotation: `fillMissingListItems.fromDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list, dependency}, + mapContinuation: {into}, + + compute: ({list, dependency}, continuation) => + continuation({ + into: list.map(item => item ?? dependency), + }), + }, + }; + } else { + return { + annotation: `fillMissingListItems.fromValue`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {value}, + + compute: ({list, '#options': {value}}, continuation) => + continuation({ + into: list.map(item => item ?? value), + }), + }, + }; + } +} + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 96ac9b12..a87e6ed6 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withPropertyFromList, withUpdateValueAsDependency, } from '#composite'; @@ -408,14 +409,8 @@ export function withResolvedContribs({ raise: {into: []}, }), - { - mapDependencies: {from}, - compute: ({from}, continuation) => - continuation({ - '#artistRefs': from.map(({who}) => who), - '#what': from.map(({what}) => what), - }), - }, + withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), + withPropertyFromList({list: from, property: 'what', into: '#what'}), withResolvedReferenceList({ list: '#artistRefs', diff --git a/src/data/things/track.js b/src/data/things/track.js index 53798cda..a307fda9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,6 +11,7 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, + withPropertyFromObject, withResultOfAvailabilityCheck, withUpdateValueAsDependency, } from '#composite'; @@ -430,20 +431,7 @@ function withAlbumProperty({ }) { return compositeFrom(`withAlbumProperty`, [ withAlbum({notFoundMode}), - - { - dependencies: ['#album'], - options: {property}, - mapContinuation: {into}, - - compute: ({ - '#album': album, - '#options': {property}, - }, continuation) => - (album - ? continuation.raise({into: album[property]}) - : continuation.raise({into: null})), - }, + withPropertyFromObject({object: '#album', property, into}), ]); } -- cgit 1.3.0-6-gf8a5 From 57d07a308dfee6d16b49f7c009853b1789597e82 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:17:44 -0300 Subject: data: withAlbumProperty -> withPropertyFromAlbum Also remove withAlbumProperties, since it's not used anywhere and mostly serves as reference code. --- src/data/things/track.js | 59 ++++++++---------------------------------------- 1 file changed, 10 insertions(+), 49 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index a307fda9..5e553b48 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -75,7 +75,7 @@ export class Track extends Thing { : continuation()), }, - withAlbumProperty({property: 'color'}), + withPropertyFromAlbum({property: 'color'}), exposeDependency({ dependency: '#album.color', @@ -103,7 +103,7 @@ export class Track extends Thing { exposeUpdateValueOrContinue(), // Expose album's trackCoverArtFileExtension if no update value set. - withAlbumProperty({property: 'trackCoverArtFileExtension'}), + withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), // Fallback to 'jpg'. @@ -123,7 +123,7 @@ export class Track extends Thing { exposeUpdateValueOrContinue(), - withAlbumProperty({property: 'trackArtDate'}), + withPropertyFromAlbum({property: 'trackArtDate'}), exposeDependency({ dependency: '#album.trackArtDate', update: {validate: isDate}, @@ -159,7 +159,7 @@ export class Track extends Thing { withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), exposeDependencyOrContinue({dependency: '#artistContribs'}), - withAlbumProperty({property: 'artistContribs'}), + withPropertyFromAlbum({property: 'artistContribs'}), exposeDependency({ dependency: '#album.artistContribs', update: {validate: isContributionList}, @@ -187,7 +187,7 @@ export class Track extends Thing { withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), - withAlbumProperty({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), exposeDependency({ dependency: '#album.trackCoverArtistContribs', update: {validate: isContributionList}, @@ -237,7 +237,7 @@ export class Track extends Thing { date: compositeFrom(`Track.date`, [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withAlbumProperty({property: 'date'}), + withPropertyFromAlbum({property: 'date'}), exposeDependency({dependency: '#album.date'}), ]), @@ -424,56 +424,17 @@ function withAlbum({ // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withAlbumProperty({ +function withPropertyFromAlbum({ property, into = '#album.' + property, notFoundMode = 'null', }) { - return compositeFrom(`withAlbumProperty`, [ + return compositeFrom(`withPropertyFromAlbum`, [ withAlbum({notFoundMode}), withPropertyFromObject({object: '#album', property, into}), ]); } -// Gets the listed properties from this track's album, providing them as -// dependencies (by default) with '#album.' prefixed before each property -// name. If the track's album isn't available, then by default, the same -// dependency names will be provided as null; set {notFoundMode: 'exit'} -// to early exit instead. -function withAlbumProperties({ - properties, - prefix = '#album', - notFoundMode = 'null', -}) { - return compositeFrom(`withAlbumProperties`, [ - withAlbum({notFoundMode}), - - { - dependencies: ['#album'], - options: {properties, prefix}, - - compute({ - '#album': album, - '#options': {properties, prefix}, - }, continuation) { - const raise = {}; - - if (album) { - for (const property of properties) { - raise[prefix + '.' + property] = album[property]; - } - } else { - for (const property of properties) { - raise[prefix + '.' + property] = null; - } - } - - return continuation.raise(raise); - }, - }, - ]); -} - // Gets the track section containing this track from its album's track list. // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. @@ -486,7 +447,7 @@ function withContainingTrackSection({ } return compositeFrom(`withContainingTrackSection`, [ - withAlbumProperty({property: 'trackSections', notFoundMode}), + withPropertyFromAlbum({property: 'trackSections', notFoundMode}), { dependencies: ['this', '#album.trackSections'], @@ -585,7 +546,7 @@ function withHasUniqueCoverArt({ : continuation.raise({into: true})), }, - withAlbumProperty({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], -- cgit 1.3.0-6-gf8a5 From 726118e7e8eefa9002562ca2dd0a4f6deb8a05b9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:26:33 -0300 Subject: data: refactor {missing} out of withPropertyFrom{Object,List} --- src/data/things/composite.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index a5adc3e9..b37b8e31 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1090,10 +1090,8 @@ export function withUpdateValueAsDependency({ } // Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, the provided dependency will also always be -// null. By default, it'll also be null if the object doesn't have the listed -// property (or its value is undefined/null); provide a value on {missing} to -// default to something else here. +// If the object itself is null, or the object doesn't have the listed property, +// the provided dependency will also be null. export function withPropertyFromObject({ object, property, @@ -1113,11 +1111,10 @@ export function withPropertyFromObject({ mapContinuation: {into}, options: {property}, - compute({object, '#options': {property}}, continuation) { - if (object === null || object === undefined) return continuation({into: null}); - if (!Object.hasOwn(object, property)) return continuation({into: null}); - return continuation({into: object[property] ?? null}); - }, + compute: ({object, '#options': {property}}, continuation) => + (object === null || object === undefined + ? continuation({into: null}) + : continuation({into: object[property] ?? null})), }, }; } @@ -1125,13 +1122,11 @@ export function withPropertyFromObject({ // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't -// have the specified property are also included in-place as null, by default; -// provide a value on {missing} to default to something else here. +// have the specified property are retained in-place as null. export function withPropertyFromList({ list, property, into = null, - missing = null, }) { into ??= (list.startsWith('#') @@ -1154,11 +1149,10 @@ export function withPropertyFromList({ return continuation({ into: - list.map(item => { - if (item === null || item === undefined) return null; - if (!Object.hasOwn(item, property)) return missing; - return item[property] ?? missing; - }), + list.map(item => + (item === null || item === undefined + ? null + : item[property] ?? null)), }); }, }, -- cgit 1.3.0-6-gf8a5 From 7a21c665d888b0db4c47c72049f7649bf1dabcde Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:47:38 -0300 Subject: data: withPropertiesFrom{Object,List} --- src/data/things/album.js | 19 +++++++----- src/data/things/composite.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ src/data/things/thing.js | 17 ++++++----- 3 files changed, 94 insertions(+), 14 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 288caa04..e11d0909 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -10,7 +10,7 @@ import { exposeUpdateValueOrContinue, fillMissingListItems, withFlattenedArray, - withPropertyFromList, + withPropertiesFromList, withUnflattenedArray, withUpdateValueAsDependency, } from '#composite'; @@ -86,17 +86,22 @@ export class Album extends Thing { withUpdateValueAsDependency({into: '#sections'}), - withPropertyFromList({list: '#sections', property: 'tracks', into: '#sections.trackRefs'}), - withPropertyFromList({list: '#sections', property: 'dateOriginallyReleased'}), - withPropertyFromList({list: '#sections', property: 'isDefaultTrackSection'}), - withPropertyFromList({list: '#sections', property: 'color'}), + withPropertiesFromList({ + list: '#sections', + properties: [ + 'tracks', + 'dateOriginallyReleased', + 'isDefaultTrackSection', + 'color', + ], + }), - fillMissingListItems({list: '#sections.trackRefs', value: []}), + fillMissingListItems({list: '#sections.tracks', value: []}), fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), fillMissingListItems({list: '#sections.color', dependency: 'color'}), withFlattenedArray({ - from: '#sections.trackRefs', + from: '#sections.tracks', into: '#trackRefs', intoIndices: '#sections.startIndex', }), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b37b8e31..e3225563 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1119,6 +1119,39 @@ export function withPropertyFromObject({ }; } +// Gets the listed properties from some object, providing each property's value +// as a dependency prefixed with the same name as the object (by default). +// If the object itself is null, all provided dependencies will be null; +// if it's missing only select properties, those will be provided as null. +export function withPropertiesFromObject({ + object, + properties, + prefix = + (object.startsWith('#') + ? object + : `#${object}`), +}) { + return { + annotation: `withPropertiesFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + options: {prefix, properties}, + + compute: ({object, '#options': {prefix, properties}}, continuation) => + continuation( + Object.fromEntries( + properties.map(property => [ + `${prefix}.${property}`, + (object === null || object === undefined + ? null + : object[property] ?? null), + ]))), + }, + }; +} + // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't @@ -1159,6 +1192,45 @@ export function withPropertyFromList({ }; } +// Gets the listed properties from each of a list of objects, providing lists +// of property values each into a dependency prefixed with the same name as the +// list (by default). Like withPropertyFromList, this doesn't alter indices. +export function withPropertiesFromList({ + list, + properties, + prefix = + (list.startsWith('#') + ? list + : `#${list}`), +}) { + return { + annotation: `withPropertiesFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + options: {prefix, properties}, + + compute({list, '#options': {prefix, properties}}, continuation) { + const lists = + Object.fromEntries( + properties.map(property => [`${prefix}.${property}`, []])); + + for (const item of list) { + for (const property of properties) { + lists[`${prefix}.${property}`].push( + (item === null || item === undefined + ? null + : item[property] ?? null)); + } + } + + return continuation(lists); + } + } + } +} + // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). // By default, this replaces the passed dependency. diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a87e6ed6..52f0b773 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,7 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, - withPropertyFromList, + withPropertiesFromList, withUpdateValueAsDependency, } from '#composite'; @@ -409,21 +409,24 @@ export function withResolvedContribs({ raise: {into: []}, }), - withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), - withPropertyFromList({list: from, property: 'what', into: '#what'}), + withPropertiesFromList({ + list: from, + properties: ['who', 'what'], + prefix: '#contribs', + }), withResolvedReferenceList({ - list: '#artistRefs', + list: '#contribs.who', data: 'artistData', - into: '#who', + into: '#contribs.who', find: find.artist, notFoundMode: 'null', }), { - dependencies: ['#who', '#what'], + dependencies: ['#contribs.who', '#contribs.what'], mapContinuation: {into}, - compute({'#who': who, '#what': what}, continuation) { + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ into: stitchArrays({who, what}), -- cgit 1.3.0-6-gf8a5 From ceaed5fef3ce2c5d59a6606a6318164b93294f2b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:01:09 -0300 Subject: data: clean up some track property implementations --- src/data/things/track.js | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 5e553b48..25d316eb 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -61,22 +61,12 @@ export class Track extends Thing { color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), - withContainingTrackSection(), - { - dependencies: ['#trackSection'], - compute: ({'#trackSection': trackSection}, continuation) => - // Album.trackSections guarantees the track section will have a - // color property (inheriting from the album's own color), but only - // if it's actually present! Color will be inherited directly from - // album otherwise. - (trackSection - ? trackSection.color - : continuation()), - }, + withContainingTrackSection(), + withPropertyFromObject({object: '#trackSection', property: 'color'}), + exposeDependencyOrContinue({dependency: '#trackSection.color'}), withPropertyFromAlbum({property: 'color'}), - exposeDependency({ dependency: '#album.color', update: {validate: isColor}, @@ -94,19 +84,13 @@ export class Track extends Thing { // of the album's main artwork. It does inherit trackCoverArtFileExtension, // if present on the album. coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [ - // No cover art file extension if the track doesn't have unique artwork - // in the first place. - withHasUniqueCoverArt(), - exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), + exitWithoutUniqueCoverArt(), - // Expose custom coverArtFileExtension update value first. exposeUpdateValueOrContinue(), - // Expose album's trackCoverArtFileExtension if no update value set. withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - // Fallback to 'jpg'. exposeConstant({ value: 'jpg', update: {validate: isFileExtension}, @@ -175,13 +159,7 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ - { - dependencies: ['disableUniqueCoverArt'], - compute: ({disableUniqueCoverArt}, continuation) => - (disableUniqueCoverArt - ? null - : continuation()), - }, + exitWithoutUniqueCoverArt(), withUpdateValueAsDependency(), withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), @@ -559,6 +537,21 @@ function withHasUniqueCoverArt({ ]); } +// Shorthand for checking if the track has unique cover art and exposing a +// fallback value if it isn't. +function exitWithoutUniqueCoverArt({ + value = null, +} = {}) { + return compositeFrom(`exitWithoutUniqueCoverArt`, [ + withHasUniqueCoverArt(), + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: 'falsy', + value, + }), + ]); +} + function trackReverseReferenceList({ property: refListProperty, }) { -- cgit 1.3.0-6-gf8a5 From 7b32066dd9629bbb220c2e2425b5294070b5a0db Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:16:50 -0300 Subject: infra, data: cut unneeded boilerplate from top-level compositions --- src/data/things/album.js | 17 ++++++++--------- src/data/things/index.js | 13 +++++++++++-- src/data/things/track.js | 48 ++++++++++++++++++++++++------------------------ 3 files changed, 43 insertions(+), 35 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index e11d0909..07859537 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -3,7 +3,6 @@ import {stitchArrays} from '#sugar'; import {isDate, isTrackSectionList} from '#validators'; import { - compositeFrom, exitWithoutDependency, exitWithoutUpdateValue, exposeDependency, @@ -51,7 +50,7 @@ export class Album extends Thing { trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), - coverArtDate: compositeFrom(`Album.coverArtDate`, [ + coverArtDate: [ withResolvedContribs({from: 'coverArtistContribs'}), exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), @@ -60,7 +59,7 @@ export class Album extends Thing { dependency: 'date', update: {validate: isDate}, }), - ]), + ], artistContribs: contributionList(), coverArtistContribs: contributionList(), @@ -80,7 +79,7 @@ export class Album extends Thing { data: 'artTagData', }), - trackSections: compositeFrom(`Album.trackSections`, [ + trackSections: [ exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), @@ -150,13 +149,13 @@ export class Album extends Thing { }), }, }, - ]), + ], - coverArtFileExtension: compositeFrom(`Album.coverArtFileExtension`, [ + coverArtFileExtension: [ withResolvedContribs({from: 'coverArtistContribs'}), exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), fileExtension('jpg'), - ]), + ], trackCoverArtFileExtension: fileExtension('jpg'), @@ -189,7 +188,7 @@ export class Album extends Thing { hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), hasBannerArt: contribsPresent('bannerArtistContribs'), - tracks: compositeFrom(`Album.tracks`, [ + tracks: [ exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), @@ -209,7 +208,7 @@ export class Album extends Thing { }), exposeDependency({dependency: '#resolvedReferenceList'}), - ]), + ], }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/index.js b/src/data/things/index.js index 3b73a772..4d8d9d1f 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -2,6 +2,7 @@ import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; import {logError} from '#cli'; +import {compositeFrom} from '#composite'; import * as serialize from '#serialize'; import {openAggregate, showAggregate} from '#sugar'; @@ -130,8 +131,16 @@ function evaluatePropertyDescriptors() { throw new Error(`Missing [Thing.getPropertyDescriptors] function`); } - constructor.propertyDescriptors = - constructor[Thing.getPropertyDescriptors](opts); + const results = constructor[Thing.getPropertyDescriptors](opts); + + for (const [key, value] of Object.entries(results)) { + if (Array.isArray(value)) { + results[key] = compositeFrom(`${constructor.name}.${key}`, value); + continue; + } + } + + constructor.propertyDescriptors = results; }, showFailedClasses(failedClasses) { diff --git a/src/data/things/track.js b/src/data/things/track.js index 25d316eb..a8d59023 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -59,7 +59,7 @@ export class Track extends Thing { urls: urls(), dateFirstReleased: simpleDate(), - color: compositeFrom(`Track.color`, [ + color: [ exposeUpdateValueOrContinue(), withContainingTrackSection(), @@ -71,7 +71,7 @@ export class Track extends Thing { dependency: '#album.color', update: {validate: isColor}, }), - ]), + ], // Disables presenting the track as though it has its own unique artwork. // This flag should only be used in select circumstances, i.e. to override @@ -83,7 +83,7 @@ export class Track extends Thing { // track's unique cover artwork, if any, and does not inherit the extension // of the album's main artwork. It does inherit trackCoverArtFileExtension, // if present on the album. - coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [ + coverArtFileExtension: [ exitWithoutUniqueCoverArt(), exposeUpdateValueOrContinue(), @@ -95,13 +95,13 @@ export class Track extends Thing { value: 'jpg', update: {validate: isFileExtension}, }), - ]), + ], // Date of cover art release. Like coverArtFileExtension, this represents // only the track's own unique cover artwork, if any. This exposes only as // the track's own coverArtDate or its album's trackArtDate, so if neither // is specified, this value is null. - coverArtDate: compositeFrom(`Track.coverArtDate`, [ + coverArtDate: [ withHasUniqueCoverArt(), exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), @@ -112,7 +112,7 @@ export class Track extends Thing { dependency: '#album.trackArtDate', update: {validate: isDate}, }), - ]), + ], commentary: commentary(), lyrics: simpleString(), @@ -136,7 +136,7 @@ export class Track extends Thing { data: 'albumData', }), - artistContribs: compositeFrom(`Track.artistContribs`, [ + artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), withUpdateValueAsDependency(), @@ -148,17 +148,17 @@ export class Track extends Thing { dependency: '#album.artistContribs', update: {validate: isContributionList}, }), - ]), + ], - contributorContribs: compositeFrom(`Track.contributorContribs`, [ + contributorContribs: [ inheritFromOriginalRelease({property: 'contributorContribs'}), contributionList(), - ]), + ], // Cover artists aren't inherited from the original release, since it // typically varies by release and isn't defined by the musical qualities // of the track. - coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [ + coverArtistContribs: [ exitWithoutUniqueCoverArt(), withUpdateValueAsDependency(), @@ -170,25 +170,25 @@ export class Track extends Thing { dependency: '#album.trackCoverArtistContribs', update: {validate: isContributionList}, }), - ]), + ], - referencedTracks: compositeFrom(`Track.referencedTracks`, [ + referencedTracks: [ inheritFromOriginalRelease({property: 'referencedTracks'}), referenceList({ class: Track, find: find.track, data: 'trackData', }), - ]), + ], - sampledTracks: compositeFrom(`Track.sampledTracks`, [ + sampledTracks: [ inheritFromOriginalRelease({property: 'sampledTracks'}), referenceList({ class: Track, find: find.track, data: 'trackData', }), - ]), + ], artTags: referenceList({ class: ArtTag, @@ -208,16 +208,16 @@ export class Track extends Thing { commentatorArtists: commentatorArtists(), - album: compositeFrom(`Track.album`, [ + album: [ withAlbum(), exposeDependency({dependency: '#album'}), - ]), + ], - date: compositeFrom(`Track.date`, [ + date: [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), withPropertyFromAlbum({property: 'date'}), exposeDependency({dependency: '#album.date'}), - ]), + ], // Whether or not the track has "unique" cover artwork - a cover which is // specifically associated with this track in particular, rather than with @@ -226,12 +226,12 @@ export class Track extends Thing { // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) - hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [ + hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), - ]), + ], - otherReleases: compositeFrom(`Track.otherReleases`, [ + otherReleases: [ exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), withOriginalRelease({selfIfOriginal: true}), @@ -253,7 +253,7 @@ export class Track extends Thing { track.originalReleaseTrack === originalRelease)), }, }, - ]), + ], // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't -- cgit 1.3.0-6-gf8a5 From a9b96deeca6b2dacb7fac309c47e7bc6289270e6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:29:51 -0300 Subject: data: be more permissive of steps w/ no special expose behavior --- src/data/things/composite.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e3225563..2dd92f17 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -432,13 +432,8 @@ export function compositeFrom(firstArg, secondArg) { ? step.expose : step); - const stepComputes = !!expose.compute; - const stepTransforms = !!expose.transform; - - if (!stepComputes && !stepTransforms) { - push(new TypeError(`Steps must provide compute or transform (or both)`)); - return; - } + const stepComputes = !!expose?.compute; + const stepTransforms = !!expose?.transform; if ( stepTransforms && !stepComputes && @@ -459,7 +454,7 @@ export function compositeFrom(firstArg, secondArg) { // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties // on the CacheableObject. - for (const dependency of expose.dependencies ?? []) { + for (const dependency of expose?.dependencies ?? []) { if (typeof dependency === 'string' && dependency.startsWith('#')) { continue; } @@ -470,22 +465,14 @@ export function compositeFrom(firstArg, secondArg) { // Mapped dependencies are always exposed on the final composition. // These are explicitly for reading values which are named outside of // the current compositional step. - for (const dependency of Object.values(expose.mapDependencies ?? {})) { + for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } }); } - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - aggregate.push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); - } - } + if (!baseComposes && !baseUpdates && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute`)); } aggregate.close(); @@ -615,6 +602,11 @@ export function compositeFrom(firstArg, secondArg) { ? step.expose : step); + if (!expose) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + const callingTransformForThisStep = expectingTransform && expose.transform; -- cgit 1.3.0-6-gf8a5 From 9109356037ce98af378765302841c957cc96b8d8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:33:04 -0300 Subject: data: exitWithoutContribs utility --- src/data/things/album.js | 9 +++------ src/data/things/thing.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 07859537..dc8d3189 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -23,6 +23,7 @@ import Thing, { contributionList, dimensions, directory, + exitWithoutContribs, fileExtension, flag, name, @@ -31,7 +32,6 @@ import Thing, { simpleString, urls, wikiData, - withResolvedContribs, withResolvedReferenceList, } from './thing.js'; @@ -51,9 +51,7 @@ export class Album extends Thing { dateAddedToWiki: simpleDate(), coverArtDate: [ - withResolvedContribs({from: 'coverArtistContribs'}), - exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), - + exitWithoutContribs({contribs: 'coverArtistContribs'}), exposeUpdateValueOrContinue(), exposeDependency({ dependency: 'date', @@ -152,8 +150,7 @@ export class Album extends Thing { ], coverArtFileExtension: [ - withResolvedContribs({from: 'coverArtistContribs'}), - exitWithoutDependency({dependency: '#resolvedContribs', mode: 'empty'}), + exitWithoutContribs({contribs: 'coverArtistContribs'}), fileExtension('jpg'), ], diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 52f0b773..fe9000b4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -436,6 +436,23 @@ export function withResolvedContribs({ ]); } +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. +export function exitWithoutContribs({ + contribs, + value = null, +}) { + return compositeFrom(`exitWithoutContribs`, [ + withResolvedContribs({from: contribs}), + exitWithoutDependency({ + dependency: '#resolvedContribs', + mode: 'empty', + value, + }), + ]); +} + // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if notFoundMode is set to 'exit', if the find -- cgit 1.3.0-6-gf8a5 From 3083e006fb8be524ca8e37c3194b78b0bf37861f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:33:14 -0300 Subject: data: rearrange Album properties, use exitWithoutContribs more --- src/data/things/album.js | 84 +++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 33 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index dc8d3189..2a8c59ed 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -59,23 +59,44 @@ export class Album extends Thing { }), ], - artistContribs: contributionList(), - coverArtistContribs: contributionList(), - trackCoverArtistContribs: contributionList(), - wallpaperArtistContribs: contributionList(), - bannerArtistContribs: contributionList(), + coverArtFileExtension: [ + exitWithoutContribs({contribs: 'coverArtistContribs'}), + fileExtension('jpg'), + ], - groups: referenceList({ - class: Group, - find: find.group, - data: 'groupData', - }), + trackCoverArtFileExtension: fileExtension('jpg'), - artTags: referenceList({ - class: ArtTag, - find: find.artTag, - data: 'artTagData', - }), + wallpaperFileExtension: [ + exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), + fileExtension('jpg'), + ], + + bannerFileExtension: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + fileExtension('jpg'), + ], + + wallpaperStyle: [ + exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), + simpleString(), + ], + + bannerStyle: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + simpleString(), + ], + + bannerDimensions: [ + exitWithoutContribs({contribs: 'bannerArtistContribs'}), + dimensions(), + ], + + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), + + commentary: commentary(), + additionalFiles: additionalFiles(), trackSections: [ exitWithoutDependency({dependency: 'trackData', value: []}), @@ -149,26 +170,23 @@ export class Album extends Thing { }, ], - coverArtFileExtension: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - fileExtension('jpg'), - ], - - trackCoverArtFileExtension: fileExtension('jpg'), - - wallpaperStyle: simpleString(), - wallpaperFileExtension: fileExtension('jpg'), - - bannerStyle: simpleString(), - bannerFileExtension: fileExtension('jpg'), - bannerDimensions: dimensions(), + artistContribs: contributionList(), + coverArtistContribs: contributionList(), + trackCoverArtistContribs: contributionList(), + wallpaperArtistContribs: contributionList(), + bannerArtistContribs: contributionList(), - hasTrackNumbers: flag(true), - isListedOnHomepage: flag(true), - isListedInGalleries: flag(true), + groups: referenceList({ + class: Group, + find: find.group, + data: 'groupData', + }), - commentary: commentary(), - additionalFiles: additionalFiles(), + artTags: referenceList({ + class: ArtTag, + find: find.artTag, + data: 'artTagData', + }), // Update only -- cgit 1.3.0-6-gf8a5 From f242d1dec3cd905e74eec6ce518781843d5f65d9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:40:15 -0300 Subject: data: update contribsPresent syntax & implementation --- src/data/things/album.js | 6 +++--- src/data/things/thing.js | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 01f52c2d..fb0c3427 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -194,9 +194,9 @@ export class Album extends Thing { commentatorArtists: commentatorArtists(), - hasCoverArt: contribsPresent('coverArtistContribs'), - hasWallpaperArt: contribsPresent('wallpaperArtistContribs'), - hasBannerArt: contribsPresent('bannerArtistContribs'), + hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}), + hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), + hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: compositeFrom(`Album.tracks`, [ exitWithoutDependency({dependency: 'trackData', value: []}), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..0f47dc90 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withResultOfAvailabilityCheck, withUpdateValueAsDependency, } from '#composite'; @@ -297,15 +298,11 @@ export function singleReference({ // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsProperty) { - return { - flags: {expose: true}, - expose: { - dependencies: [contribsProperty], - compute: ({[contribsProperty]: contribs}) => - !empty(contribs), - }, - }; +export function contribsPresent({contribs}) { + return compositeFrom(`contribsPresent`, [ + withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), + exposeDependency({dependency: '#availability'}), + ]); } // Neat little shortcut for "reversing" the reference lists stored on other -- cgit 1.3.0-6-gf8a5 From 272a2f47102451a277d099d032e6f4d0ad673d80 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 18:20:00 -0300 Subject: data: handle missing expose specially in base This is for better compatibility with an updating base that doesn't transform its update value, but attempts to behave reasonably for non-transforming contexts as well. --- src/data/things/composite.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2dd92f17..3a63f22d 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -603,7 +603,31 @@ export function compositeFrom(firstArg, secondArg) { : step); if (!expose) { - debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + if (!isBase) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + + if (expectingTransform) { + debug(() => `step #${i+1} (base) - no expose description, returning so-far update value:`, valueSoFar); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(valueSoFar); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return valueSoFar; + } + } else { + debug(() => `step #${i+1} (base) - no expose description, nothing to continue with`); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return null; + } + } + continue; } -- cgit 1.3.0-6-gf8a5 From b06c194fc02da22564bcb165db33282f411859a3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 18:31:09 -0300 Subject: data, test: filter out empty track sections Also test unmatched track references. --- src/data/things/album.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 9ca662a0..7569eb80 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,6 +1,7 @@ import find from '#find'; -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; import {isDate, isTrackSectionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; import { exitWithoutDependency, @@ -152,20 +153,25 @@ export class Album extends Thing { '#sections.startIndex', ], - transform: (trackSections, { + transform(trackSections, { '#sections.tracks': tracks, '#sections.color': color, '#sections.dateOriginallyReleased': dateOriginallyReleased, '#sections.isDefaultTrackSection': isDefaultTrackSection, '#sections.startIndex': startIndex, - }) => - stitchArrays({ + }) { + filterMultipleArrays( + tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks => !empty(tracks)); + + return stitchArrays({ tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, - }), + }); + } }, }, ], -- cgit 1.3.0-6-gf8a5 From 14329ec8eedb7ad5dcb6a3308a26686bd381ab36 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 19:04:04 -0300 Subject: data, test: ArtTag.nameShort --- src/data/things/art-tag.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 3d65b578..7e466555 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,4 +1,6 @@ +import {exposeUpdateValueOrContinue} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; +import {isName} from '#validators'; import Thing, { color, @@ -19,15 +21,20 @@ export class ArtTag extends Thing { color: color(), isContentWarning: flag(false), - nameShort: { - flags: {update: true, expose: true}, + nameShort: [ + exposeUpdateValueOrContinue(), - expose: { + { dependencies: ['name'], - transform: (value, {name}) => - value ?? name.replace(/ \(.*?\)$/, ''), + compute: ({name}) => + name.replace(/ \([^)]*?\)$/, ''), }, - }, + + { + flags: {update: true, expose: true}, + validate: {isName}, + }, + ], // Update only -- cgit 1.3.0-6-gf8a5 From c4f6c41a248ba9ef4f802cc03c20757d417540e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 21:08:06 -0300 Subject: data: WIP cached composition nonsense --- src/data/things/album.js | 8 ++++ src/data/things/composite.js | 111 +++++++++++++++++++++++++++++++++++++++---- src/data/things/thing.js | 25 ++++++---- 3 files changed, 128 insertions(+), 16 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 7569eb80..b134b78d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -125,6 +125,14 @@ export class Album extends Thing { intoIndices: '#sections.startIndex', }), + { + dependencies: ['#trackRefs'], + compute: ({'#trackRefs': tracks}, continuation) => { + console.log(tracks); + return continuation(); + } + }, + withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3a63f22d..26124b56 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {TupleMap} from '#wiki-data'; import { empty, @@ -341,6 +342,8 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // +const globalCompositeCache = {}; + export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { @@ -567,8 +570,8 @@ export function compositeFrom(firstArg, secondArg) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); + const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); + const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { const expectingTransform = initialValue !== noTransformSymbol; @@ -634,21 +637,83 @@ export function compositeFrom(firstArg, secondArg) { const callingTransformForThisStep = expectingTransform && expose.transform; + let continuationStorage; + const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); - const result = + let result; + + const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.transform(valueSoFar, continuation)) + ? ['transform', valueSoFar, filteredDependencies] + : ['transform', valueSoFar]) : (filteredDependencies - ? expose.compute(filteredDependencies, continuation) - : expose.compute(continuation))); + ? ['compute', filteredDependencies] + : ['compute'])); + + const naturalEvaluate = () => { + const [name, ...args] = getExpectedEvaluation(); + let continuation; + ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); + return expose[name](...args, continuation); + } + + switch (step.cache) { + // Warning! Highly WIP! + case 'aggressive': { + const hrnow = () => { + const hrTime = process.hrtime(); + return hrTime[0] * 1000000000 + hrTime[1]; + }; + + const [name, ...args] = getExpectedEvaluation(); + + let cache = globalCompositeCache[step.annotation]; + if (!cache) { + cache = globalCompositeCache[step.annotation] = { + transform: new TupleMap(), + compute: new TupleMap(), + times: { + read: [], + evaluate: [], + }, + }; + } + + const tuplefied = args + .flatMap(arg => [ + Symbol.for('compositeFrom: tuplefied arg divider'), + ...(typeof arg !== 'object' || Array.isArray(arg) + ? [arg] + : Object.entries(arg).flat()), + ]); + + const readTime = hrnow(); + const cacheContents = cache[name].get(tuplefied); + cache.times.read.push(hrnow() - readTime); + + if (cacheContents) { + ({result, continuationStorage} = cacheContents); + } else { + const evaluateTime = hrnow(); + result = naturalEvaluate(); + cache.times.evaluate.push(hrnow() - evaluateTime); + cache[name].set(tuplefied, {result, continuationStorage}); + } + + break; + } + + default: { + result = naturalEvaluate(); + break; + } + } if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -775,6 +840,7 @@ export function compositeFrom(firstArg, secondArg) { if (baseComposes) { if (anyStepsTransform) expose.transform = transformFn; if (anyStepsCompute) expose.compute = computeFn; + if (base.cacheComposition) expose.cache = base.cacheComposition; } else if (baseUpdates) { expose.transform = transformFn; } else { @@ -785,6 +851,35 @@ export function compositeFrom(firstArg, secondArg) { return constructedDescriptor; } +export function displayCompositeCacheAnalysis() { + const showTimes = (cache, key) => { + const times = cache.times[key].slice().sort(); + + const all = times; + const worst10pc = times.slice(-times.length / 10); + const best10pc = times.slice(0, times.length / 10); + const middle50pc = times.slice(times.length / 4, -times.length / 4); + const middle80pc = times.slice(times.length / 10, -times.length / 10); + + const fmt = val => `${(val / 1000).toFixed(2)}ms`.padStart(9); + const avg = times => times.reduce((a, b) => a + b, 0) / times.length; + + const left = ` - ${key}: `; + const indn = ' '.repeat(left.length); + console.log(left + `${fmt(avg(all))} (all ${all.length})`); + console.log(indn + `${fmt(avg(worst10pc))} (worst 10%)`); + console.log(indn + `${fmt(avg(best10pc))} (best 10%)`); + console.log(indn + `${fmt(avg(middle80pc))} (middle 80%)`); + console.log(indn + `${fmt(avg(middle50pc))} (middle 50%)`); + }; + + for (const [annotation, cache] of Object.entries(globalCompositeCache)) { + console.log(`Cached ${annotation}:`); + showTimes(cache, 'evaluate'); + showTimes(cache, 'read'); + } +} + // Evaluates a function with composite debugging enabled, turns debugging // off again, and returns the result of the function. This is mostly syntax // sugar, but also helps avoid unit tests avoid accidentally printing debug diff --git a/src/data/things/thing.js b/src/data/things/thing.js index b1a9a802..19954b19 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -512,7 +512,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`withResolvedReferenceList`, [ + const composite = compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], @@ -526,13 +526,19 @@ export function withResolvedReferenceList({ }), { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + cache: 'aggressive', + annotation: `withResolvedReferenceList.getMatches`, + flags: {expose: true, compose: true}, + + compute: { + mapDependencies: {list, data}, + options: {findFunction}, + + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, }, { @@ -569,6 +575,9 @@ export function withResolvedReferenceList({ }, }, ]); + + console.log(composite.expose); + return composite; } // Check out the info on reverseReferenceList! -- cgit 1.3.0-6-gf8a5 From d878ab29f20c0727acafb4b1150d4e31d69c55c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:09:48 -0300 Subject: data, html, infra: supporting changes for sanitizing content --- src/data/things/language.js | 88 +++++++++++++++++++++++++++++++-------------- src/data/things/thing.js | 4 +-- 2 files changed, 63 insertions(+), 29 deletions(-) (limited to 'src/data') diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..cc49b735 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,5 +1,10 @@ import Thing from './thing.js'; +import {Tag} from '#html'; +import {isLanguageCode} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + export class Language extends Thing { static [Thing.getPropertyDescriptors] = ({ validators: { @@ -68,7 +73,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: Thing.common.externalFunction({expose: true}), // Expose only @@ -140,19 +145,9 @@ export class Language extends Thing { } formatString(key, args = {}) { - if (this.strings && !this.strings_htmlEscaped) { - throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`); - } - - return this.formatStringHelper(this.strings_htmlEscaped, key, args); - } + const strings = this.strings_htmlEscaped; - formatStringNoHTMLEscape(key, args = {}) { - return this.formatStringHelper(this.strings, key, args); - } - - formatStringHelper(strings, key, args = {}) { - if (!strings) { + if (!this.strings) { throw new Error(`Strings unavailable`); } @@ -160,22 +155,25 @@ export class Language extends Thing { throw new Error(`Invalid key ${key} accessed`); } - const template = strings[key]; + const template = this.strings[key]; // Convert the keys on the args dict from camelCase to CONSTANT_CASE. // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. - const processedArgs = Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - v, - ]); + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedArgs = + Object.entries(args).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); // Replacement time! Woot. Reduce comes in handy here! - const output = processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template - ); + const output = + processedArgs.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. @@ -183,7 +181,37 @@ export class Language extends Thing { throw new Error(`Args in ${key} were missing - output: ${output}`); } - return output; + // Last caveat: Wrap the output in an HTML tag so that it doesn't get + // treated as unsanitized HTML if *it* gets passed as an argument to + // *another* formatString call. + return this.#wrapSanitized(output); + } + + // Escapes HTML special characters so they're displayed as-are instead of + // treated by the browser as a tag. This does *not* have an effect on actual + // html.Tag objects, which are treated as sanitized by default (so that they + // can be nested inside strings at all). + #sanitizeStringArg(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + if (typeof arg !== 'string') { + return arg.toString(); + } + + return escapeHTML(arg); + } + + // Wraps the output of a formatting function in a no-name-nor-attributes + // HTML tag, which will indicate to other calls to formatString that this + // content is a string *that may contain HTML* and doesn't need to + // sanitized any further. It'll still .toString() to just the string + // contents, if needed. + #wrapSanitized(output) { + return new Tag(null, null, output); } formatDate(date) { @@ -252,19 +280,25 @@ export class Language extends Thing { // Conjunction list: A, B, and C formatConjunctionList(array) { this.assertIntlAvailable('intl_listConjunction'); - return this.intl_listConjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listConjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Disjunction lists: A, B, or C formatDisjunctionList(array) { this.assertIntlAvailable('intl_listDisjunction'); - return this.intl_listDisjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listDisjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Unit lists: A, B, C formatUnitList(array) { this.assertIntlAvailable('intl_listUnit'); - return this.intl_listUnit.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listUnit.format( + array.map(item => this.#sanitizeStringArg(item)))); } // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..5705ee7e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -105,8 +105,8 @@ export default class Thing extends CacheableObject { // External function. These should only be used as dependencies for other // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, + externalFunction: ({expose = false} = {}) => ({ + flags: {update: true, expose}, update: {validate: (t) => typeof t === 'function'}, }), -- cgit 1.3.0-6-gf8a5 From 3eb82ab2e3f9d921095af05cf0bc284f335aaa35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:11:44 -0300 Subject: content: misc. changes to handle HTML sanitization --- src/data/things/language.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'src/data') diff --git a/src/data/things/language.js b/src/data/things/language.js index cc49b735..afa9f1ee 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -214,6 +214,28 @@ export class Language extends Thing { return new Tag(null, null, output); } + // Similar to the above internal methods, but this one is public. + // It should be used when embedding content that may not have previously + // been sanitized directly into an HTML tag or template's contents. + // The templating engine usually handles this on its own, as does passing + // a value (sanitized or not) directly as an argument to formatString, + // but if you used a custom validation function ({validate: v => v.isHTML} + // instead of {type: 'string'} / {type: 'html'}) and are embedding the + // contents of a slot directly, it should be manually sanitized with this + // function first. + sanitize(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + return ( + (typeof arg === 'string' + ? new Tag(null, null, escapeHTML(arg)) + : arg)); + } + formatDate(date) { this.assertIntlAvailable('intl_date'); return this.intl_date.format(date); @@ -301,6 +323,13 @@ export class Language extends Thing { array.map(item => this.#sanitizeStringArg(item)))); } + // Lists without separator: A B C + formatListWithoutSeparator(array) { + return this.#wrapSanitized( + array.map(item => this.#sanitizeStringArg(item)) + .join(' ')); + } + // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB formatFileSize(bytes) { if (!bytes) return ''; -- cgit 1.3.0-6-gf8a5 From 9dd9d5c328da8ad1d90cd33d4a13efac92104398 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 12 Sep 2023 14:34:20 -0300 Subject: data: more syntax WIP --- src/data/things/composite.js | 65 ++--- src/data/things/thing.js | 591 +++++++++++++++++++++++++++---------------- 2 files changed, 409 insertions(+), 247 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26124b56..32a61033 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -344,7 +344,9 @@ import { const globalCompositeCache = {}; -export function compositeFrom(firstArg, secondArg) { +export function compositeFrom(description) { + const {annotation, steps: composition} = description; + const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -363,13 +365,6 @@ export function compositeFrom(firstArg, secondArg) { } }; - let annotation, composition; - if (typeof firstArg === 'string') { - [annotation, composition] = [firstArg, secondArg]; - } else { - [annotation, composition] = [null, firstArg]; - } - const base = composition.at(-1); const steps = composition.slice(); @@ -974,6 +969,7 @@ export function exposeConstant({ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, + modeDependency, mode = 'null', into = '#availability', }) { @@ -1026,31 +1022,40 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue({ - dependency, - mode = 'null', -}) { - return compositeFrom(`exposeDependencyOrContinue`, [ - withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), +export const exposeDependencyOrContinue = + templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), + inputs: { + dependency: input(), + mode: input.default('null'), }, - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); -} + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + dependencies: [input('#dependency')], + compute: (continuation, { + [input('#dependency')]: dependency, + }) => + continuation.exit(dependency), + }, + ], + }); // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19954b19..5cfeaeb2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -203,15 +203,18 @@ export function externalFunction() { // also existing on this object! // export function contributionList() { - return compositeFrom(`contributionList`, [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({ - value: [], - update: {validate: isContributionList}, - }), - ]); + return compositeFrom({ + annotation: `contributionList`, + + update: {validate: isContributionList}, + + steps: [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: []}), + ], + }); } // Artist commentary! Generally present on tracks and albums. @@ -259,7 +262,7 @@ export function additionalFiles() { export function referenceList({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -270,29 +273,40 @@ export function referenceList({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`referenceList`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `referenceList`, - withResolvedReferenceList({ - data, find, - list: '#updateValue', - notFoundMode: 'filter', - }), + update: { + validate: validateReferenceList(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReferenceList', - update: { - validate: validateReferenceList(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], + }); } // Corresponding function for a single reference. export function singleReference({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -303,27 +317,56 @@ export function singleReference({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`singleReference`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `singleReference`, - withResolvedReference({ref: '#updateValue', data, find}), + update: { + validate: validateReference(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReference', - update: { - validate: validateReference(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReference({ + ref: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReference'}), + ], + }); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({contribs}) { - return compositeFrom(`contribsPresent`, [ - withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), - exposeDependency({dependency: '#availability'}), - ]); +export function contribsPresent({ + contribs, +}) { + return compositeFrom({ + annotation: `contribsPresent`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + steps: [ + withResultOfAvailabilityCheck({ + fromDependency: '#composition.contribs', + mode: 'empty', + }), + + exposeDependency({dependency: '#availability'}), + ], + }); } // Neat little shortcut for "reversing" the reference lists stored on other @@ -332,10 +375,23 @@ export function contribsPresent({contribs}) { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. export function reverseReferenceList({data, list}) { - return compositeFrom(`reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); + return compositeFrom({ + annotation: `reverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + '#composition.list': list, + }, + + steps: [ + withReverseReferenceList({ + data: '#composition.data', + list: '#composition.list', + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], + }); } // General purpose wiki data constructor, for properties like artistData, @@ -353,39 +409,51 @@ export function wikiData(thingClass) { // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. export function commentatorArtists() { - return compositeFrom(`commentatorArtists`, [ - exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), + return compositeFrom({ + annotation: `commentatorArtists`, + + constantDependencies: { + '#composition.findFunction': find.artists, }, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: find.artist, - }), + steps: [ + exitWithoutDependency({ + dependency: 'commentary', + mode: 'falsy', + value: [], + }), - { - flags: {expose: true}, + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), + }, - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: '#composition.findFunction', + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, }, - }, - ]); + ], + }); } // Compositional utilities @@ -398,39 +466,54 @@ export function withResolvedContribs({ from, into = '#resolvedContribs', }) { - return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency({ - dependency: from, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - withPropertiesFromList({ - list: from, - properties: ['who', 'what'], - prefix: '#contribs', - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: find.artist, - notFoundMode: 'null', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - mapContinuation: {into}, - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - into: stitchArrays({who, what}), - }); - }, + return compositeFrom({ + annotation: `withResolvedContribs`, + + mapDependencies: { + '#composition.from': from, }, - ]); + + mapContinuation: { + '#composition.into': into, + }, + + constantDependencies: { + '#composition.findFunction': find.artist, + '#composition.notFoundMode': 'null', + }, + + steps: [ + raiseWithoutDependency({ + dependency: '#composition.from', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + withPropertiesFromList({ + list: '#composition.from', + prefix: '#contribs', + properties: ['who', 'what'], + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: '#composition.findFunction', + notFoundMode: '#composition.notFoundMode', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); + }, + }, + ], + }); } // Shorthand for exiting if the contribution list (usually a property's update @@ -440,14 +523,37 @@ export function exitWithoutContribs({ contribs, value = null, }) { - return compositeFrom(`exitWithoutContribs`, [ - withResolvedContribs({from: contribs}), - exitWithoutDependency({ - dependency: '#resolvedContribs', - mode: 'empty', - value, - }), - ]); + return compositeFrom({ + annotation: `exitWithoutContribs`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + constantDependencies: { + '#composition.value': value, + }, + + steps: [ + withResolvedContribs({from: '#composition.contribs'}), + + withResultOfAvailabilityCheck({ + fromDependency: '#resolvedContribs', + mode: 'empty', + }), + + { + dependencies: ['#availability', '#composition.value'], + compute: ({ + '#availability': availability, + '#composition.value': value, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], + }); } // Resolves a reference by using the provided find function to match it @@ -461,39 +567,54 @@ export function withResolvedReference({ data, find: findFunction, into = '#resolvedReference', - notFoundMode = 'null', + notFoundMode, }) { - if (!['exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be exit or null`); - } - - return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency({ - dependency: ref, - map: {into}, - raise: {into: null}, - }), - - exitWithoutDependency({ - dependency: data, - }), - - { - options: {findFunction, notFoundMode}, - mapDependencies: {ref, data}, - mapContinuation: {match: into}, + return compositeFrom({ + annotation: `withResolvedReference`, + + mapDependencies: { + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, - compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + constantDependencies: { + '#composition.notFoundMode': 'null', + }, - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } + mapContinuation: { + '#composition.into': into, + }, - return continuation.raise({match}); + steps: [ + raiseWithoutDependency({ + dependency: '#composition.ref', + raise: {'#composition.into': null}, + }), + + exitWithoutDependency({ + dependency: '#composition.data', + }), + + { + compute({ + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, }, - }, - ]); + ], + }); } // Resolves a list of references, with each reference matched with provided @@ -505,79 +626,93 @@ export function withResolvedReferenceList({ list, data, find: findFunction, + notFoundMode, into = '#resolvedReferenceList', - notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - const composite = compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - raiseWithoutDependency({ - dependency: list, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - { - cache: 'aggressive', - annotation: `withResolvedReferenceList.getMatches`, - flags: {expose: true, compose: true}, - - compute: { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, + return compositeFrom({ + annotation: `withResolvedReferenceList`, + + mapDependencies: { + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, }, - { - dependencies: ['#matches'], - mapContinuation: {into}, + constantDependencies: { + '#composition.notFoundMode': 'filter', + }, - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({into: matches}) - : continuation()), + mapContinuation: { + '#composition.into': into, }, - { - dependencies: ['#matches'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute({ - '#matches': matches, - '#options': {notFoundMode}, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({into: matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({into: matches}); - } + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + raiseWithoutDependency({ + dependency: '#composition.list', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + { + dependencies: [ + '#composition.list', + '#composition.data', + '#composition.findFunction', + ], + + compute: ({ + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, + + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - }, - ]); - console.log(composite.expose); - return composite; + { + dependencies: ['#matches', '#composition.notFoundMode'], + compute({ + '#matches': matches, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], + }); } // Check out the info on reverseReferenceList! @@ -587,22 +722,44 @@ export function withReverseReferenceList({ list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`withReverseReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {into}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - into: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), + return compositeFrom({ + annotation: `withReverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.refListProperty': refListProperty, + }, + + mapContinuation: { + '#composition.into': into, }, - ]); + + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], + }); } -- cgit 1.3.0-6-gf8a5 From 88ae3f19a38782ca1396b8bc131d1adffb9699e2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 14 Sep 2023 08:54:34 -0300 Subject: data: update syntax for essential compositional utilities Also withPropertyFromObject because some commits were messed up along the way... WIP as usual. --- src/data/things/composite.js | 422 ++++++++++++++++++++----------------------- 1 file changed, 198 insertions(+), 224 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 32a61033..3e766b2c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -953,11 +953,10 @@ export function exposeConstant({ }; } -// Checks the availability of a dependency or the update value and provides -// the result to later steps under '#availability' (by default). This is -// mainly intended for use by the more specific utilities, which you should -// consider using instead. Customize {mode} to select one of these modes, -// or leave unset and default to 'null': +// Checks the availability of a dependency and provides the result to later +// steps under '#availability' (by default). This is mainly intended for use +// by the more specific utilities, which you should consider using instead. +// Customize {mode} to select one of these modes, or default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. @@ -966,274 +965,249 @@ export function exposeConstant({ // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! // -export function withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - modeDependency, - mode = 'null', - into = '#availability', -}) { - if (!['null', 'empty', 'falsy'].includes(mode)) { - throw new TypeError(`Expected mode to be null, empty, or falsy`); - } - if (fromUpdateValue && fromDependency) { - throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); - } +const availabilityCheckMode = { + validate: oneOf('null', 'empty', 'falsy'), + defaultValue: 'null', +}; - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } +export const withResultOfAvailabilityCheck = templateCompositeFrom({ + annotation: `withResultOfAvailabilityCheck`, - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null && value !== undefined; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; + inputs: { + from: input(), + mode: input(availabilityCheckMode), + }, - if (fromDependency) { - return { - annotation: `withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {into}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({into: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {into}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {into: checkAvailability(value, mode)}), - }, - }; - } -} + outputs: { + into: '#availability', + }, -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = - templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, + steps: [ + { + dependencies: [input('from'), input('mode')], - inputs: { - dependency: input(), - mode: input.default('null'), - }, + compute: (continuation, { + [input('from')]: dependency, + [input('mode')]: mode, + }) => { + let availability; - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability'], - compute: (continuation, { - ['#availability']: availability, - }) => - (availability - ? continuation() - : continuation.raise()), - }, + switch (mode) { + case 'null': + availability = value !== null && value !== undefined; + break; + + case 'empty': + availability = !empty(value); + break; + + case 'falsy': + availability = !!value && (!Array.isArray(value) || !empty(value)); + break; + } - { - dependencies: [input('#dependency')], - compute: (continuation, { - [input('#dependency')]: dependency, - }) => - continuation.exit(dependency), + return continuation({into: availability}); }, - ], - }); + }, + ], +}); -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! -export function exposeUpdateValueOrContinue({ - mode = 'null', -} = {}) { - return compositeFrom(`exposeUpdateValueOrContinue`, [ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options! +export const exposeDependencyOrContinue = templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, + + inputs: { + dependency: input(), + mode: input(availabilityCheckMode), + }, + + steps: () => [ withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, + from: input('dependency'), + mode: input('mode'), }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('dependency')], + compute: (continuation, { + ['#availability']: availability, + [input('dependency')]: dependency, + }) => (availability - ? continuation() - : continuation.raise()), + ? continuation.exit(dependency) + : continuation()), }, + ], +}); - { - transform: (value, continuation) => - continuation.exit(value), - }, - ]); -} +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. See withResultOfAvailabilityCheck +// for {mode} options! +export const exposeUpdateValueOrContinue = templateCompositeFrom({ + annotation: `exposeUpdateValueOrContinue`, -// Early exits if an availability check has failed. -// This is for internal use only - use `exitWithoutDependency` or -// `exitWithoutUpdateValue` instead. -export function exitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, -} = {}) { - return compositeFrom(`exitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + inputs: { + mode: input(availabilityCheckMode), + }, - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); -} + steps: () => [ + exposeDependencyOrContinue({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency({ - dependency, - mode = 'null', - value = null, -}) { - return compositeFrom(`exitWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} +export const exitWithoutDependency = templateCompositeFrom({ + annotation: `exitWithoutDependency`, -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutUpdateValue({ - mode = 'null', - value = null, -} = {}) { - return compositeFrom(`exitWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency({ - dependency, - mode = 'null', - map = {}, - raise = {}, -}) { - return compositeFrom(`raiseWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('value')], + continuation: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.exit(value)), }, + ], +}); - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); -} +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export const exitWithoutUpdateValue = templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, + + steps: [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); -// Raises if this property's update value isn't available. +// Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, -} = {}) { - return compositeFrom(`raiseWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), +export const raiseOutputWithoutDependency = templateCompositeFrom({ + annotation: `raiseOutputWithoutDependency`, + + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, + + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.raiseOutputAbove(output)), }, + ], +}); - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); -} +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ + annotation: `raiseOutputWithoutUpdateValue`, -// Turns an updating property's update value into a dependency, so it can be -// conveniently passed to other functions. -export function withUpdateValueAsDependency({ - into = '#updateValue', -} = {}) { - return { - annotation: `withUpdateValueAsDependency`, - flags: {expose: true, compose: true}, + inputs: { + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, - expose: { - mapContinuation: {into}, - transform: (value, continuation) => - continuation(value, {into: value}), + steps: [ + withResultOfAvailabilityCheck({ + from: input.updateValue(), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), }, - }; -} + ], +}); // Gets a property of some object (in a dependency) and provides that value. // If the object itself is null, or the object doesn't have the listed property, // the provided dependency will also be null. -export function withPropertyFromObject({ - object, - property, - into = null, -}) { - into ??= - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`); +export const withPropertyFromObject = templateCompositeFrom({ + annotation: `withPropertyFromObject`, - return { - annotation: `withPropertyFromObject`, - flags: {expose: true, compose: true}, + inputs: { + object: input({type: 'object', null: true}), + property: input.required({type: 'string'}), + } - expose: { - mapDependencies: {object}, - mapContinuation: {into}, - options: {property}, + outputs: { + into: { + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + default: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`), + }, + }, - compute: ({object, '#options': {property}}, continuation) => - (object === null || object === undefined + steps: [ + { + dependencies: [input('object'), input('property')], + compute: (continuation, { + [input('object')]: object, + [input('property')]: property, + }) => + (object === null ? continuation({into: null}) : continuation({into: object[property] ?? null})), }, - }; -} + ], +}); // Gets the listed properties from some object, providing each property's value // as a dependency prefixed with the same name as the object (by default). -- cgit 1.3.0-6-gf8a5 From 194676f45f54d09a3ad247e9ba4e2b3ba2e56db4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 15 Sep 2023 20:02:44 -0300 Subject: data: experimental templateCompositeFrom implementation --- src/data/things/composite.js | 374 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3e766b2c..091faa3a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -7,6 +7,7 @@ import { empty, filterProperties, openAggregate, + decorateErrorWithIndex, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -344,6 +345,379 @@ import { const globalCompositeCache = {}; +export function input(nameOrDescription) { + if (typeof nameOrDescription === 'string') { + return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); + } else { + return { + symbol: Symbol.for('hsmusic.composite.input'), + shape: 'input', + value: nameOrDescription, + }; + } +} + +input.symbol = Symbol.for('hsmusic.composite.input'); + +input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); +input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); +input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); +input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); + +function isInputToken(token) { + if (typeof token === 'object') { + return token.symbol === Symbol.for('hsmusic.composite.input'); + } else if (typeof token === 'symbol') { + return token.description.startsWith('hsmusic.composite.input'); + } else { + return false; + } +} + +function getInputTokenShape(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.shape; + } else { + return token.description.match(/hsmusic\.composite\.(input.*?)(:|$)/)[1]; + } +} + +function getInputTokenValue(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.value; + } else { + return token.description.match(/hsmusic\.composite\.input.*?:(.*)/)?.[1] ?? null; + } +} + +export function templateCompositeFrom(description) { + const compositeName = + (description.annotation + ? description.annotation + : `unnamed composite`); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + + if ('steps' in description) { + if (Array.isArray(description.steps)) { + descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + } + } + + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; + + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (getInputTokenShape(value) !== 'input') { + wrongCallsToInput.push(name); + } + } + + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); + } + + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input(), got ${shape}`)); + } + }); + + descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { + const wrongType = []; + const notPrivate = []; + + const missingDependenciesDefault = []; + const wrongDependenciesType = []; + const wrongDefaultType = []; + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value === 'object') { + if (!('dependencies' in value && 'default' in value)) { + missingDependenciesDefault.push(name); + continue; + } + + if (!Array.isArray(value.dependencies)) { + wrongDependenciesType.push(name); + } + + if (typeof value.default !== 'function') { + wrongDefaultType.push(name); + } + + continue; + } + + if (typeof value !== 'string') { + wrongType.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivate.push(name); + continue; + } + } + + for (const name of wrongType) { + const type = typeof description.outputs[name]; + push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivate) { + const into = description.outputs[name]; + push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + for (const name of missingDependenciesDefault) { + push(new Error(`${name}: Expected both dependencies & default`)); + } + + for (const name of wrongDependenciesType) { + const {dependencies} = description.outputs[name]; + push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); + } + + for (const name of wrongDefaultType) { + const type = typeof description.outputs[name].default; + push(new Error(`${name}: Expected default to be function, got ${type}`)); + } + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value !== 'object') continue; + + map( + description.outputs[name].dependencies, + decorateErrorWithIndex(dependency => { + if (!isInputToken(dependency)) { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); + } + + const shape = getInputTokenShape(dependency); + if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + } + }), + {message: `${name}: Errors in dependencies`}); + } + }); + + descriptionAggregate.close(); + + const expectedInputNames = + (description.inputs + ? Object.keys(description.inputs) + : []); + + const expectedOutputNames = + (description.outputs + ? Object.keys(description.outputs) + : []); + + return (inputOptions = {}) => { + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + if (inputDescription.null === true) return false; + return true; + }); + + const wrongTypeInputNames = []; + const wrongInputCallInputNames = []; + + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } + + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } + } + + if (!empty(misplacedInputNames)) { + inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } + + if (!empty(missingInputNames)) { + inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } + + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + + inputOptionsAggregate.close(); + + const outputOptions = {}; + + const instantiatedTemplate = { + symbol: templateCompositeFrom.symbol, + + outputs(providedOptions) { + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + const notPrivateOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } + + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivateOutputNames.push(name); + continue; + } + } + + if (!empty(misplacedOutputNames)) { + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + } + + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivateOutputNames) { + const into = providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + outputOptionsAggregate.close(); + + Object.assign(outputOptions, providedOptions); + return instantiatedTemplate; + }, + + toDescription() { + const finalDescription = {}; + + if ('annotation' in description) { + finalDescription.annotation = description.annotation; + } + + if ('update' in description) { + finalDescription.update = description.update; + } + + if ('inputs' in description) { + const finalInputs = {}; + + for (const [name, description_] of Object.entries(description.inputs)) { + const description = description_; + if (name in inputOptions) { + if (typeof inputOptions[name] === 'string') { + finalInputs[name] = input.dependency(inputOptions[name]); + } else { + finalInputs[name] = inputOptions[name]; + } + } else if (description.defaultValue) { + finalInputs[name] = input.value(defaultValue); + } else if (description.defaultDependency) { + finalInputs[name] = input.dependency(defaultValue); + } else { + finalInputs[name] = input.value(null); + } + } + + finalDescription.inputs = finalInputs; + } + + if ('outputs' in description) { + const finalOutputs = {}; + + for (const [name, defaultDependency] of Object.entries(description.outputs)) { + if (name in outputOptions) { + finalOutputs[name] = outputOptions[name]; + } else { + finalOutputs[name] = defaultDependency; + } + } + + finalDescription.outputs = finalOutputs; + } + + if ('steps' in description) { + finalDescription.steps = description.steps; + } + + return finalDescription; + }, + + toResolvedComposition() { + const ownDescription = instantiatedTemplate.toDescription(); + + const finalDescription = {...ownDescription}; + + const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + + const steps = ownDescription.steps(); + + const resolvedSteps = + aggregate.map( + steps, + decorateErrorWithIndex(step => + (step.symbol === templateCompositeFrom.symbol + ? step.toResolvedComposition() + : step)), + {message: `Errors resolving steps`}); + + aggregate.close(); + + finalDescription.steps = resolvedSteps; + + return finalDescription; + }, + }; + + return instantiatedTemplate; + }; +} + +templateCompositeFrom.symbol = Symbol(); + export function compositeFrom(description) { const {annotation, steps: composition} = description; -- cgit 1.3.0-6-gf8a5 From 7cd3bdc4998ae1fc1b9ab4bb721d2727f64511e1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 15 Sep 2023 20:03:25 -0300 Subject: data: miscellaneous composite template updates --- src/data/things/album.js | 10 +- src/data/things/composite.js | 23 +- src/data/things/homepage-layout.js | 11 +- src/data/things/thing.js | 543 ++++++++++++++++++------------------- src/data/things/track.js | 93 +++---- 5 files changed, 323 insertions(+), 357 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index b134b78d..805d177d 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -12,7 +12,6 @@ import { withFlattenedArray, withPropertiesFromList, withUnflattenedArray, - withUpdateValueAsDependency, } from '#composite'; import Thing, { @@ -103,16 +102,15 @@ export class Album extends Thing { exitWithoutDependency({dependency: 'trackData', value: []}), exitWithoutUpdateValue({value: [], mode: 'empty'}), - withUpdateValueAsDependency({into: '#sections'}), - withPropertiesFromList({ - list: '#sections', - properties: [ + list: input.updateValue(), + prefix: input.value('#sections'), + properties: input.value([ 'tracks', 'dateOriginallyReleased', 'isDefaultTrackSection', 'color', - ], + ]), }), fillMissingListItems({list: '#sections.tracks', value: []}), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 091faa3a..cd713169 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1357,7 +1358,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ into: '#availability', }, - steps: [ + steps: () => [ { dependencies: [input('from'), input('mode')], @@ -1440,12 +1441,12 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), - value: input({defaultValue: null}), + value: input({null: true}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1474,7 +1475,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ value: input({defaultValue: null}), }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), @@ -1488,12 +1489,12 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1522,7 +1523,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), mode: input('mode'), @@ -1549,8 +1550,8 @@ export const withPropertyFromObject = templateCompositeFrom({ inputs: { object: input({type: 'object', null: true}), - property: input.required({type: 'string'}), - } + property: input({type: 'string'}), + }, outputs: { into: { @@ -1569,7 +1570,7 @@ export const withPropertyFromObject = templateCompositeFrom({ }, }, - steps: [ + steps: () => [ { dependencies: [input('object'), input('property')], compute: (continuation, { diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index b509c1e2..1d86f4d0 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -3,7 +3,6 @@ import find from '#find'; import { compositeFrom, exposeDependency, - withUpdateValueAsDependency, } from '#composite'; import { @@ -115,22 +114,20 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { : continuation(value)), }, - withUpdateValueAsDependency(), - withResolvedReference({ - ref: '#updateValue', + ref: input.updateValue(), data: 'groupData', - find: find.group, + find: input.value(find.group), }), exposeDependency({ dependency: '#resolvedReference', - update: { + update: input.value({ validate: oneOf( is('new-releases', 'new-additions'), validateReference(Group[Thing.referenceType])), - }, + }), }), ]), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5cfeaeb2..d1a8fdc1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,6 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; +import {oneOf} from '#validators'; import { compositeFrom, @@ -14,10 +15,11 @@ import { exposeConstant, exposeDependency, exposeDependencyOrContinue, - raiseWithoutDependency, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, withResultOfAvailabilityCheck, withPropertiesFromList, - withUpdateValueAsDependency, } from '#composite'; import { @@ -208,7 +210,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedContribs({from: '#updateValue'}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), @@ -288,7 +290,7 @@ export function referenceList({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReferenceList({ @@ -332,7 +334,7 @@ export function singleReference({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReference({ @@ -358,7 +360,7 @@ export function contribsPresent({ '#composition.contribs': contribs, }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ fromDependency: '#composition.contribs', mode: 'empty', @@ -383,7 +385,7 @@ export function reverseReferenceList({data, list}) { '#composition.list': list, }, - steps: [ + steps: () => [ withReverseReferenceList({ data: '#composition.data', list: '#composition.list', @@ -416,7 +418,7 @@ export function commentatorArtists() { '#composition.findFunction': find.artists, }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: 'commentary', mode: 'falsy', @@ -462,99 +464,97 @@ export function commentatorArtists() { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({ - from, - into = '#resolvedContribs', -}) { - return compositeFrom({ - annotation: `withResolvedContribs`, - - mapDependencies: { - '#composition.from': from, - }, - - mapContinuation: { - '#composition.into': into, - }, - - constantDependencies: { - '#composition.findFunction': find.artist, - '#composition.notFoundMode': 'null', - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.from', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - withPropertiesFromList({ - list: '#composition.from', - prefix: '#contribs', - properties: ['who', 'what'], - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: '#composition.findFunction', - notFoundMode: '#composition.notFoundMode', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - '#composition.into': stitchArrays({who, what}), - }); - }, +export const withResolvedContribs = templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + // todo: validate + from: input(), + + findFunction: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedContribs', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: input('find'), + notFoundMode: input('notFoundMode'), + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); }, - ], - }); -} + }, + ], +}); // Shorthand for exiting if the contribution list (usually a property's update // value) resolves to empty - ensuring that the later computed results are only // returned if these contributions are present. -export function exitWithoutContribs({ - contribs, - value = null, -}) { - return compositeFrom({ - annotation: `exitWithoutContribs`, - - mapDependencies: { - '#composition.contribs': contribs, +export const exitWithoutContribs = templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + // todo: validate + contribs: input(), + + value: input({null: true}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), }, - - constantDependencies: { - '#composition.value': value, - }, - - steps: [ - withResolvedContribs({from: '#composition.contribs'}), - - withResultOfAvailabilityCheck({ - fromDependency: '#resolvedContribs', - mode: 'empty', - }), - - { - dependencies: ['#availability', '#composition.value'], - compute: ({ - '#availability': availability, - '#composition.value': value, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], - }); -} + ], +}); // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -562,204 +562,187 @@ export function exitWithoutContribs({ // function doesn't match anything for the reference. Otherwise, the data // object is provided on the output dependency; or null, if the reference // doesn't match anything or itself was null to begin with. -export function withResolvedReference({ - ref, - data, - find: findFunction, - into = '#resolvedReference', - notFoundMode, -}) { - return compositeFrom({ - annotation: `withResolvedReference`, - - mapDependencies: { - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'null', - }, - - mapContinuation: { - '#composition.into': into, - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.ref', - raise: {'#composition.into': null}, - }), - - exitWithoutDependency({ - dependency: '#composition.data', - }), - - { - compute({ - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, +export const withResolvedReference = templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + // todo: validate + ref: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedReference', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({into: null}), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute({ + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); }, - ], - }); -} + }, + ], +}); // Resolves a list of references, with each reference matched with provided // data in the same way as withResolvedReference. This will early exit if the // data dependency is null (even if the reference list is empty). By default // it will filter out references which don't match, but this can be changed // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export function withResolvedReferenceList({ - list, - data, - find: findFunction, - notFoundMode, - into = '#resolvedReferenceList', -}) { - if (!['filter', 'exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); - } - - return compositeFrom({ - annotation: `withResolvedReferenceList`, - - mapDependencies: { - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'filter', +export const withResolvedReferenceList = templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + // todo: validate + list: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: { + into: '#resolvedReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + { + dependencies: [input('list'), input('data'), input('find')], + compute: ({ + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), }, - mapContinuation: { - '#composition.into': into, + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - raiseWithoutDependency({ - dependency: '#composition.list', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - { - dependencies: [ - '#composition.list', - '#composition.data', - '#composition.findFunction', - ], - - compute: ({ - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - }, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + { + dependencies: ['#matches', input('notFoundMode')], + compute({ + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } }, - - { - dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) - : continuation()), - }, - - { - dependencies: ['#matches', '#composition.notFoundMode'], - compute({ - '#matches': matches, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], - }); -} + }, + ], +}); // Check out the info on reverseReferenceList! // This is its composable form. -export function withReverseReferenceList({ - data, - list: refListProperty, - into = '#reverseReferenceList', -}) { - return compositeFrom({ - annotation: `withReverseReferenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.refListProperty': refListProperty, - }, - - mapContinuation: { - '#composition.into': into, +export const withReverseReferenceList = templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + // todo: validate + data: input(), + + list: input({type: 'string'}), + }, + + outputs: { + into: '#reverseReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), }, - - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], - - compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, - }, continuation) => - continuation({ - '#composition.into': - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], - }); -} + ], +}); diff --git a/src/data/things/track.js b/src/data/things/track.js index a8d59023..ccfbc357 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,9 +11,11 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, withPropertyFromObject, withResultOfAvailabilityCheck, - withUpdateValueAsDependency, } from '#composite'; import { @@ -21,6 +23,7 @@ import { isContributionList, isDate, isFileExtension, + oneOf, } from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -139,8 +142,10 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue', into: '#artistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({into: '#artistContribs'}), + exposeDependencyOrContinue({dependency: '#artistContribs'}), withPropertyFromAlbum({property: 'artistContribs'}), @@ -161,8 +166,10 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({into: '#coverArtistContribs'}), + exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), @@ -302,6 +309,7 @@ export class Track extends Thing { } } +/* // Early exits with a value inherited from the original release, if // this track is a rerelease, and otherwise continues with no further // dependencies provided. If allowOverride is true, then the continuation @@ -327,77 +335,55 @@ function inheritFromOriginalRelease({ }, ]); } +*/ // Gets the track's album. This will early exit if albumData is missing. // By default, if there's no album whose list of tracks includes this track, // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. -function withAlbum({ - into = '#album', - notFoundMode = 'null', -} = {}) { - return compositeFrom(`withAlbum`, [ - withResultOfAvailabilityCheck({ - fromDependency: 'albumData', - mode: 'empty', - into: '#albumDataAvailability', +export const withAlbum = templateCompositeFrom({ + annotation: `Track.withAlbum`, + + inputs: { + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', }), + }, - { - dependencies: ['#albumDataAvailability'], - options: {notFoundMode}, - mapContinuation: {into}, + outputs: { + into: '#album', + }, - compute: ({ - '#albumDataAvailability': albumDataAvailability, - '#options': {notFoundMode}, - }, continuation) => - (albumDataAvailability - ? continuation() - : (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raise({into: null}))), - }, + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'albumData', + mode: input.value('empty'), + output: input.value({into: null}), + }), { dependencies: ['this', 'albumData'], - compute: ({this: track, albumData}, continuation) => + compute: (continuation, {this: track, albumData}) => continuation({ '#album': albumData.find(album => album.tracks.includes(track)), }), }, - withResultOfAvailabilityCheck({ - fromDependency: '#album', - mode: 'null', - into: '#albumAvailability', + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({into: null}), }), - { - dependencies: ['#albumAvailability'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute: ({ - '#albumAvailability': albumAvailability, - '#options': {notFoundMode}, - }, continuation) => - (albumAvailability - ? continuation() - : (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raise({into: null}))), - }, - { dependencies: ['#album'], - mapContinuation: {into}, - compute: ({'#album': album}, continuation) => + compute: (continuation, {'#album': album}) => continuation({into: album}), }, - ]); -} + ], +}); +/* // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; @@ -571,3 +557,4 @@ function trackReverseReferenceList({ }, ]); } +*/ -- cgit 1.3.0-6-gf8a5 From b4dd9d3f288130acdd9fefa2321b4b547f348b32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 16 Sep 2023 13:03:26 -0300 Subject: data: more WIP syntax updates --- src/data/things/album.js | 1 + src/data/things/composite.js | 14 +- src/data/things/homepage-layout.js | 1 + src/data/things/thing.js | 72 ++++++--- src/data/things/track.js | 299 ++++++++++++++++++++++--------------- 5 files changed, 244 insertions(+), 143 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 805d177d..c0042ae2 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -8,6 +8,7 @@ import { exitWithoutUpdateValue, exposeDependency, exposeUpdateValueOrContinue, + input, fillMissingListItems, withFlattenedArray, withPropertiesFromList, diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cd713169..f2ca2c7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -420,7 +420,7 @@ export function templateCompositeFrom(description) { const missingCallsToInput = []; const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { + for (const [name, value] of Object.entries(description.inputs ?? {})) { if (!isInputToken(value)) { missingCallsToInput.push(name); continue; @@ -533,7 +533,7 @@ export function templateCompositeFrom(description) { ? Object.keys(description.outputs) : []); - return (inputOptions = {}) => { + const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); const providedInputNames = Object.keys(inputOptions); @@ -593,7 +593,7 @@ export function templateCompositeFrom(description) { const misplacedOutputNames = []; const wrongTypeOutputNames = []; - const notPrivateOutputNames = []; + // const notPrivateOutputNames = []; for (const [name, value] of Object.entries(providedOptions)) { if (!expectedOutputNames.includes(name)) { @@ -606,10 +606,12 @@ export function templateCompositeFrom(description) { continue; } + /* if (!value.startsWith('#')) { notPrivateOutputNames.push(name); continue; } + */ } if (!empty(misplacedOutputNames)) { @@ -621,10 +623,12 @@ export function templateCompositeFrom(description) { outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); } + /* for (const name of notPrivateOutputNames) { const into = providedOptions[name]; outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); } + */ outputOptionsAggregate.close(); @@ -715,6 +719,10 @@ export function templateCompositeFrom(description) { return instantiatedTemplate; }; + + instantiate.inputs = instantiate; + + return instantiate; } templateCompositeFrom.symbol = Symbol(); diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 1d86f4d0..007e0236 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -3,6 +3,7 @@ import find from '#find'; import { compositeFrom, exposeDependency, + input, } from '#composite'; import { diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d1a8fdc1..45e91238 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -35,6 +35,7 @@ import { isFileExtension, isName, isString, + isType, isURL, validateArrayItems, validateInstanceOf, @@ -211,8 +212,7 @@ export function contributionList() { update: {validate: isContributionList}, steps: () => [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), + withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), ], @@ -261,27 +261,61 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. +export const referenceList = templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: input({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }), + + find: input({type: 'function'}), + + // todo: validate + data: input(), + }, + + update: { + dependencies: [ + input.staticValue('class'), + ], + + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + }, + + steps: () => [ + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}) export function referenceList({ class: thingClass, data, find: findFunction, }) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } - - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - return compositeFrom({ annotation: `referenceList`, - update: { - validate: validateReferenceList(referenceType), - }, - mapDependencies: { '#composition.data': data, }, @@ -292,14 +326,6 @@ export function referenceList({ steps: () => [ withUpdateValueAsDependency(), - - withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), ], }); } diff --git a/src/data/things/track.js b/src/data/things/track.js index ccfbc357..870b9913 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -5,7 +5,6 @@ import find from '#find'; import {empty} from '#sugar'; import { - compositeFrom, exitWithoutDependency, exposeConstant, exposeDependency, @@ -15,7 +14,6 @@ import { raiseOutputWithoutDependency, templateCompositeFrom, withPropertyFromObject, - withResultOfAvailabilityCheck, } from '#composite'; import { @@ -142,9 +140,9 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs({ - from: input.updateValue(), - }).outputs({into: '#artistContribs'}), + withResolvedContribs + .inputs({from: input.updateValue()}) + .outputs({into: '#artistContribs'}), exposeDependencyOrContinue({dependency: '#artistContribs'}), @@ -166,9 +164,9 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withResolvedContribs({ - from: input.updateValue(), - }).outputs({into: '#coverArtistContribs'}), + withResolvedContribs + .inputs({from: input.updateValue()}) + .outputs({into: '#coverArtistContribs'}), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), @@ -271,12 +269,12 @@ export class Track extends Thing { // the "Tracks - by Times Referenced" listing page (or other data // processing). referencedByTracks: trackReverseReferenceList({ - property: 'referencedTracks', + list: 'referencedTracks', }), // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ - property: 'sampledTracks', + list: 'sampledTracks', }), featuredInFlashes: reverseReferenceList({ @@ -309,33 +307,44 @@ export class Track extends Thing { } } -/* // Early exits with a value inherited from the original release, if // this track is a rerelease, and otherwise continues with no further // dependencies provided. If allowOverride is true, then the continuation // will also be called if the original release exposed the requested // property as null. -function inheritFromOriginalRelease({ - property: originalProperty, - allowOverride = false, -}) { - return compositeFrom(`inheritFromOriginalRelease`, [ +export const inheritFromOriginalRelease = templateCompositeFrom({ + annotation: `Track.inheritFromOriginalRelease`, + + inputs: { + property: input({type: 'string'}), + allowOverride: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ withOriginalRelease(), { - dependencies: ['#originalRelease'], - compute({'#originalRelease': originalRelease}, continuation) { - if (!originalRelease) return continuation.raise(); + dependencies: [ + '#originalRelease', + input('property'), + input('allowOverride'), + ], + + compute: (continuation, { + ['#originalRelease']: originalRelease, + [input('property')]: originalProperty, + [input('allowOverride')]: allowOverride, + }) => { + if (!originalRelease) return continuation(); const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation.raise(); + if (allowOverride && value === null) return continuation(); return continuation.exit(value); }, }, - ]); -} -*/ + ], +}); // Gets the track's album. This will early exit if albumData is missing. // By default, if there's no album whose list of tracks includes this track, @@ -383,64 +392,95 @@ export const withAlbum = templateCompositeFrom({ ], }); -/* // Gets a single property from this track's album, providing it as the same // property name prefixed with '#album.' (by default). If the track's album // isn't available, then by default, the property will be provided as null; // set {notFoundMode: 'exit'} to early exit instead. -function withPropertyFromAlbum({ - property, - into = '#album.' + property, - notFoundMode = 'null', -}) { - return compositeFrom(`withPropertyFromAlbum`, [ - withAlbum({notFoundMode}), - withPropertyFromObject({object: '#album', property, into}), - ]); -} +export const withPropertyFromAlbum = templateCompositeFrom({ + annotation: `withPropertyFromAlbum`, + + inputs: { + property: input({type: 'string'}), + + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: { + into: { + dependencies: [input.staticValue('property')], + default: ({ + [input.staticValue('property')]: property, + }) => '#album.' + property, + }, + }, + + steps: () => [ + withAlbum({ + notFoundMode: input('notFoundMode'), + }), + + withPropertyFromObject + .inputs({object: '#album', property: input('property')}) + .outputs({into: 'into'}), + ], +}); // Gets the track section containing this track from its album's track list. // If notFoundMode is set to 'exit', this will early exit if the album can't be // found or if none of its trackSections includes the track for some reason. -function withContainingTrackSection({ - into = '#trackSection', - notFoundMode = 'null', -} = {}) { - if (!['exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be exit or null`); - } +export const withContainingTrackSection = templateCompositeFrom({ + annotation: `withContainingTrackSection`, + + inputs: { + notFoundMode: input({ + validate: oneOf('exit', 'null'), + defaultValue: 'null', + }), + }, - return compositeFrom(`withContainingTrackSection`, [ - withPropertyFromAlbum({property: 'trackSections', notFoundMode}), + outputs: { + into: '#trackSection', + }, + + steps: () => [ + withPropertyFromAlbum({ + property: input.value('trackSections'), + notFoundMode: input('notFoundMode'), + }), { - dependencies: ['this', '#album.trackSections'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute({ - this: track, - '#album.trackSections': trackSections, - '#options': {notFoundMode}, - }, continuation) { + dependencies: [ + input.myself(), + input('notFoundMode'), + '#album.trackSections', + ], + + compute(continuation, { + [input.myself()]: track, + [input('notFoundMode')]: notFoundMode, + ['#album.trackSections']: trackSections, + }) { if (!trackSections) { - return continuation.raise({into: null}); + return continuation({into: null}); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation.raise({into: trackSection}); + return continuation({into: trackSection}); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation.raise({into: null}); + return continuation({into: null}); } }, }, - ]); -} + ], +}); // Just includes the original release of this track as a dependency. // If this track isn't a rerelease, then it'll provide null, unless the @@ -448,29 +488,40 @@ function withContainingTrackSection({ // itself. Note that this will early exit if the original release is // specified by reference and that reference doesn't resolve to anything. // Outputs to '#originalRelease' by default. -function withOriginalRelease({ - into = '#originalRelease', - selfIfOriginal = false, -} = {}) { - return compositeFrom(`withOriginalRelease`, [ - withResolvedReference({ - ref: 'originalReleaseTrack', - data: 'trackData', - into: '#originalRelease', - find: find.track, - notFoundMode: 'exit', - }), +export const withOriginalRelease = templateCompositeFrom({ + annotation: `withOriginalRelease`, + + inputs: { + selfIfOriginal: input({type: 'boolean', defaultValue: false}), + }, + + outputs: { + into: '#originalRelease', + }, + + steps: () => [ + withResolvedReference + .inputs({ + ref: 'originalReleaseTrack', + data: 'trackData', + find: input.value(find.track), + notFoundMode: input.value('exit'), + }) + .outputs({into: '#originalRelease'}), { - dependencies: ['this', '#originalRelease'], - options: {selfIfOriginal}, - mapContinuation: {into}, - compute: ({ - this: track, - '#originalRelease': originalRelease, - '#options': {selfIfOriginal}, - }, continuation) => - continuation.raise({ + dependencies: [ + input.myself(), + input('selfIfOriginal'), + '#originalRelease', + ], + + compute: (continuation, { + [input.myself()]: track, + [input('selfIfOriginal')]: selfIfOriginal, + ['#originalRelease']: originalRelease, + }) => + continuation({ into: (originalRelease ?? (selfIfOriginal @@ -478,83 +529,97 @@ function withOriginalRelease({ : null)), }), }, - ]); -} + ], +}); // The algorithm for checking if a track has unique cover art is used in a // couple places, so it's defined in full as a compositional step. -function withHasUniqueCoverArt({ - into = '#hasUniqueCoverArt', -} = {}) { - return compositeFrom(`withHasUniqueCoverArt`, [ +export const withHasUniqueCoverArt = templateCompositeFrom({ + annotation: 'withHasUniqueCoverArt', + + outputs: { + into: '#hasUniqueCoverArt', + }, + + steps: () => [ { dependencies: ['disableUniqueCoverArt'], - mapContinuation: {into}, - compute: ({disableUniqueCoverArt}, continuation) => + compute: (continuation, {disableUniqueCoverArt}) => (disableUniqueCoverArt - ? continuation.raise({into: false}) + ? continuation.raiseOutput({into: false}) : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribs', - into: '#coverArtistContribs', - }), + withResolvedContribs + .inputs({from: 'coverArtistContribs'}) + .outputs({into: '#coverArtistContribs'}), { dependencies: ['#coverArtistContribs'], - mapContinuation: {into}, - compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) => + compute: (continuation, { + ['#coverArtistContribs']: contribsFromTrack, + }) => (empty(contribsFromTrack) ? continuation() - : continuation.raise({into: true})), + : continuation.raiseOutput({into: true})), }, withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), { dependencies: ['#album.trackCoverArtistContribs'], - mapContinuation: {into}, - compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) => - (empty(contribsFromAlbum) - ? continuation.raise({into: false}) - : continuation.raise({into: true})), + compute: (continuation, { + ['#album.trackCoverArtistContribs']: contribsFromAlbum, + }) => + continuation({ + into: !empty(contribsFromAlbum), + }), }, - ]); -} + ], +}); // Shorthand for checking if the track has unique cover art and exposing a // fallback value if it isn't. -function exitWithoutUniqueCoverArt({ - value = null, -} = {}) { - return compositeFrom(`exitWithoutUniqueCoverArt`, [ +export const exitWithoutUniqueCoverArt = templateCompositeFrom({ + annotation: `exitWithoutUniqueCoverArt`, + + inputs: { + value: input({null: true}), + }, + + steps: () => [ withHasUniqueCoverArt(), + exitWithoutDependency({ dependency: '#hasUniqueCoverArt', mode: 'falsy', - value, + value: input('value'), }), - ]); -} + ], +}); + +export const trackReverseReferenceList = templateCompositeFrom({ + annotation: `trackReverseReferenceList`, + + inputs: { + list: input({type: 'string'}), + }, -function trackReverseReferenceList({ - property: refListProperty, -}) { - return compositeFrom(`trackReverseReferenceList`, [ + steps: () => [ withReverseReferenceList({ data: 'trackData', - list: refListProperty, + list: input('list'), }), { flags: {expose: true}, expose: { dependencies: ['#reverseReferenceList'], - compute: ({'#reverseReferenceList': reverseReferenceList}) => + compute: ({ + ['#reverseReferenceList']: reverseReferenceList, + }) => reverseReferenceList.filter(track => !track.originalReleaseTrack), }, }, - ]); -} -*/ + ], +}); -- cgit 1.3.0-6-gf8a5 From fd102ee597e2ad2ba8f0950ce1a16fd34029963d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 18 Sep 2023 13:26:18 -0300 Subject: data: MORE composite wip --- src/data/things/composite.js | 44 +++---- src/data/things/thing.js | 265 +++++++++++++++++++------------------------ src/data/things/track.js | 45 +++++--- 3 files changed, 170 insertions(+), 184 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f2ca2c7c..c33fc03c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1349,7 +1349,7 @@ export function exposeConstant({ // for values like zero and the empty string! // -const availabilityCheckMode = { +const availabilityCheckModeInput = { validate: oneOf('null', 'empty', 'falsy'), defaultValue: 'null', }; @@ -1359,7 +1359,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ inputs: { from: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, outputs: { @@ -1403,7 +1403,7 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1432,7 +1432,7 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1450,7 +1450,7 @@ export const exitWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({null: true}), }, @@ -1479,7 +1479,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({defaultValue: null}), }, @@ -1498,7 +1498,7 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1527,7 +1527,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1562,19 +1562,21 @@ export const withPropertyFromObject = templateCompositeFrom({ }, outputs: { - into: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - default: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`), + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return ( + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value')); }, }, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 45e91238..a5f0b78d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays, unique} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import {oneOf} from '#validators'; @@ -253,6 +253,18 @@ export function additionalFiles() { }; } +const thingClassInput = { + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, +}; + // A reference list! Keep in mind this is for general references to wiki // objects of (usually) other Thing subclasses, not specifically leitmotif // references in tracks (although that property uses referenceList too!). @@ -267,18 +279,7 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input({ - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, - }), - + class: input(thingClassInput), find: input({type: 'function'}), // todo: validate @@ -300,127 +301,100 @@ export const referenceList = templateCompositeFrom({ steps: () => [ withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', + list: input.updateValue(), + data: input('data'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReferenceList'}), ], -}) -export function referenceList({ - class: thingClass, - data, - find: findFunction, -}) { - return compositeFrom({ - annotation: `referenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.findFunction': findFunction, - }, - - steps: () => [ - withUpdateValueAsDependency(), - ], - }); -} +}); // Corresponding function for a single reference. -export function singleReference({ - class: thingClass, - data, - find: findFunction, -}) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } +export const singleReference = templateCompositeFrom({ + annotation: `singleReference`, - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } + compose: false, - return compositeFrom({ - annotation: `singleReference`, + inputs: { + class: input(thingClassInput), + find: input({type: 'function'}), - update: { - validate: validateReference(referenceType), - }, + // todo: validate + data: input(), + }, - mapDependencies: { - '#composition.data': data, - }, + update: { + dependencies: [ + input.staticValue('class'), + ], - constantDependencies: { - '#composition.findFunction': findFunction, + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, + }, - steps: () => [ - withUpdateValueAsDependency(), - - withResolvedReference({ - ref: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('findFunction'), + }), - exposeDependency({dependency: '#resolvedReference'}), - ], - }); -} + exposeDependency({dependency: '#resolvedReference'}), + ], +}); // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({ - contribs, -}) { - return compositeFrom({ - annotation: `contribsPresent`, +export const contribsPresent = templateCompositeFrom({ + annotation: `contribsPresent`, - mapDependencies: { - '#composition.contribs': contribs, - }, + compose: false, - steps: () => [ - withResultOfAvailabilityCheck({ - fromDependency: '#composition.contribs', - mode: 'empty', - }), + inputs: { + contribs: input({type: 'string'}), + }, - exposeDependency({dependency: '#availability'}), - ], - }); -} + steps: () => [ + withResultOfAvailabilityCheck({ + fromDependency: input('contribs'), + mode: input.value('empty'), + }), + + exposeDependency({dependency: '#availability'}), + ], +}); // Neat little shortcut for "reversing" the reference lists stored on other // things - for example, tracks specify a "referenced tracks" property, and // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. -export function reverseReferenceList({data, list}) { - return compositeFrom({ - annotation: `reverseReferenceList`, +export const reverseReferenceList = templateCompositeFrom({ + annotation: `reverseReferenceList`, - mapDependencies: { - '#composition.data': data, - '#composition.list': list, - }, + compose: false, - steps: () => [ - withReverseReferenceList({ - data: '#composition.data', - list: '#composition.list', - }), + inputs: { + // todo: validate + data: input(), - exposeDependency({dependency: '#reverseReferenceList'}), - ], - }); -} + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); // General purpose wiki data constructor, for properties like artistData, // trackData, etc. @@ -436,53 +410,50 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists() { - return compositeFrom({ - annotation: `commentatorArtists`, +export const commentatorArtists = templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), - constantDependencies: { - '#composition.findFunction': find.artists, + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: 'falsy', - value: [], - }), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { + flags: {expose: true}, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: '#composition.findFunction', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), }, - ], - }); -} + }, + ], +}); // Compositional utilities diff --git a/src/data/things/track.js b/src/data/things/track.js index 870b9913..b41dbb5b 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -140,9 +140,11 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({property: 'artistContribs'}), - withResolvedContribs - .inputs({from: input.updateValue()}) - .outputs({into: '#artistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({ + '#resolvedContribs': '#artistContribs', + }), exposeDependencyOrContinue({dependency: '#artistContribs'}), @@ -164,9 +166,11 @@ export class Track extends Thing { coverArtistContribs: [ exitWithoutUniqueCoverArt(), - withResolvedContribs - .inputs({from: input.updateValue()}) - .outputs({into: '#coverArtistContribs'}), + withResolvedContribs({ + from: input.updateValue(), + }).outputs({ + '#resolvedContribs': '#coverArtistContribs', + }), exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), @@ -400,7 +404,7 @@ export const withPropertyFromAlbum = templateCompositeFrom({ annotation: `withPropertyFromAlbum`, inputs: { - property: input({type: 'string'}), + property: input.staticValue({type: 'string'}), notFoundMode: input({ validate: oneOf('exit', 'null'), @@ -409,12 +413,10 @@ export const withPropertyFromAlbum = templateCompositeFrom({ }, outputs: { - into: { - dependencies: [input.staticValue('property')], - default: ({ - [input.staticValue('property')]: property, - }) => '#album.' + property, - }, + dependencies: [input.staticValue('property')], + compute: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], }, steps: () => [ @@ -422,9 +424,20 @@ export const withPropertyFromAlbum = templateCompositeFrom({ notFoundMode: input('notFoundMode'), }), - withPropertyFromObject - .inputs({object: '#album', property: input('property')}) - .outputs({into: 'into'}), + withPropertyFromObject({ + object: '#album', + property: input('property'), + }), + + { + dependencies: ['#value', input.staticValue('property')], + compute: (continuation, { + ['#value']: value, + [input.staticValue('property')]: property, + }) => continuation({ + ['#album.' + property]: value, + }), + }, ], }); -- cgit 1.3.0-6-gf8a5 From fdd8f355bfe0992fc340f800297df524276b1946 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 18 Sep 2023 16:05:05 -0300 Subject: data: Track.alwaysReferencedByDirectory flag & field --- src/data/things/track.js | 35 +++++++++++++++++++++++++++++++++++ src/data/yaml.js | 2 ++ 2 files changed, 37 insertions(+) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index e176acb4..14510d96 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -36,6 +36,41 @@ export class Track extends Thing { urls: Thing.common.urls(), dateFirstReleased: Thing.common.simpleDate(), + // Controls how find.track works - it'll never be matched by a reference + // just to the track's name, which means you don't have to always reference + // some *other* (much more commonly referenced) track by directory instead + // of more naturally by name. + alwaysReferenceByDirectory: { + flags: {update: true, expose: true}, + + // Deliberately defaults to null - this will fall back to false in most + // cases. + update: {validate: isBoolean, default: null}, + + expose: { + dependencies: ['name', 'originalReleaseTrackByRef', 'trackData'], + + transform(value, { + name, + originalReleaseTrackByRef, + trackData, + [Track.instance]: thisTrack, + }) { + if (value !== null) return value; + + const original = + find.track( + originalReleaseTrackByRef, + trackData.filter(track => track !== thisTrack), + {quiet: true}); + + if (!original) return false; + + return name === original.name; + } + }, + }, + artistContribsByRef: Thing.common.contribsByRef(), contributorContribsByRef: Thing.common.contribsByRef(), coverArtistContribsByRef: Thing.common.contribsByRef(), diff --git a/src/data/yaml.js b/src/data/yaml.js index 35943199..07e0a3d2 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -338,6 +338,8 @@ export const processTrackDocument = makeProcessDocument(T.Track, { coverArtFileExtension: 'Cover Art File Extension', hasCoverArt: 'Has Cover Art', + alwaysReferenceByDirectory: 'Always Reference By Directory', + lyrics: 'Lyrics', commentary: 'Commentary', additionalFiles: 'Additional Files', -- cgit 1.3.0-6-gf8a5 From 33558828e70e4dd942bec1fbdb8aea3819ed8a19 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 19 Sep 2023 10:16:52 -0300 Subject: data: declare {update} in higher-context locations --- src/data/things/album.js | 9 ++-- src/data/things/composite.js | 92 ++++++++++++++++++++++++++------------ src/data/things/homepage-layout.js | 29 ++++++------ src/data/things/track.js | 33 +++++--------- 4 files changed, 97 insertions(+), 66 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index c0042ae2..ec133a34 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -53,11 +53,12 @@ export class Album extends Thing { coverArtDate: [ exitWithoutContribs({contribs: 'coverArtistContribs'}), - exposeUpdateValueOrContinue(), - exposeDependency({ - dependency: 'date', - update: {validate: isDate}, + + exposeUpdateValueOrContinue({ + validate: input.value(isDate), }), + + exposeDependency({dependency: 'date'}), ], coverArtFileExtension: [ diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c33fc03c..011f307e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1279,34 +1279,21 @@ export function debugComposite(fn) { // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. -// Since this serves as a base, specify a value for {update} to indicate -// that the property as a whole updates (and some previous compositional -// step works with that update value). Set {update: true} to only enable -// the update flag, or set update to an object to specify a descriptor -// (e.g. for custom value validation). // // Please note that this *doesn't* verify that the dependency exists, so // if you provide the wrong name or it hasn't been set by a previous // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency({ - dependency, - update = false, -}) { +export function exposeDependency({dependency}) { return { annotation: `exposeDependency`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1314,25 +1301,16 @@ export function exposeDependency({ // is typically the base of a composition serving as a particular property // descriptor. It generally follows steps which will conditionally early // exit with some other value, with the exposeConstant base serving as the -// fallback default value. Like exposeDependency, set {update} to true or -// an object to indicate that the property as a whole updates. -export function exposeConstant({ - value, - update = false, -}) { +// fallback default value. +export function exposeConstant({value}) { return { annotation: `exposeConstant`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { options: {value}, compute: ({'#options': {value}}) => value, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1427,12 +1405,24 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! +// for {mode} options! Also provide {validate} here to conveniently +// set a custom validation check for this property's update value. export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { mode: input(availabilityCheckModeInput), + validate: input({type: 'function', null: true}), + }, + + update: { + dependencies: [input.staticValue('validate')], + compute: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), }, steps: () => [ @@ -1757,6 +1747,52 @@ export function fillMissingListItems({ } } +// Filters particular values out of a list. Note that this will always +// completely skip over null, but can be used to filter out any other +// primitive or object value. +export const excludeFromList = templateCompositeFrom({ + annotation: `excludeFromList`, + + inputs: { + list: input(), + + item: input({null: true}), + items: input({validate: isArray, null: true}), + }, + + outputs: { + dependencies: [input.staticDependency('list')], + compute: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + }, + + steps: [ + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('item'), + input('items'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: listName, + [input('list')]: listContents, + [input('item')]: excludeItem, + [input('items')]: excludeItems, + }) => continuation({ + [listName ?? '#list']: + listContents.filter(item => { + if (excludeItem !== null && item === excludeItem) return false; + if (!empty(excludeItems) && exclueItems.includes(item)) return false; + return true; + }), + }), + }, + ], +}); + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 007e0236..677a2756 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -109,10 +109,21 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ { - transform: (value, continuation) => - (value === 'new-releases' || value === 'new-additions' - ? value - : continuation(value)), + flags: {expose: true, update: true, compose: true}, + + update: { + validate: + oneOf( + is('new-releases', 'new-additions'), + validateReference(Group[Thing.referenceType])), + }, + + expose: { + transform: (value, continuation) => + (value === 'new-releases' || value === 'new-additions' + ? value + : continuation(value)), + }, }, withResolvedReference({ @@ -121,15 +132,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { find: input.value(find.group), }), - exposeDependency({ - dependency: '#resolvedReference', - update: input.value({ - validate: - oneOf( - is('new-releases', 'new-additions'), - validateReference(Group[Thing.referenceType])), - }), - }), + exposeDependency({dependency: '#resolvedReference'}), ]), sourceAlbums: referenceList({ diff --git a/src/data/things/track.js b/src/data/things/track.js index 28caf1de..135e6d1f 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -121,15 +121,13 @@ export class Track extends Thing { coverArtFileExtension: [ exitWithoutUniqueCoverArt(), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - - exposeConstant({ - value: 'jpg', - update: {validate: isFileExtension}, - }), + exposeConstant({value: 'jpg'}), ], // Date of cover art release. Like coverArtFileExtension, this represents @@ -140,13 +138,12 @@ export class Track extends Thing { withHasUniqueCoverArt(), exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isDate), + }), withPropertyFromAlbum({property: 'trackArtDate'}), - exposeDependency({ - dependency: '#album.trackArtDate', - update: {validate: isDate}, - }), + exposeDependency({dependency: '#album.trackArtDate'}), ], commentary: commentary(), @@ -175,7 +172,7 @@ export class Track extends Thing { inheritFromOriginalRelease({property: 'artistContribs'}), withResolvedContribs({ - from: input.updateValue(), + from: input.updateValue({validate: isContributionList}), }).outputs({ '#resolvedContribs': '#artistContribs', }), @@ -183,10 +180,7 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#artistContribs'}), withPropertyFromAlbum({property: 'artistContribs'}), - exposeDependency({ - dependency: '#album.artistContribs', - update: {validate: isContributionList}, - }), + exposeDependency({dependency: '#album.artistContribs'}), ], contributorContribs: [ @@ -201,7 +195,7 @@ export class Track extends Thing { exitWithoutUniqueCoverArt(), withResolvedContribs({ - from: input.updateValue(), + from: input.updateValue({validate: isContributionList}), }).outputs({ '#resolvedContribs': '#coverArtistContribs', }), @@ -209,10 +203,7 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), - exposeDependency({ - dependency: '#album.trackCoverArtistContribs', - update: {validate: isContributionList}, - }), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ], referencedTracks: [ -- cgit 1.3.0-6-gf8a5 From 86679ee48eee7e1000b2b2f35e4c3d1a8d1be143 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 13:01:04 -0300 Subject: data: update a bunch of template composite validation --- src/data/things/composite.js | 290 +++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 135 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 011f307e..98b04a7e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -416,110 +416,65 @@ export function templateCompositeFrom(description) { } } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; - - for (const [name, value] of Object.entries(description.inputs ?? {})) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; - } - - if (getInputTokenShape(value) !== 'input') { - wrongCallsToInput.push(name); - } - } - - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input(), got ${shape}`)); - } - }); - - descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { - const wrongType = []; - const notPrivate = []; - - const missingDependenciesDefault = []; - const wrongDependenciesType = []; - const wrongDefaultType = []; + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value === 'object') { - if (!('dependencies' in value && 'default' in value)) { - missingDependenciesDefault.push(name); + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); continue; } - if (!Array.isArray(value.dependencies)) { - wrongDependenciesType.push(name); + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); } - - if (typeof value.default !== 'function') { - wrongDefaultType.push(name); - } - - continue; } - if (typeof value !== 'string') { - wrongType.push(name); - continue; + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - if (!value.startsWith('#')) { - notPrivate.push(name); - continue; + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); } - } - - for (const name of wrongType) { - const type = typeof description.outputs[name]; - push(new Error(`${name}: Expected string, got ${type}`)); - } - - for (const name of notPrivate) { - const into = description.outputs[name]; - push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - - for (const name of missingDependenciesDefault) { - push(new Error(`${name}: Expected both dependencies & default`)); - } - - for (const name of wrongDependenciesType) { - const {dependencies} = description.outputs[name]; - push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); - } + }); + } - for (const name of wrongDefaultType) { - const type = typeof description.outputs[name].default; - push(new Error(`${name}: Expected default to be function, got ${type}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value !== 'object') continue; - - map( - description.outputs[name].dependencies, - decorateErrorWithIndex(dependency => { - if (!isInputToken(dependency)) { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); - } - - const shape = getInputTokenShape(dependency); - if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + if (Array.isArray(description.outputs)) { + descriptionAggregate.map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); } }), - {message: `${name}: Errors in dependencies`}); + {message: `Errors in output descriptions for ${compositeName}`}); } - }); + } descriptionAggregate.close(); @@ -772,66 +727,130 @@ export function compositeFrom(description) { ? base.flags.compose : true); - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } + // TODO: Check description.compose ?? true instead. + const compositionNests = baseComposes; const exposeDependencies = new Set(); + const updateDescription = {}; - let anyStepsCompute = false; - let anyStepsTransform = false; + // Steps default to exposing if using a shorthand syntax where flags aren't + // specified at all. + const stepsExpose = + steps + .map(step => + (step.flags + ? step.flags.expose ?? false + : true)); + + // Steps default to composing if using a shorthand syntax where flags aren't + // specified at all - *and* aren't the base (final step), unless the whole + // composition is nestable. + const stepsCompose = + steps + .map((step, index, {length}) => + (step.flags + ? step.flags.compose ?? false + : (index === length - 1 + ? compositionNests + : true))); + + // Steps don't update unless the corresponding flag is explicitly set. + const stepsUpdate = + steps + .map(step => + (step.flags + ? step.flags.update ?? false + : false)); + + // The expose description for a step is just the entire step object, when + // using the shorthand syntax where {flags: {expose: true}} is left implied. + const stepExposeDescriptions = + steps + .map((step, index) => + (stepsExpose[index] + ? (step.flags + ? step.expose ?? null + : step) + : null)); + + // The update description for a step, if present at all, is always set + // explicitly. + const stepUpdateDescriptions = + steps + .map((step, index) => + (stepsUpdate[index] + ? step.update ?? null + : null)); + + // Indicates presence of a {compute} function on the expose description. + const stepsCompute = + stepExposeDescriptions + .map(expose => !!expose?.compute); + + // Indicates presence of a {transform} function on the expose description. + const stepsTransform = + stepExposeDescriptions + .map(expose => !!expose?.transform); + + const anyStepsExpose = + stepsExpose.includes(true); + + const anyStepsUpdate = + stepsUpdate.includes(true); + + const anyStepsCompute = + stepsCompute.includes(true); + + const anyStepsTransform = + stepsTransform.includes(true); + + const stepEntries = stitchArrays({ + step: steps, + expose: stepExposeDescriptions, + update: stepUpdateDescriptions, + stepComposes: stepsCompose, + stepComputes: stepsCompute, + stepTransforms: stepsTransform, + }); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + for (let i = 0; i < stepEntries.length; i++) { + const { + step, + expose, + update, + stepComposes, + stepComputes, + stepTransforms, + } = stepEntries[i]; + + const isBase = i === stepEntries.length - 1; const message = `Errors in step #${i + 1}` + (isBase ? ` (base)` : ``) + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (step.flags) { - let flagsErrored = false; - - if (!step.flags.compose && !isBase) { - push(new TypeError(`All steps but base must compose`)); - flagsErrored = true; - } - - if (!step.flags.expose) { - push(new TypeError(`All steps must expose`)); - flagsErrored = true; - } - - if (flagsErrored) { - return; - } + if (isBase && stepComposes !== compositionNests) { + return push(new TypeError( + (compositionNests + ? `Base must compose, this composition is nestable` + : `Base must not compose, this composition isn't nestable`))); + } else if (!isBase && !stepComposes) { + return push(new TypeError( + (compositionNests + ? `All steps must compose` + : `All steps (except base) must compose`))); } - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose?.compute; - const stepTransforms = !!expose?.transform; - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes + !compositionNests && !anyStepsUpdate && + stepTransforms && !stepComputes ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - return; - } - - if (stepComputes) { - anyStepsCompute = true; - } - - if (stepTransforms) { - anyStepsTransform = true; + return push(new TypeError( + `Steps which only transform can't be used in a composition that doesn't update`)); } + /* // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties // on the CacheableObject. @@ -849,6 +868,7 @@ export function compositeFrom(description) { for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } + */ }); } @@ -1194,13 +1214,13 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, + update: anyStepsUpdate, + expose: anyStepsExpose, + compose: compositionNests, }; - if (baseUpdates) { - constructedDescriptor.update = base.update; + if (constructedDescriptor.update) { + constructedDescriptor.update = updateDescription; } if (baseExposes) { -- cgit 1.3.0-6-gf8a5 From 8db50e29b5a1cfddfddf499129b697ecabfadcb0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 13:01:25 -0300 Subject: data: moar WIP composite syntax! --- src/data/things/composite.js | 86 +++++++++++++++++++++++++------------------ src/data/things/thing.js | 69 +++++++++++++++++----------------- src/data/things/track.js | 73 +++++++++++++++++++----------------- src/data/things/validators.js | 4 +- 4 files changed, 129 insertions(+), 103 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98b04a7e..2e85374f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,7 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {oneOf} from '#validators'; +import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1193,7 +1193,7 @@ export function compositeFrom(description) { case 'raiseAbove': debug(() => colors.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); + return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { @@ -1360,9 +1360,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ mode: input(availabilityCheckModeInput), }, - outputs: { - into: '#availability', - }, + outputs: ['#availability'], steps: () => [ { @@ -1388,7 +1386,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ break; } - return continuation({into: availability}); + return continuation({'#availability': availability}); }, }, ], @@ -1571,35 +1569,56 @@ export const withPropertyFromObject = templateCompositeFrom({ property: input({type: 'string'}), }, - outputs: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => { - return ( - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value')); - }, + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return [ + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + ]; }, steps: () => [ { - dependencies: [input('object'), input('property')], + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => continuation({ + '#output': + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + }), + }, + + { + dependencies: [ + '#output', + input('object'), + input('property'), + ], + compute: (continuation, { + ['#output']: output, [input('object')]: object, [input('property')]: property, - }) => - (object === null - ? continuation({into: null}) - : continuation({into: object[property] ?? null})), + }) => continuation({ + [output]: + (object === null + ? null + : object[property] ?? null), + }), }, ], }); @@ -1780,14 +1799,11 @@ export const excludeFromList = templateCompositeFrom({ items: input({validate: isArray, null: true}), }, - outputs: { - dependencies: [input.staticDependency('list')], - compute: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - }, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - steps: [ + steps: () => [ { dependencies: [ input.staticDependency('list'), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a5f0b78d..cff2f498 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -476,15 +476,15 @@ export const withResolvedContribs = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedContribs', - }, + outputs: ['#resolvedContribs'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('from'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedContribs']: [], + }), }), withPropertiesFromList({ @@ -496,9 +496,10 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - into: '#contribs.who', find: input('find'), notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', }), { @@ -510,7 +511,7 @@ export const withResolvedContribs = templateCompositeFrom({ }) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - '#composition.into': stitchArrays({who, what}), + ['#resolvedContribs']: stitchArrays({who, what}), }); }, }, @@ -577,14 +578,14 @@ export const withResolvedReference = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReference', - }, + outputs: ['#resolvedReference'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('ref'), - output: input.value({into: null}), + output: input.value({ + ['#resolvedReference']: null, + }), }), exitWithoutDependency({ @@ -611,7 +612,9 @@ export const withResolvedReference = templateCompositeFrom({ return continuation.exit(null); } - return continuation.raise({match}); + return continuation.raiseOutput({ + ['#resolvedReference']: match, + }); }, }, ], @@ -640,9 +643,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReferenceList', - }, + outputs: ['#resolvedReferenceList'], steps: () => [ exitWithoutDependency({ @@ -653,7 +654,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ raiseOutputWithoutDependency({ dependency: input('list'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedReferenceList']: [], + }), }), { @@ -672,7 +675,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ dependencies: ['#matches'], compute: ({'#matches': matches}, continuation) => (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) : continuation()), }, @@ -687,12 +692,16 @@ export const withResolvedReferenceList = templateCompositeFrom({ return continuation.exit([]); case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); default: throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); @@ -714,30 +723,24 @@ export const withReverseReferenceList = templateCompositeFrom({ list: input({type: 'string'}), }, - outputs: { - into: '#reverseReferenceList', - }, + outputs: ['#reverseReferenceList'], steps: () => [ exitWithoutDependency({ - dependency: '#composition.data', + dependency: input('data'), value: [], }), { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], + dependencies: [input.myself(), input('data'), input('list')], compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, }, continuation) => continuation({ - '#composition.into': + ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 135e6d1f..37b36287 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -103,7 +103,7 @@ export class Track extends Thing { { dependencies: ['name', '#originalRelease.name'], - compute({name, '#originalRelease.name': originalName}) => + compute: ({name, '#originalRelease.name': originalName}) => name === originalName, }, ], @@ -389,34 +389,40 @@ export const withAlbum = templateCompositeFrom({ }), }, - outputs: { - into: '#album', - }, + outputs: ['#album'], steps: () => [ raiseOutputWithoutDependency({ dependency: 'albumData', mode: input.value('empty'), - output: input.value({into: null}), + output: input.value({ + ['#album']: null, + }), }), { - dependencies: ['this', 'albumData'], - compute: (continuation, {this: track, albumData}) => + dependencies: [input.myself(), 'albumData'], + compute: (continuation, { + [input.myself()]: track, + ['albumData']: albumData, + }) => continuation({ - '#album': albumData.find(album => album.tracks.includes(track)), + ['#album']: + albumData.find(album => album.tracks.includes(track)), }), }, raiseOutputWithoutDependency({ dependency: '#album', - output: input.value({into: null}), + output: input.value({ + ['#album']: null, + }), }), { dependencies: ['#album'], compute: (continuation, {'#album': album}) => - continuation({into: album}), + continuation.raiseOutput({'#album': album}), }, ], }); @@ -437,12 +443,9 @@ export const withPropertyFromAlbum = templateCompositeFrom({ }), }, - outputs: { - dependencies: [input.staticValue('property')], - compute: ({ - [input.staticValue('property')]: property, - }) => ['#album.' + property], - }, + outputs: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], steps: () => [ withAlbum({ @@ -479,9 +482,7 @@ export const withContainingTrackSection = templateCompositeFrom({ }), }, - outputs: { - into: '#trackSection', - }, + outputs: ['#trackSection'], steps: () => [ withPropertyFromAlbum({ @@ -502,18 +503,24 @@ export const withContainingTrackSection = templateCompositeFrom({ ['#album.trackSections']: trackSections, }) { if (!trackSections) { - return continuation({into: null}); + return continuation.raiseOutput({ + ['#trackSection']: null, + }); } const trackSection = trackSections.find(({tracks}) => tracks.includes(track)); if (trackSection) { - return continuation({into: trackSection}); + return continuation.raiseOutput({ + ['#trackSection']: trackSection, + }); } else if (notFoundMode === 'exit') { return continuation.exit(null); } else { - return continuation({into: null}); + return continuation.raiseOutput({ + ['#trackSection']: null, + }); } }, }, @@ -536,9 +543,7 @@ export const withOriginalRelease = templateCompositeFrom({ data: input({defaultDependency: 'trackData'}), }, - outputs: { - into: '#originalRelease', - }, + outputs: ['#originalRelease'], steps: () => [ withResolvedReference({ @@ -547,7 +552,7 @@ export const withOriginalRelease = templateCompositeFrom({ find: input.value(find.track), notFoundMode: input.value('exit'), }).outputs({ - '#resolvedReference': '#originalRelease', + ['#resolvedReference']: '#originalRelease', }), { @@ -563,7 +568,7 @@ export const withOriginalRelease = templateCompositeFrom({ ['#originalRelease']: originalRelease, }) => continuation({ - into: + ['#originalRelease']: (originalRelease ?? (selfIfOriginal ? track @@ -578,9 +583,7 @@ export const withOriginalRelease = templateCompositeFrom({ export const withHasUniqueCoverArt = templateCompositeFrom({ annotation: 'withHasUniqueCoverArt', - outputs: { - into: '#hasUniqueCoverArt', - }, + outputs: ['#hasUniqueCoverArt'], steps: () => [ { @@ -602,7 +605,10 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ }) => (empty(contribsFromTrack) ? continuation() - : continuation.raiseOutput({into: true})), + : continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + true, + })), }, withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), @@ -612,8 +618,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ compute: (continuation, { ['#album.trackCoverArtistContribs']: contribsFromAlbum, }) => - continuation({ - into: !empty(contribsFromAlbum), + continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + !empty(contribsFromAlbum), }), }, ], diff --git a/src/data/things/validators.js b/src/data/things/validators.js index f0d1d9fd..cd4c2b46 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -9,11 +9,11 @@ function inspect(value) { // Basic types (primitives) -function a(noun) { +export function a(noun) { return /[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`; } -function isType(value, type) { +export function isType(value, type) { if (typeof value !== type) throw new TypeError(`Expected ${a(type)}, got ${typeof value}`); -- cgit 1.3.0-6-gf8a5 From e0cec3ff368175341526ff1b3c849f82e377b286 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 17:33:27 -0300 Subject: data: work together validation internals --- src/data/things/composite.js | 70 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 13 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2e85374f..fbdc52f5 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,10 +5,11 @@ import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { + decorateErrorWithIndex, empty, filterProperties, openAggregate, - decorateErrorWithIndex, + stitchArrays, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -361,6 +362,8 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); + input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); @@ -400,6 +403,35 @@ function getInputTokenValue(token) { } } +function getStaticInputMetadata(inputOptions) { + const metadata = {}; + + for (const [name, token] of Object.entries(inputOptions)) { + if (typeof token === 'string') { + metadata[input.staticDependency(name)] = token; + metadata[input.staticValue(name)] = null; + } else if (isInputToken(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + + metadata[input.staticDependency(name)] = + (tokenShape === 'input.dependency' + ? tokenValue + : null); + + metadata[input.staticValue(name)] = + (tokenShape === 'input.value' + ? tokenValue + : null); + } else { + metadata[input.staticDependency(name)] = null; + metadata[input.staticValue(name)] = null; + } + } + + return metadata; +} + export function templateCompositeFrom(description) { const compositeName = (description.annotation @@ -483,11 +515,6 @@ export function templateCompositeFrom(description) { ? Object.keys(description.inputs) : []); - const expectedOutputNames = - (description.outputs - ? Object.keys(description.outputs) - : []); - const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); @@ -538,6 +565,13 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const expectedOutputNames = + (Array.isArray(description.outputs) + ? description.outputs + : typeof description.outputs === 'function' + ? description.outputs(getStaticInputMetadata(inputOptions)) + : []); + const outputOptions = {}; const instantiatedTemplate = { @@ -570,7 +604,7 @@ export function templateCompositeFrom(description) { } if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } for (const name of wrongTypeOutputNames) { @@ -703,6 +737,12 @@ export function compositeFrom(description) { } }; + if (!Array.isArray(composition)) { + throw new TypeError( + `Expected steps to be array, got ${typeof composition}` + + (annotation ? ` (${annotation})` : '')); + } + const base = composition.at(-1); const steps = composition.slice(); @@ -714,17 +754,17 @@ export function compositeFrom(description) { const baseExposes = (base.flags - ? base.flags.expose + ? base.flags.expose ?? false : true); const baseUpdates = (base.flags - ? base.flags.update + ? base.flags.update ?? false : false); const baseComposes = (base.flags - ? base.flags.compose + ? base.flags.compose ?? false : true); // TODO: Check description.compose ?? true instead. @@ -850,6 +890,12 @@ export function compositeFrom(description) { `Steps which only transform can't be used in a composition that doesn't update`)); } + if (update) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + Object.assign(updateDescription, update); + } + /* // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties @@ -1028,8 +1074,6 @@ export function compositeFrom(description) { return null; } } - - continue; } const callingTransformForThisStep = @@ -1821,7 +1865,7 @@ export const excludeFromList = templateCompositeFrom({ [listName ?? '#list']: listContents.filter(item => { if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && exclueItems.includes(item)) return false; + if (!empty(excludeItems) && excludeItems.includes(item)) return false; return true; }), }), -- cgit 1.3.0-6-gf8a5 From cc4bf401f4d1df63ce33191ae82af6327c7da568 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 17:33:50 -0300 Subject: data: fix many validation errors --- src/data/things/album.js | 70 +++++++++++++---------- src/data/things/artist.js | 5 +- src/data/things/composite.js | 113 +++++++++++++++++++++++-------------- src/data/things/flash.js | 9 +-- src/data/things/group.js | 9 +-- src/data/things/homepage-layout.js | 8 +-- src/data/things/index.js | 7 ++- src/data/things/thing.js | 8 +-- src/data/things/track.js | 52 ++++++++++------- src/data/things/wiki-info.js | 5 +- 10 files changed, 172 insertions(+), 114 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index ec133a34..44af5cbf 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -10,9 +10,9 @@ import { exposeUpdateValueOrContinue, input, fillMissingListItems, - withFlattenedArray, + withFlattenedList, withPropertiesFromList, - withUnflattenedArray, + withUnflattenedList, } from '#composite'; import Thing, { @@ -101,8 +101,15 @@ export class Album extends Thing { additionalFiles: additionalFiles(), trackSections: [ - exitWithoutDependency({dependency: 'trackData', value: []}), - exitWithoutUpdateValue({value: [], mode: 'empty'}), + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutUpdateValue({ + mode: input.value('empty'), + value: input.value([]), + }), withPropertiesFromList({ list: input.updateValue(), @@ -119,32 +126,27 @@ export class Album extends Thing { fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), fillMissingListItems({list: '#sections.color', dependency: 'color'}), - withFlattenedArray({ - from: '#sections.tracks', - into: '#trackRefs', - intoIndices: '#sections.startIndex', + withFlattenedList({ + list: '#sections.tracks', + }).outputs({ + ['#flattenedList']: '#trackRefs', + ['#flattenedIndices']: '#sections.startIndex', }), - { - dependencies: ['#trackRefs'], - compute: ({'#trackRefs': tracks}, continuation) => { - console.log(tracks); - return continuation(); - } - }, - withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - notFoundMode: 'null', - find: find.track, - into: '#tracks', + notFoundMode: input.value('null'), + find: input.value(find.track), + }).outputs({ + ['#resolvedReferenceList']: '#tracks', }), - withUnflattenedArray({ - from: '#tracks', - fromIndices: '#sections.startIndex', - into: '#sections.tracks', + withUnflattenedList({ + list: '#tracks', + indices: '#sections.startIndex', + }).outputs({ + ['#unflattenedList']: '#sections.tracks', }), { @@ -191,14 +193,14 @@ export class Album extends Thing { bannerArtistContribs: contributionList(), groups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), artTags: referenceList({ - class: ArtTag, - find: find.artTag, + class: input.value(ArtTag), + find: input.value(find.artTag), data: 'artTagData', }), @@ -218,8 +220,16 @@ export class Album extends Thing { hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: [ - exitWithoutDependency({dependency: 'trackData', value: []}), - exitWithoutDependency({dependency: 'trackSections', mode: 'empty', value: []}), + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutDependency({ + dependency: 'trackSections', + mode: input.value('empty'), + value: input.value([]), + }), { dependencies: ['trackSections'], @@ -233,7 +243,7 @@ export class Album extends Thing { withResolvedReferenceList({ list: '#trackRefs', data: 'trackData', - find: find.track, + find: input.value(find.track), }), exposeDependency({dependency: '#resolvedReferenceList'}), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 7a9dbd3c..085e5663 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import {isName, validateArrayItems} from '#validators'; @@ -35,8 +36,8 @@ export class Artist extends Thing { isAlias: flag(), aliasedArtist: singleReference({ - class: Artist, - find: find.artist, + class: input.value(Artist), + find: input.value(find.artist), data: 'artistData', }), diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fbdc52f5..83879c54 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,9 +1,15 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; +import { + isArray, + isWholeNumber, + oneOf, + validateArrayItems, +} from '#validators'; + import { decorateErrorWithIndex, empty, @@ -1876,72 +1882,93 @@ export const excludeFromList = templateCompositeFrom({ // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. -export function withFlattenedArray({ - from, - into = '#flattenedArray', - intoIndices = '#flattenedIndices', -}) { - return { - annotation: `withFlattenedArray`, - flags: {expose: true, compose: true}, +export const withFlattenedList = templateCompositeFrom({ + annotation: `withFlattenedList`, - expose: { - mapDependencies: {from}, - mapContinuation: {into, intoIndices}, + inputs: { + list: input({type: 'array'}), + }, - compute({from: sourceArray}, continuation) { - const into = sourceArray.flat(); - const intoIndices = []; + outputs: ['#flattenedList', '#flattenedIndices'], + steps: () => [ + { + dependencies: [input('list')], + compute(continuation, { + [input('list')]: sourceList, + }) { + const flattenedList = sourceList.flat(); + const indices = []; let lastEndIndex = 0; for (const {length} of sourceArray) { - intoIndices.push(lastEndIndex); + indices.push(lastEndIndex); lastEndIndex += length; } - return continuation({into, intoIndices}); + return continuation({ + ['#flattenedList']: flattenedList, + ['#flattenedIndices']: indices, + }); }, }, - }; -} + ], +}); // After mapping the contents of a flattened array in-place (being careful to // retain the original indices by replacing unmatched results with null instead // of filtering them out), this function allows for recombining them. It will // filter out null and undefined items by default (pass {filter: false} to // disable this). -export function withUnflattenedArray({ - from, - fromIndices = '#flattenedIndices', - into = '#unflattenedArray', - filter = true, -}) { - return { - annotation: `withUnflattenedArray`, - flags: {expose: true, compose: true}, +export const withUnflattenedList = templateCompositeFrom({ + annotation: `withUnflattenedList`, - expose: { - mapDependencies: {from, fromIndices}, - mapContinuation: {into}, - compute({from, fromIndices}, continuation) { - const arrays = []; + inputs: { + list: input({ + type: 'array', + defaultDependency: '#flattenedList', + }), + + indices: input({ + validate: validateArrayItems(isWholeNumber), + defaultDependency: '#flattenedIndices', + }), + + filter: input({ + type: 'boolean', + defaultValue: true, + }), + }, - for (let i = 0; i < fromIndices.length; i++) { - const startIndex = fromIndices[i]; + outputs: ['#unflattenedList'], + + steps: () => [ + { + dependencies: [input('list'), input('indices')], + compute({ + [input('list')]: list, + [input('indices')]: indices, + [input('filter')]: filter, + }) { + const unflattenedList = []; + + for (let i = 0; i < indices.length; i++) { + const startIndex = indices[i]; const endIndex = - (i === fromIndices.length - 1 - ? from.length - : fromIndices[i + 1]); + (i === indices.length - 1 + ? list.length + : indices[i + 1]); - const values = from.slice(startIndex, endIndex); - arrays.push( + const values = list.slice(startIndex, endIndex); + unflattenedList.push( (filter ? values.filter(value => value !== null && value !== undefined) : values)); } - return continuation({into: arrays}); + return continuation({ + ['#unflattenedList']: unflattenedList, + }); }, }, - }; -} + ], +}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index eb16d29e..c3f90260 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import { @@ -61,8 +62,8 @@ export class Flash extends Thing { contributorContribs: contributionList(), featuredTracks: referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), @@ -133,9 +134,9 @@ export class FlashAct extends Thing { }, flashes: referenceList({ - class: Flash, + class: input.value(Flash), + find: input.value(find.flash), data: 'flashData', - find: find.flash, }), // Update only diff --git a/src/data/things/group.js b/src/data/things/group.js index f53fa48e..0b117801 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import Thing, { @@ -24,8 +25,8 @@ export class Group extends Thing { urls: urls(), featuredAlbums: referenceList({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), @@ -87,8 +88,8 @@ export class GroupCategory extends Thing { color: color(), groups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 677a2756..bade280c 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -107,7 +107,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [ + sourceGroup: [ { flags: {expose: true, update: true, compose: true}, @@ -133,11 +133,11 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }), exposeDependency({dependency: '#resolvedReference'}), - ]), + ], sourceAlbums: referenceList({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), diff --git a/src/data/things/index.js b/src/data/things/index.js index 4d8d9d1f..f908653d 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -135,7 +135,12 @@ function evaluatePropertyDescriptors() { for (const [key, value] of Object.entries(results)) { if (Array.isArray(value)) { - results[key] = compositeFrom(`${constructor.name}.${key}`, value); + results[key] = compositeFrom({ + annotation: `${constructor.name}.${key}`, + compose: false, + steps: value, + }); + continue; } } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cff2f498..a75ff3e1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -211,7 +211,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: () => [ + steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), @@ -468,8 +468,6 @@ export const withResolvedContribs = templateCompositeFrom({ // todo: validate from: input(), - findFunction: input({type: 'function'}), - notFoundMode: input({ validate: oneOf('exit', 'filter', 'null'), defaultValue: 'null', @@ -496,7 +494,7 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - find: input('find'), + find: input.value(find.artist), notFoundMode: input('notFoundMode'), }).outputs({ ['#resolvedReferenceList']: '#contribs.who', @@ -728,7 +726,7 @@ export const withReverseReferenceList = templateCompositeFrom({ steps: () => [ exitWithoutDependency({ dependency: input('data'), - value: [], + value: input.value([]), }), { diff --git a/src/data/things/track.js b/src/data/things/track.js index 37b36287..05b762b9 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -6,6 +6,7 @@ import {empty} from '#sugar'; import { exitWithoutDependency, + excludeFromList, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -17,6 +18,7 @@ import { } from '#composite'; import { + isBoolean, isColor, isContributionList, isDate, @@ -136,7 +138,11 @@ export class Track extends Thing { // is specified, this value is null. coverArtDate: [ withHasUniqueCoverArt(), - exitWithoutDependency({dependency: '#hasUniqueCoverArt', mode: 'falsy'}), + + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + }), exposeUpdateValueOrContinue({ validate: input.value(isDate), @@ -154,8 +160,8 @@ export class Track extends Thing { midiProjectFiles: additionalFiles(), originalReleaseTrack: singleReference({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), @@ -163,8 +169,8 @@ export class Track extends Thing { // util.inspect display, if it isn't indirectly available (by way of being // included in an album's track list). dataSourceAlbum: singleReference({ - class: Album, - find: find.album, + class: input.value(Album), + find: input.value(find.album), data: 'albumData', }), @@ -208,25 +214,27 @@ export class Track extends Thing { referencedTracks: [ inheritFromOriginalRelease({property: 'referencedTracks'}), + referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), ], sampledTracks: [ inheritFromOriginalRelease({property: 'sampledTracks'}), + referenceList({ - class: Track, - find: find.track, + class: input.value(Track), + find: input.value(find.track), data: 'trackData', }), ], artTags: referenceList({ - class: ArtTag, - find: find.artTag, + class: input.value(ArtTag), + find: input.value(find.artTag), data: 'artTagData', }), @@ -266,8 +274,14 @@ export class Track extends Thing { ], otherReleases: [ - exitWithoutDependency({dependency: 'trackData', mode: 'empty'}), - withOriginalRelease({selfIfOriginal: true}), + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + }), + + withOriginalRelease({ + selfIfOriginal: input.value(true), + }), { flags: {expose: true}, @@ -594,14 +608,14 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ : continuation()), }, - withResolvedContribs - .inputs({from: 'coverArtistContribs'}) - .outputs({into: '#coverArtistContribs'}), + withResolvedContribs({ + from: 'coverArtistContribs', + }), { - dependencies: ['#coverArtistContribs'], + dependencies: ['#resolvedContribs'], compute: (continuation, { - ['#coverArtistContribs']: contribsFromTrack, + ['#resolvedContribs']: contribsFromTrack, }) => (empty(contribsFromTrack) ? continuation() @@ -640,7 +654,7 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ exitWithoutDependency({ dependency: '#hasUniqueCoverArt', - mode: 'falsy', + mode: input.value('falsy'), value: input('value'), }), ], diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 7c2de324..c764b528 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,3 +1,4 @@ +import {input} from '#composite'; import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; @@ -45,8 +46,8 @@ export class WikiInfo extends Thing { }, divideTrackListsByGroups: referenceList({ - class: Group, - find: find.group, + class: input.value(Group), + find: input.value(find.group), data: 'groupData', }), -- cgit 1.3.0-6-gf8a5 From a2704c0992beb4ddfeb67813d4f8adac0ae6af7d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 18:30:48 -0300 Subject: data: syntax fixes --- src/data/things/album.js | 2 +- src/data/things/art-tag.js | 11 ++++------- src/data/things/composite.js | 10 +++++----- src/data/things/thing.js | 20 +++++++++++--------- 4 files changed, 21 insertions(+), 22 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 44af5cbf..20a1a5b3 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -233,7 +233,7 @@ export class Album extends Thing { { dependencies: ['trackSections'], - compute: ({trackSections}, continuation) => + compute: (continuation, {trackSections}) => continuation({ '#trackRefs': trackSections .flatMap(section => section.tracks ?? []), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 7e466555..ba3cbd0d 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,4 +1,4 @@ -import {exposeUpdateValueOrContinue} from '#composite'; +import {exposeUpdateValueOrContinue, input} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; import {isName} from '#validators'; @@ -22,18 +22,15 @@ export class ArtTag extends Thing { isContentWarning: flag(false), nameShort: [ - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isName), + }), { dependencies: ['name'], compute: ({name}) => name.replace(/ \([^)]*?\)$/, ''), }, - - { - flags: {update: true, expose: true}, - validate: {isName}, - }, ], // Update only diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 83879c54..e2dbc70b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1693,7 +1693,7 @@ export function withPropertiesFromObject({ mapDependencies: {object}, options: {prefix, properties}, - compute: ({object, '#options': {prefix, properties}}, continuation) => + compute: (continuation, {object, '#options': {prefix, properties}}) => continuation( Object.fromEntries( properties.map(property => [ @@ -1729,7 +1729,7 @@ export function withPropertyFromList({ mapContinuation: {into}, options: {property}, - compute({list, '#options': {property}}, continuation) { + compute(continuation, {list, '#options': {property}}) { if (list === undefined || empty(list)) { return continuation({into: []}); } @@ -1765,7 +1765,7 @@ export function withPropertiesFromList({ mapDependencies: {list}, options: {prefix, properties}, - compute({list, '#options': {prefix, properties}}, continuation) { + compute(continuation, {list, '#options': {prefix, properties}}) { const lists = Object.fromEntries( properties.map(property => [`${prefix}.${property}`, []])); @@ -1811,7 +1811,7 @@ export function fillMissingListItems({ mapDependencies: {list, dependency}, mapContinuation: {into}, - compute: ({list, dependency}, continuation) => + compute: (continuation, {list, dependency}) => continuation({ into: list.map(item => item ?? dependency), }), @@ -1827,7 +1827,7 @@ export function fillMissingListItems({ mapContinuation: {into}, options: {value}, - compute: ({list, '#options': {value}}, continuation) => + compute: (continuation, {list, '#options': {value}}) => continuation({ into: list.map(item => item ?? value), }), diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a75ff3e1..265cfe18 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -209,6 +209,8 @@ export function contributionList() { return compositeFrom({ annotation: `contributionList`, + compose: false, + update: {validate: isContributionList}, steps: [ @@ -598,12 +600,12 @@ export const withResolvedReference = templateCompositeFrom({ input('notFoundMode'), ], - compute({ + compute(continuation, { [input('ref')]: ref, [input('data')]: data, [input('find')]: findFunction, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && notFoundMode === 'exit') { @@ -659,11 +661,11 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: [input('list'), input('data'), input('find')], - compute: ({ + compute: (continuation, { [input('list')]: list, [input('data')]: data, [input('find')]: findFunction, - }, continuation) => + }) => continuation({ '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), }), @@ -671,7 +673,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => + compute: (continuation, {'#matches': matches}) => (matches.every(match => match) ? continuation.raiseOutput({ ['#resolvedReferenceList']: matches, @@ -681,10 +683,10 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches', input('notFoundMode')], - compute({ + compute(continuation, { ['#matches']: matches, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { switch (notFoundMode) { case 'exit': return continuation.exit([]); @@ -732,11 +734,11 @@ export const withReverseReferenceList = templateCompositeFrom({ { dependencies: [input.myself(), input('data'), input('list')], - compute: ({ + compute: (continuation, { [input.myself()]: thisThing, [input('data')]: data, [input('list')]: refListProperty, - }, continuation) => + }) => continuation({ ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), -- cgit 1.3.0-6-gf8a5 From 66544e6730bd79c9cb1c50d89421f9a08329e27d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 18:31:30 -0300 Subject: data: make composite work --- src/data/things/composite.js | 195 +++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 74 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e2dbc70b..aa383db9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + unique, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -638,6 +639,10 @@ export function templateCompositeFrom(description) { finalDescription.annotation = description.annotation; } + if ('compose' in description) { + finalDescription.compose = description.compose; + } + if ('update' in description) { finalDescription.update = description.update; } @@ -700,7 +705,7 @@ export function templateCompositeFrom(description) { steps, decorateErrorWithIndex(step => (step.symbol === templateCompositeFrom.symbol - ? step.toResolvedComposition() + ? compositeFrom(step.toResolvedComposition()) : step)), {message: `Errors resolving steps`}); @@ -723,7 +728,7 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); export function compositeFrom(description) { - const {annotation, steps: composition} = description; + const {annotation} = description; const debug = fn => { if (compositeFrom.debug === true) { @@ -743,12 +748,37 @@ export function compositeFrom(description) { } }; - if (!Array.isArray(composition)) { + if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof composition}` + + `Expected steps to be array, got ${typeof description.steps}` + (annotation ? ` (${annotation})` : '')); } + const composition = + description.steps.map(step => + ('toResolvedComposition' in step + ? compositeFrom(step.toResolvedComposition()) + : step)); + + const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + + // These dependencies were all provided by the composition which this one is + // nested inside, so input('name')-shaped tokens are going to be evaluated + // in the context of the containing composition. + const dependenciesFromInputs = + Object.values(description.inputs ?? {}) + .map(token => { + switch (getInputTokenShape(token)) { + case 'input.dependency': + return getInputTokenValue(token); + case 'input': + return token; + default: + return null; + } + }) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -758,23 +788,8 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - const baseExposes = - (base.flags - ? base.flags.expose ?? false - : true); - - const baseUpdates = - (base.flags - ? base.flags.update ?? false - : false); - - const baseComposes = - (base.flags - ? base.flags.compose ?? false - : true); - // TODO: Check description.compose ?? true instead. - const compositionNests = baseComposes; + const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); const updateDescription = {}; @@ -838,6 +853,18 @@ export function compositeFrom(description) { stepExposeDescriptions .map(expose => !!expose?.transform); + const dependenciesFromSteps = + unique( + stepExposeDescriptions + .flatMap(expose => expose?.dependencies ?? []) + .map(dependency => + (typeof dependency === 'string' + ? dependency + : getInputTokenShape(dependency) === 'input.dependency' + ? getInputTokenValue(dependency) + : null)) + .filter(Boolean)); + const anyStepsExpose = stepsExpose.includes(true); @@ -924,39 +951,12 @@ export function compositeFrom(description) { }); } - if (!baseComposes && !baseUpdates && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); + if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute or update`)); } aggregate.close(); - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - if (!dependencies && !mapDependencies && !options) { - return null; - } - - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [into, from] of Object.entries(mapDependencies)) { - filteredDependencies[into] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - function _assignDependencies(continuationAssignment, {mapContinuation}) { if (!mapContinuation) { return continuationAssignment; @@ -998,7 +998,7 @@ export function compositeFrom(description) { return continuationSymbol; }; - if (baseComposes) { + if (compositionNests) { const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { @@ -1033,6 +1033,31 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; + // console.log('input description:', description.inputs); + const inputValues = + ('inputs' in description + ? Object.fromEntries(Object.entries(description.inputs) + .map(([name, token]) => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return [input(name), initialDependencies[tokenValue]]; + case 'input.value': + return [input(name), tokenValue]; + case 'input.updateValue': + return [input(name), initialValue]; + case 'myself': + return [input(name), myself]; + case 'input': + return [input(name), initialDependencies[token]]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + })) + : {}); + // console.log('input values:', inputValues); + if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { @@ -1087,7 +1112,12 @@ export function compositeFrom(description) { let continuationStorage; - const filteredDependencies = _filterDependencies(availableDependencies, expose); + const filteredDependencies = + filterProperties({ + ...availableDependencies, + ...inputMetadata, + ...inputValues, + }, expose.dependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, @@ -1106,9 +1136,17 @@ export function compositeFrom(description) { const naturalEvaluate = () => { const [name, ...args] = getExpectedEvaluation(); - let continuation; - ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](...args, continuation); + + if (isBase && !compositionNests) { + return expose[name](...args); + } else { + let continuation; + + ({continuation, continuationStorage} = + _prepareContinuation(callingTransformForThisStep)); + + return expose[name](continuation, ...args); + } } switch (step.cache) { @@ -1166,7 +1204,7 @@ export function compositeFrom(description) { if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - if (baseComposes) { + if (compositionNests) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } @@ -1183,7 +1221,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (baseComposes) { + if (composiitonNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1273,26 +1311,35 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (baseExposes) { + if (anyStepsExpose) { const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const transformFn = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); - - const computeFn = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - - if (baseComposes) { - if (anyStepsTransform) expose.transform = transformFn; - if (anyStepsCompute) expose.compute = computeFn; - if (base.cacheComposition) expose.cache = base.cacheComposition; - } else if (baseUpdates) { - expose.transform = transformFn; + + expose.dependencies = + unique([ + ...dependenciesFromInputs, + ...dependenciesFromSteps, + ]); + + if (compositionNests) { + if (anyStepsTransform) { + expose.transform = (value, continuation, dependencies) => + _computeOrTransform(value, dependencies, continuation); + } + + if (anyStepsCompute) { + expose.compute = (continuation, dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, continuation); + } + + if (base.cacheComposition) { + expose.cache = base.cacheComposition; + } + } else if (anyStepsUpdate) { + expose.transform = (value, dependencies) => + _computeOrTransform(value, dependencies, null); } else { - expose.compute = computeFn; + expose.compute = (dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, null); } } -- cgit 1.3.0-6-gf8a5 From 572b5465f9ce1e992e0384aa92461ec11dbaabff Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 11:04:33 -0300 Subject: data: make composites work --- src/data/things/composite.js | 232 ++++++++++++++++++++++++------------------- src/data/things/index.js | 4 +- 2 files changed, 130 insertions(+), 106 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index aa383db9..cbbe6f8f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -651,7 +651,10 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - const description = description_; + // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes + const description = getInputTokenValue(description_); + const tokenShape = getInputTokenShape(description_); + if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { finalInputs[name] = input.dependency(inputOptions[name]); @@ -659,9 +662,9 @@ export function templateCompositeFrom(description) { finalInputs[name] = inputOptions[name]; } } else if (description.defaultValue) { - finalInputs[name] = input.value(defaultValue); + finalInputs[name] = input.value(description.defaultValue); } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(defaultValue); + finalInputs[name] = input.dependency(description.defaultDependency); } else { finalInputs[name] = input.value(null); } @@ -673,11 +676,11 @@ export function templateCompositeFrom(description) { if ('outputs' in description) { const finalOutputs = {}; - for (const [name, defaultDependency] of Object.entries(description.outputs)) { + for (const name of expectedOutputNames) { if (name in outputOptions) { finalOutputs[name] = outputOptions[name]; } else { - finalOutputs[name] = defaultDependency; + finalOutputs[name] = name; } } @@ -740,7 +743,7 @@ export function compositeFrom(description) { if (Array.isArray(result)) { console.log(label, ...result.map(value => (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + ? inspect(value, {depth: 1, colors: true, compact: true, breakLength: Infinity}) : value))); } else { console.log(label, result); @@ -762,16 +765,37 @@ export function compositeFrom(description) { const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + function _mapDependenciesToOutputs(providedDependencies) { + if (!description.outputs) { + return {}; + } + + if (!providedDependencies) { + return {}; + } + + return ( + Object.fromEntries( + Object.entries(description.outputs) + .map(([continuationName, outputName]) => [ + outputName, + providedDependencies[continuationName], + ]))); + } + // These dependencies were all provided by the composition which this one is // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = Object.values(description.inputs ?? {}) .map(token => { - switch (getInputTokenShape(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { case 'input.dependency': - return getInputTokenValue(token); + return tokenValue; case 'input': + case 'input.updateValue': return token; default: return null; @@ -779,6 +803,9 @@ export function compositeFrom(description) { }) .filter(Boolean); + const anyInputsUseUpdateValue = + dependenciesFromInputs.includes(input.updateValue()); + const base = composition.at(-1); const steps = composition.slice(); @@ -857,14 +884,30 @@ export function compositeFrom(description) { unique( stepExposeDescriptions .flatMap(expose => expose?.dependencies ?? []) - .map(dependency => - (typeof dependency === 'string' - ? dependency - : getInputTokenShape(dependency) === 'input.dependency' - ? getInputTokenValue(dependency) - : null)) + .map(dependency => { + if (typeof dependency === 'string') + return (dependency.startsWith('#') ? null : dependency); + + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input.dependency': + return (tokenValue.startsWith('#') ? null : tokenValue); + case 'input.myself': + return 'this'; + default: + return null; + } + }) .filter(Boolean)); + const anyStepsUseUpdateValue = + stepExposeDescriptions + .some(expose => + (expose?.dependencies + ? expose.dependencies.includes(input.updateValue()) + : false)); + const anyStepsExpose = stepsExpose.includes(true); @@ -877,6 +920,14 @@ export function compositeFrom(description) { const anyStepsTransform = stepsTransform.includes(true); + const compositionExposes = + anyStepsExpose; + + const compositionUpdates = + anyInputsUseUpdateValue || + anyStepsUseUpdateValue || + anyStepsUpdate; + const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, @@ -916,7 +967,7 @@ export function compositeFrom(description) { } if ( - !compositionNests && !anyStepsUpdate && + !compositionNests && !compositionUpdates && stepTransforms && !stepComputes ) { return push(new TypeError( @@ -928,26 +979,6 @@ export function compositeFrom(description) { // interesting things, like combining validation functions. Object.assign(updateDescription, update); } - - /* - // Unmapped dependencies are exposed on the final composition only if - // they're "public", i.e. pointing to update values of other properties - // on the CacheableObject. - for (const dependency of expose?.dependencies ?? []) { - if (typeof dependency === 'string' && dependency.startsWith('#')) { - continue; - } - - exposeDependencies.add(dependency); - } - - // Mapped dependencies are always exposed on the final composition. - // These are explicitly for reading values which are named outside of - // the current compositional step. - for (const dependency of Object.values(expose?.mapDependencies ?? {})) { - exposeDependencies.add(dependency); - } - */ }); } @@ -957,20 +988,6 @@ export function compositeFrom(description) { aggregate.close(); - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, into] of Object.entries(mapContinuation)) { - assignDependencies[into] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, @@ -1013,8 +1030,8 @@ export function compositeFrom(description) { return continuationSymbol; }); - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); + continuation.raiseOutput = makeRaiseLike('raiseOutput'); + continuation.raiseOutputAbove = makeRaiseLike('raiseOutputAbove'); } return {continuation, continuationStorage}; @@ -1023,7 +1040,7 @@ export function compositeFrom(description) { const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; let valueSoFar = @@ -1033,7 +1050,6 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; - // console.log('input description:', description.inputs); const inputValues = ('inputs' in description ? Object.fromEntries(Object.entries(description.inputs) @@ -1046,9 +1062,12 @@ export function compositeFrom(description) { case 'input.value': return [input(name), tokenValue]; case 'input.updateValue': - return [input(name), initialValue]; - case 'myself': - return [input(name), myself]; + if (!expectingTransform) { + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + } + return [input(name), valueSoFar]; + case 'input.myself': + return [input(name), initialDependencies['this']]; case 'input': return [input(name), initialDependencies[token]]; default: @@ -1056,7 +1075,6 @@ export function compositeFrom(description) { } })) : {}); - // console.log('input values:', inputValues); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1117,36 +1135,51 @@ export function compositeFrom(description) { ...availableDependencies, ...inputMetadata, ...inputValues, - }, expose.dependencies); + ... + (callingTransformForThisStep + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies['this'], + }, expose.dependencies ?? []); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + `with dependencies:`, filteredDependencies, + ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? ['transform', valueSoFar, filteredDependencies] - : ['transform', valueSoFar]) + ? ['transform', valueSoFar, continuationSymbol, filteredDependencies] + : ['transform', valueSoFar, continuationSymbol]) : (filteredDependencies - ? ['compute', filteredDependencies] - : ['compute'])); + ? ['compute', continuationSymbol, filteredDependencies] + : ['compute', continuationSymbol])); const naturalEvaluate = () => { - const [name, ...args] = getExpectedEvaluation(); + const [name, ...argsLayout] = getExpectedEvaluation(); + + let args; if (isBase && !compositionNests) { - return expose[name](...args); + args = + argsLayout.filter(arg => arg !== continuationSymbol); } else { let continuation; ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](continuation, ...args); + args = + argsLayout.map(arg => + (arg === continuationSymbol + ? continuation + : arg)); } + + return expose[name](...args); } switch (step.cache) { @@ -1221,7 +1254,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (composiitonNests) { + if (compositionNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1230,36 +1263,24 @@ export function compositeFrom(description) { const {providedValue, providedDependencies} = continuationStorage; - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); + if (expectingTransform) { + continuationArgs.push( + (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null)); + } debug(() => { const base = `step #${i+1} - result: ` + returnedWith; const parts = []; if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } + parts.push('value:', providedValue); } - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); + if (providedDependencies !== null) { + parts.push(`deps:`, providedDependencies); } else { parts.push(`(no deps)`); } @@ -1272,23 +1293,26 @@ export function compositeFrom(description) { }); switch (returnedWith) { - case 'raise': + case 'raiseOutput': debug(() => (isBase - ? colors.bright(`end composition - raise (base: explicit)`) - : colors.bright(`end composition - raise`))); + ? colors.bright(`end composition - raiseOutput (base: explicit)`) + : colors.bright(`end composition - raiseOutput`))); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); - case 'raiseAbove': - debug(() => colors.bright(`end composition - raiseAbove`)); + case 'raiseOutputAbove': + debug(() => colors.bright(`end composition - raiseOutputAbove`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { - debug(() => colors.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raiseOutput (inferred)`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); } else { - Object.assign(availableDependencies, continuingWithDependencies); + Object.assign(availableDependencies, providedDependencies); break; } } @@ -1302,8 +1326,8 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: anyStepsUpdate, - expose: anyStepsExpose, + update: compositionUpdates, + expose: compositionExposes, compose: compositionNests, }; @@ -1311,7 +1335,7 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (anyStepsExpose) { + if (compositionExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = @@ -1321,25 +1345,25 @@ export function compositeFrom(description) { ]); if (compositionNests) { - if (anyStepsTransform) { + if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, dependencies, continuation); + _computeOrTransform(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, continuation); + _computeOrTransform(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { expose.cache = base.cacheComposition; } - } else if (anyStepsUpdate) { + } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, dependencies, null); + _computeOrTransform(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, null); + _computeOrTransform(noTransformSymbol, null, dependencies); } } diff --git a/src/data/things/index.js b/src/data/things/index.js index f908653d..77e5fa76 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -140,8 +140,8 @@ function evaluatePropertyDescriptors() { compose: false, steps: value, }); - - continue; + } else if (value.toResolvedComposition) { + results[key] = compositeFrom(value.toResolvedComposition()); } } -- cgit 1.3.0-6-gf8a5 From cd73f85962f542f9b44feb2a7616bc0d9aac797b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 11:05:00 -0300 Subject: data: miscellaneous utility updates --- src/data/things/composite.js | 142 ++++++++++++++++++++++++++++++------------- src/data/things/thing.js | 2 +- src/data/things/track.js | 53 ++++++++++++---- 3 files changed, 141 insertions(+), 56 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cbbe6f8f..cfa557de 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,6 +5,7 @@ import {TupleMap} from '#wiki-data'; import { isArray, + isString, isWholeNumber, oneOf, validateArrayItems, @@ -1426,17 +1427,24 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency({dependency}) { - return { - annotation: `exposeDependency`, - flags: {expose: true}, +export const exposeDependency = templateCompositeFrom({ + annotation: `exposeDependency`, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, + compose: false, + + inputs: { + dependency: input.staticDependency(), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, }, - }; -} + ], +}); // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1488,7 +1496,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ dependencies: [input('from'), input('mode')], compute: (continuation, { - [input('from')]: dependency, + [input('from')]: value, [input('mode')]: mode, }) => { let availability; @@ -1591,7 +1599,7 @@ export const exitWithoutDependency = templateCompositeFrom({ { dependencies: ['#availability', input('value')], - continuation: (continuation, { + compute: (continuation, { ['#availability']: availability, [input('value')]: value, }) => @@ -1628,9 +1636,13 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), @@ -1657,9 +1669,13 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ inputs: { mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), @@ -1820,41 +1836,81 @@ export function withPropertyFromList({ // Gets the listed properties from each of a list of objects, providing lists // of property values each into a dependency prefixed with the same name as the // list (by default). Like withPropertyFromList, this doesn't alter indices. -export function withPropertiesFromList({ - list, - properties, - prefix = - (list.startsWith('#') - ? list - : `#${list}`), -}) { - return { - annotation: `withPropertiesFromList`, - flags: {expose: true, compose: true}, +export const withPropertiesFromList = templateCompositeFrom({ + annotation: `withPropertiesFromList`, - expose: { - mapDependencies: {list}, - options: {prefix, properties}, + inputs: { + list: input({type: 'array'}), + + properties: input({ + validate: validateArrayItems(isString), + }), - compute(continuation, {list, '#options': {prefix, properties}}) { - const lists = + prefix: input({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`)) + : '#lists'), + + steps: () => [ + { + dependencies: [input('list'), input('properties')], + compute: (continuation, { + [input('list')]: list, + [input('properties')]: properties, + }) => continuation({ + ['#lists']: Object.fromEntries( - properties.map(property => [`${prefix}.${property}`, []])); + properties.map(property => [ + property, + list.map(item => item[property] ?? null), + ])), + }), + }, - for (const item of list) { - for (const property of properties) { - lists[`${prefix}.${property}`].push( - (item === null || item === undefined - ? null - : item[property] ?? null)); - } - } + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#lists', + ], - return continuation(lists); - } - } - } -} + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#lists']: lists, + }) => + (properties + ? continuation( + Object.fromEntries( + properties.map(property => [ + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`), + lists[property], + ]))) + : continuation({'#lists': lists})), + }, + ], +}); // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 265cfe18..f63a619d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -363,7 +363,7 @@ export const contribsPresent = templateCompositeFrom({ steps: () => [ withResultOfAvailabilityCheck({ - fromDependency: input('contribs'), + from: input('contribs'), mode: input.value('empty'), }), diff --git a/src/data/things/track.js b/src/data/things/track.js index 05b762b9..f31fe3ae 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -68,10 +68,18 @@ export class Track extends Thing { }), withContainingTrackSection(), - withPropertyFromObject({object: '#trackSection', property: 'color'}), + + withPropertyFromObject({ + object: '#trackSection', + property: input.value('color'), + }), + exposeDependencyOrContinue({dependency: '#trackSection.color'}), - withPropertyFromAlbum({property: 'color'}), + withPropertyFromAlbum({ + property: input.value('color'), + }), + exposeDependency({dependency: '#album.color'}), ], @@ -127,9 +135,15 @@ export class Track extends Thing { validate: input.value(isFileExtension), }), - withPropertyFromAlbum({property: 'trackCoverArtFileExtension'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtFileExtension'), + }), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - exposeConstant({value: 'jpg'}), + + exposeConstant({ + value: input.value('jpg'), + }), ], // Date of cover art release. Like coverArtFileExtension, this represents @@ -148,7 +162,10 @@ export class Track extends Thing { validate: input.value(isDate), }), - withPropertyFromAlbum({property: 'trackArtDate'}), + withPropertyFromAlbum({ + property: input.value('trackArtDate'), + }), + exposeDependency({dependency: '#album.trackArtDate'}), ], @@ -185,7 +202,10 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#artistContribs'}), - withPropertyFromAlbum({property: 'artistContribs'}), + withPropertyFromAlbum({ + property: input.value('artistContribs'), + }), + exposeDependency({dependency: '#album.artistContribs'}), ], @@ -208,7 +228,10 @@ export class Track extends Thing { exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), - withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ], @@ -257,7 +280,11 @@ export class Track extends Thing { date: [ exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - withPropertyFromAlbum({property: 'date'}), + + withPropertyFromAlbum({ + property: input.value('date'), + }), + exposeDependency({dependency: '#album.date'}), ], @@ -608,9 +635,7 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ : continuation()), }, - withResolvedContribs({ - from: 'coverArtistContribs', - }), + withResolvedContribs({from: 'coverArtistContribs'}), { dependencies: ['#resolvedContribs'], @@ -625,7 +650,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ })), }, - withPropertyFromAlbum({property: 'trackCoverArtistContribs'}), + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), { dependencies: ['#album.trackCoverArtistContribs'], @@ -663,6 +690,8 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ export const trackReverseReferenceList = templateCompositeFrom({ annotation: `trackReverseReferenceList`, + compose: false, + inputs: { list: input({type: 'string'}), }, -- cgit 1.3.0-6-gf8a5 From 6ca1e4b3ad691478e94f09cfe94683cb079f6bdf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:20:19 -0300 Subject: data: update withPropertiesFromObject --- src/data/things/composite.js | 103 ++++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 26 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cfa557de..4be01a55 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1764,34 +1764,85 @@ export const withPropertyFromObject = templateCompositeFrom({ // as a dependency prefixed with the same name as the object (by default). // If the object itself is null, all provided dependencies will be null; // if it's missing only select properties, those will be provided as null. -export function withPropertiesFromObject({ - object, - properties, - prefix = - (object.startsWith('#') - ? object - : `#${object}`), -}) { - return { - annotation: `withPropertiesFromObject`, - flags: {expose: true, compose: true}, +export const withPropertiesFromObject = templateCompositeFrom({ + annotation: `withPropertiesFromObject`, - expose: { - mapDependencies: {object}, - options: {prefix, properties}, + inputs: { + object: input({ + type: 'object', + null: true, + }), - compute: (continuation, {object, '#options': {prefix, properties}}) => - continuation( - Object.fromEntries( - properties.map(property => [ - `${prefix}.${property}`, - (object === null || object === undefined - ? null - : object[property] ?? null), - ]))), + properties: input({ + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`)) + : '#object'), + + steps: () => [ + { + dependencies: [input('object'), input('properties')], + compute: (continuation, { + [input('object')]: object, + [input('properties')]: properties, + }) => continuation({ + ['#entries']: + (object === null + ? properties.map(property => [property, null]) + : properties.map(property => [property, object[property]])), + }), }, - }; -} + + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#entries', + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#entries']: entries, + }) => + (properties + ? continuation( + Object.fromEntries( + entries.map(([property, value]) => [ + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`), + value ?? null, + ]))) + : continuation({ + ['#object']: + Object.fromEntries(entries), + })), + }, + ], +}); // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions @@ -1846,7 +1897,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input({ + prefix: input.staticValue({ type: 'string', null: true, }), -- cgit 1.3.0-6-gf8a5 From ee3b52cfe889eb514f5d6a5f78297875f278e206 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:37:20 -0300 Subject: data: update exposeConstant, fillMissingListItems --- src/data/things/composite.js | 103 ++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 54 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4be01a55..40f4fc16 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1451,17 +1451,24 @@ export const exposeDependency = templateCompositeFrom({ // descriptor. It generally follows steps which will conditionally early // exit with some other value, with the exposeConstant base serving as the // fallback default value. -export function exposeConstant({value}) { - return { - annotation: `exposeConstant`, - flags: {expose: true}, +export const exposeConstant = templateCompositeFrom({ + annotation: `exposeConstant`, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, }, - }; -} + ], +}); // Checks the availability of a dependency and provides the result to later // steps under '#availability' (by default). This is mainly intended for use @@ -1964,55 +1971,43 @@ export const withPropertiesFromList = templateCompositeFrom({ }); // Replaces items of a list, which are null or undefined, with some fallback -// value, either a constant (set {value}) or from a dependency ({dependency}). -// By default, this replaces the passed dependency. -export function fillMissingListItems({ - list, - value, - dependency, - into = list, -}) { - if (value !== undefined && dependency !== undefined) { - throw new TypeError(`Don't provide both value and dependency`); - } +// value. By default, this replaces the passed dependency. +export const fillMissingListItems = templateCompositeFrom({ + annotation: `fillMissingListItems`, - if (value === undefined && dependency === undefined) { - throw new TypeError(`Missing value or dependency`); - } - - if (dependency) { - return { - annotation: `fillMissingListItems.fromDependency`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list, dependency}, - mapContinuation: {into}, + inputs: { + list: input({type: 'array'}), + fill: input(), + }, - compute: (continuation, {list, dependency}) => - continuation({ - into: list.map(item => item ?? dependency), - }), - }, - }; - } else { - return { - annotation: `fillMissingListItems.fromValue`, - flags: {expose: true, compose: true}, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {value}, + steps: () => [ + { + dependencies: [input('list'), input('fill')], + compute: (continuation, { + [input('list')]: list, + [input('fill')]: fill, + }) => continuation({ + ['#filled']: + list.map(item => item ?? fill), + }), + }, - compute: (continuation, {list, '#options': {value}}) => - continuation({ - into: list.map(item => item ?? value), - }), - }, - }; - } -} + { + dependencies: [input.staticDependency('list'), '#filled'], + compute: (continuation, { + [input.staticDependency('list')]: list, + ['#filled']: filled, + }) => continuation({ + [list ?? '#list']: + filled, + }), + }, + ], +}); // Filters particular values out of a list. Note that this will always // completely skip over null, but can be used to filter out any other -- cgit 1.3.0-6-gf8a5 From e3e8a904c24e71f303a1f29c8f1700478d929901 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:37:36 -0300 Subject: data: miscellaneous syntax fixes --- src/data/things/album.js | 17 ++++++++++++++--- src/data/things/composite.js | 6 +++--- src/data/things/thing.js | 4 ++-- src/data/things/track.js | 25 ++++++++++++++++--------- 4 files changed, 35 insertions(+), 17 deletions(-) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index 20a1a5b3..fd8a71d3 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -122,9 +122,20 @@ export class Album extends Thing { ]), }), - fillMissingListItems({list: '#sections.tracks', value: []}), - fillMissingListItems({list: '#sections.isDefaultTrackSection', value: false}), - fillMissingListItems({list: '#sections.color', dependency: 'color'}), + fillMissingListItems({ + list: '#sections.tracks', + fill: input.value([]), + }), + + fillMissingListItems({ + list: '#sections.isDefaultTrackSection', + fill: input.value(false), + }), + + fillMissingListItems({ + list: '#sections.color', + fill: input.dependency('color'), + }), withFlattenedList({ list: '#sections.tracks', diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 40f4fc16..38b7bcc9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2073,7 +2073,7 @@ export const withFlattenedList = templateCompositeFrom({ const flattenedList = sourceList.flat(); const indices = []; let lastEndIndex = 0; - for (const {length} of sourceArray) { + for (const {length} of sourceList) { indices.push(lastEndIndex); lastEndIndex += length; } @@ -2116,8 +2116,8 @@ export const withUnflattenedList = templateCompositeFrom({ steps: () => [ { - dependencies: [input('list'), input('indices')], - compute({ + dependencies: [input('list'), input('indices'), input('filter')], + compute(continuation, { [input('list')]: list, [input('indices')]: indices, [input('filter')]: filter, diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f63a619d..0dea1fa4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -216,7 +216,7 @@ export function contributionList() { steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: []}), + exposeConstant({value: input.value([])}), ], }); } @@ -343,7 +343,7 @@ export const singleReference = templateCompositeFrom({ withResolvedReference({ ref: input.updateValue(), data: input('data'), - find: input('findFunction'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReference'}), diff --git a/src/data/things/track.js b/src/data/things/track.js index f31fe3ae..5ccf4f8b 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -210,7 +210,10 @@ export class Track extends Thing { ], contributorContribs: [ - inheritFromOriginalRelease({property: 'contributorContribs'}), + inheritFromOriginalRelease({ + property: input.value('contributorContribs'), + }), + contributionList(), ], @@ -236,7 +239,9 @@ export class Track extends Thing { ], referencedTracks: [ - inheritFromOriginalRelease({property: 'referencedTracks'}), + inheritFromOriginalRelease({ + property: input.value('referencedTracks'), + }), referenceList({ class: input.value(Track), @@ -246,7 +251,9 @@ export class Track extends Thing { ], sampledTracks: [ - inheritFromOriginalRelease({property: 'sampledTracks'}), + inheritFromOriginalRelease({ + property: input.value('sampledTracks'), + }), referenceList({ class: input.value(Track), @@ -313,11 +320,11 @@ export class Track extends Thing { { flags: {expose: true}, expose: { - dependencies: ['this', 'trackData', '#originalRelease'], + dependencies: [input.myself(), '#originalRelease', 'trackData'], compute: ({ - this: thisTrack, + [input.myself()]: thisTrack, + ['#originalRelease']: originalRelease, trackData, - '#originalRelease': originalRelease, }) => (originalRelease === thisTrack ? [] @@ -339,17 +346,17 @@ export class Track extends Thing { // the "Tracks - by Times Referenced" listing page (or other data // processing). referencedByTracks: trackReverseReferenceList({ - list: 'referencedTracks', + list: input.value('referencedTracks'), }), // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ - list: 'sampledTracks', + list: input.value('sampledTracks'), }), featuredInFlashes: reverseReferenceList({ data: 'flashData', - list: 'featuredTracks', + list: input.value('featuredTracks'), }), }); -- cgit 1.3.0-6-gf8a5 From c1018a0163ae28dc122aad7cb292a5e805c3d25a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:30:49 -0300 Subject: data: fix update collation from steps --- src/data/things/composite.js | 48 +++++++++++++++++++++++--------------------- src/data/things/thing.js | 35 +++++++++++--------------------- 2 files changed, 37 insertions(+), 46 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 38b7bcc9..f744f604 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -573,13 +573,22 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const inputMetadata = getStaticInputMetadata(inputOptions); + const expectedOutputNames = (Array.isArray(description.outputs) ? description.outputs : typeof description.outputs === 'function' - ? description.outputs(getStaticInputMetadata(inputOptions)) + ? description.outputs(inputMetadata) : []); + const ownUpdateDescription = + (typeof description.update === 'object' + ? description.update + : typeof description.update === 'function' + ? description.update(inputMetadata) + : null); + const outputOptions = {}; const instantiatedTemplate = { @@ -644,8 +653,8 @@ export function templateCompositeFrom(description) { finalDescription.compose = description.compose; } - if ('update' in description) { - finalDescription.update = description.update; + if (ownUpdateDescription) { + finalDescription.update = ownUpdateDescription; } if ('inputs' in description) { @@ -820,7 +829,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); - const updateDescription = {}; // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. @@ -932,7 +940,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, - update: stepUpdateDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -942,7 +949,6 @@ export function compositeFrom(description) { const { step, expose, - update, stepComposes, stepComputes, stepTransforms, @@ -974,12 +980,6 @@ export function compositeFrom(description) { return push(new TypeError( `Steps which only transform can't be used in a composition that doesn't update`)); } - - if (update) { - // TODO: This is a dumb assign statement, and it could probably do more - // interesting things, like combining validation functions. - Object.assign(updateDescription, update); - } }); } @@ -1332,8 +1332,13 @@ export function compositeFrom(description) { compose: compositionNests, }; - if (constructedDescriptor.update) { - constructedDescriptor.update = updateDescription; + if (compositionUpdates) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + constructedDescriptor.update = + Object.assign( + {...description.update ?? {}}, + ...stepUpdateDescriptions.filter(Boolean)); } if (compositionExposes) { @@ -1569,15 +1574,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ validate: input({type: 'function', null: true}), }, - update: { - dependencies: [input.staticValue('validate')], - compute: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - }, + update: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), steps: () => [ exposeDependencyOrContinue({ diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0dea1fa4..ca1018eb 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -281,24 +281,19 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input(thingClassInput), + class: input.staticValue(thingClassInput), + find: input({type: 'function'}), // todo: validate data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; }, steps: () => [ @@ -326,17 +321,11 @@ export const singleReference = templateCompositeFrom({ data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, steps: () => [ -- cgit 1.3.0-6-gf8a5 From cb124756780e41c6791981233da4b56c031d6142 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:52:46 -0300 Subject: data: support update description in input.updateValue() --- src/data/things/composite.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f744f604..e6cc267a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -369,7 +369,15 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.updateValue = (description = null) => + (description + ? { + symbol: input.symbol, + shape: 'input.updateValue', + value: description, + } + : Symbol.for('hsmusic.composite.input.updateValue')); + input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); @@ -814,7 +822,9 @@ export function compositeFrom(description) { .filter(Boolean); const anyInputsUseUpdateValue = - dependenciesFromInputs.includes(input.updateValue()); + dependenciesFromInputs + .filter(dependency => isInputToken(dependency)) + .some(token => getInputTokenShape(token) === 'input.updateValue'); const base = composition.at(-1); const steps = composition.slice(); @@ -871,13 +881,22 @@ export function compositeFrom(description) { : null)); // The update description for a step, if present at all, is always set - // explicitly. + // explicitly. There may be multiple per step - namely that step's own + // {update} description, and any descriptions passed as the value in an + // input.updateValue({...}) token. const stepUpdateDescriptions = steps .map((step, index) => (stepsUpdate[index] - ? step.update ?? null - : null)); + ? [ + step.update ?? null, + ...(stepExposeDescriptions[index]?.dependencies ?? []) + .filter(dependency => isInputToken(dependency)) + .filter(token => getInputTokenShape(token) === 'input.updateValue') + .map(token => getInputTokenValue(token)) + .filter(Boolean), + ] + : [])); // Indicates presence of a {compute} function on the expose description. const stepsCompute = @@ -1338,7 +1357,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, - ...stepUpdateDescriptions.filter(Boolean)); + ...stepUpdateDescriptions.flat()); } if (compositionExposes) { -- cgit 1.3.0-6-gf8a5 From 8e3e15be98d43c1aa8a4f13709106f6848a0a9e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:53:35 -0300 Subject: data: use error.cause for nested composite compute errors --- src/data/things/composite.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e6cc267a..4074aef7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1369,15 +1369,27 @@ export function compositeFrom(description) { ...dependenciesFromSteps, ]); + const _wrapper = (...args) => { + try { + return _computeOrTransform(...args); + } catch (thrownError) { + const error = new Error( + `Error computing composition` + + (annotation ? ` ${annotation}` : '')); + error.cause = thrownError; + throw error; + } + }; + if (compositionNests) { if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, continuation, dependencies); + _wrapper(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, continuation, dependencies); + _wrapper(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { @@ -1385,10 +1397,10 @@ export function compositeFrom(description) { } } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, null, dependencies); + _wrapper(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, null, dependencies); + _wrapper(noTransformSymbol, null, dependencies); } } -- cgit 1.3.0-6-gf8a5 From 998cc860302e3fb1e7a40c055e8ac66f195b1366 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:54:34 -0300 Subject: data: withResultOfAvailabilityCheck: handle undefined in 'empty' --- src/data/things/composite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4074aef7..700cc922 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1512,8 +1512,8 @@ export const exposeConstant = templateCompositeFrom({ // Customize {mode} to select one of these modes, or default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null nor an empty array. -// This will outright error for undefined. +// * 'empty': Check that the value is neither null, undefined, nor an empty +// array. // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! @@ -1546,11 +1546,11 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ switch (mode) { case 'null': - availability = value !== null && value !== undefined; + availability = value !== undefined && value !== null; break; case 'empty': - availability = !empty(value); + availability = value !== undefined && !empty(value); break; case 'falsy': -- cgit 1.3.0-6-gf8a5 From 4852931ecf2c7ce63851ea6f3a60c9d5b142ae6f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 16:04:06 -0300 Subject: data: minor fixes --- src/data/things/thing.js | 2 +- src/data/things/track.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src/data') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ca1018eb..77f549fe 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -602,7 +602,7 @@ export const withResolvedReference = templateCompositeFrom({ } return continuation.raiseOutput({ - ['#resolvedReference']: match, + ['#resolvedReference']: match ?? null, }); }, }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 5ccf4f8b..54b2c124 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -638,7 +638,9 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ dependencies: ['disableUniqueCoverArt'], compute: (continuation, {disableUniqueCoverArt}) => (disableUniqueCoverArt - ? continuation.raiseOutput({into: false}) + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: false, + }) : continuation()), }, @@ -652,8 +654,7 @@ export const withHasUniqueCoverArt = templateCompositeFrom({ (empty(contribsFromTrack) ? continuation() : continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - true, + ['#hasUniqueCoverArt']: true, })), }, -- cgit 1.3.0-6-gf8a5 From 72c526dfeee2b227400b73c3b220cf36c885b703 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 17:01:21 -0300 Subject: data: auto-prefix '#' in output names --- src/data/things/composite.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 700cc922..26df71ae 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,6 +588,10 @@ export function templateCompositeFrom(description) { ? description.outputs : typeof description.outputs === 'function' ? description.outputs(inputMetadata) + .map(name => + (name.startsWith('#') + ? name + : '#' + name)) : []); const ownUpdateDescription = @@ -797,7 +801,9 @@ export function compositeFrom(description) { Object.entries(description.outputs) .map(([continuationName, outputName]) => [ outputName, - providedDependencies[continuationName], + (continuationName in providedDependencies + ? providedDependencies[continuationName] + : providedDependencies[continuationName.replace(/^#/, '')]), ]))); } -- cgit 1.3.0-6-gf8a5 From 981a39a5f3c2a592c84f92692c204b090622aec9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 17:01:51 -0300 Subject: data: fix input.myself() not being spotted in inputs --- src/data/things/composite.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26df71ae..7a9048c2 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -821,6 +821,8 @@ export function compositeFrom(description) { case 'input': case 'input.updateValue': return token; + case 'input.myself': + return 'this'; default: return null; } -- cgit 1.3.0-6-gf8a5 From 64476074cbc7375afe2388ddd6e9e3275c25f3bd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 17:02:18 -0300 Subject: data: minor fixes --- src/data/things/track.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 54b2c124..3e0d95bf 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -192,7 +192,9 @@ export class Track extends Thing { }), artistContribs: [ - inheritFromOriginalRelease({property: 'artistContribs'}), + inheritFromOriginalRelease({ + property: input.value('artistContribs'), + }), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), -- cgit 1.3.0-6-gf8a5 From e14ed656f5bd1577118d053317037377c1a7a818 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 22 Sep 2023 14:00:16 -0300 Subject: data: miscellaneous improvements/fixes for updating composites --- src/data/things/composite.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a9048c2..98537c95 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -869,13 +869,18 @@ export function compositeFrom(description) { ? compositionNests : true))); - // Steps don't update unless the corresponding flag is explicitly set. + // Steps update if the corresponding flag is explicitly set, if a transform + // function is provided, or if the dependencies include an input.updateValue + // token. const stepsUpdate = steps .map(step => (step.flags ? step.flags.update ?? false - : false)); + : !!step.transform || + !!step.dependencies?.some(dependency => + isInputToken(dependency) && + getInputTokenShape(dependency) === 'input.updateValue'))); // The expose description for a step is just the entire step object, when // using the shorthand syntax where {flags: {expose: true}} is left implied. @@ -901,9 +906,8 @@ export function compositeFrom(description) { ...(stepExposeDescriptions[index]?.dependencies ?? []) .filter(dependency => isInputToken(dependency)) .filter(token => getInputTokenShape(token) === 'input.updateValue') - .map(token => getInputTokenValue(token)) - .filter(Boolean), - ] + .map(token => getInputTokenValue(token)), + ].filter(Boolean) : [])); // Indicates presence of a {compute} function on the expose description. @@ -960,6 +964,7 @@ export function compositeFrom(description) { anyStepsExpose; const compositionUpdates = + 'update' in description || anyInputsUseUpdateValue || anyStepsUseUpdateValue || anyStepsUpdate; @@ -1010,8 +1015,8 @@ export function compositeFrom(description) { }); } - if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute or update`)); + if (!compositionNests && !anyStepsCompute && !anyStepsTransform) { + aggregate.push(new TypeError(`Expected at least one step to compute or transform`)); } aggregate.close(); @@ -1341,6 +1346,7 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); + if (providedValue !== null) valueSoFar = providedValue; break; } } @@ -1395,7 +1401,7 @@ export function compositeFrom(description) { _wrapper(value, continuation, dependencies); } - if (anyStepsCompute) { + if (anyStepsCompute && !anyStepsUseUpdateValue && !anyInputsUseUpdateValue) { expose.compute = (continuation, dependencies) => _wrapper(noTransformSymbol, continuation, dependencies); } -- cgit 1.3.0-6-gf8a5 From 7f7c50e7976bebc937c302638cade5e1fd543ff4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 22 Sep 2023 14:00:43 -0300 Subject: data: improve selecting values for input tokens in dependencies --- src/data/things/composite.js | 45 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98537c95..da2848f8 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1163,21 +1163,46 @@ export function compositeFrom(description) { let continuationStorage; + const filterableDependencies = { + ...availableDependencies, + ...inputMetadata, + ...inputValues, + ... + (expectingTransform + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies?.['this'] ?? null, + }; + + const selectDependencies = + (expose.dependencies ?? []).map(dependency => { + if (!isInputToken(dependency)) return dependency; + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input': + case 'input.staticDependency': + case 'input.staticValue': + return dependency; + case 'input.myself': + return input.myself(); + case 'input.dependency': + return tokenValue; + case 'input.updateValue': + return input.updateValue(); + default: + throw new Error(`Unexpected token ${tokenShape} as dependency`); + } + }) + const filteredDependencies = - filterProperties({ - ...availableDependencies, - ...inputMetadata, - ...inputValues, - ... - (callingTransformForThisStep - ? {[input.updateValue()]: valueSoFar} - : {}), - [input.myself()]: initialDependencies['this'], - }, expose.dependencies ?? []); + filterProperties(filterableDependencies, selectDependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies, + `selecting:`, selectDependencies, + `from available:`, filterableDependencies, ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; -- cgit 1.3.0-6-gf8a5 From 8bcae16b391762f6b533654ec06c3bf0c8770d35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 20:24:08 -0300 Subject: data, test: WIP tests for compositeFrom --- src/data/things/composite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index da2848f8..c0f0ab0b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -752,6 +752,9 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); +export const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); +export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); + export function compositeFrom(description) { const {annotation} = description; @@ -1070,9 +1073,6 @@ export function compositeFrom(description) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); - const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; -- cgit 1.3.0-6-gf8a5 From f3d98f5ea63db7f7b2155e7efb0812f025c5bcf3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 20:35:57 -0300 Subject: data, test: collate update description from composition inputs --- src/data/things/composite.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c0f0ab0b..34e550a1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -837,6 +837,16 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + // Update descriptions passed as the value in an input.updateValue() token, + // as provided as inputs for this composition. + const inputUpdateDescriptions = + Object.values(description.inputs ?? {}) + .map(token => + (getInputTokenShape(token) === 'input.updateValue' + ? getInputTokenValue(token) + : null)) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -1396,6 +1406,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, + ...inputUpdateDescriptions, ...stepUpdateDescriptions.flat()); } -- cgit 1.3.0-6-gf8a5 From 84b09a42c7baf248115f596217c07871e374d1af Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:10:38 -0300 Subject: data: fix updating valueSoFar on non-transform calls --- src/data/things/composite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 34e550a1..fdb80cf3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1381,7 +1381,9 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); - if (providedValue !== null) valueSoFar = providedValue; + if (callingTransformForThisStep && providedValue !== null) { + valueSoFar = providedValue; + } break; } } -- cgit 1.3.0-6-gf8a5 From 3b458e5c403054bda58733e238ab666596cc9f70 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:12:54 -0300 Subject: data: refactor/tidy input token construction --- src/data/things/composite.js | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fdb80cf3..293952b7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -355,35 +355,30 @@ import { const globalCompositeCache = {}; -export function input(nameOrDescription) { - if (typeof nameOrDescription === 'string') { - return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); - } else { - return { - symbol: Symbol.for('hsmusic.composite.input'), - shape: 'input', - value: nameOrDescription, - }; - } -} +const _valueIntoToken = shape => + (value = null) => + (value === null + ? Symbol.for(`hsmusic.composite.${shape}`) + : typeof value === 'string' + ? Symbol.for(`hsmusic.composite.${shape}:${value}`) + : { + symbol: Symbol.for(`hsmusic.composite.input`), + shape, + value, + }); +export const input = _valueIntoToken('input'); input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = (description = null) => - (description - ? { - symbol: input.symbol, - shape: 'input.updateValue', - value: description, - } - : Symbol.for('hsmusic.composite.input.updateValue')); +input.value = _valueIntoToken('input.value'); +input.dependency = _valueIntoToken('input.dependency'); input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); -input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); -input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); -input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); -input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); +input.updateValue = _valueIntoToken('input.updateValue'); + +input.staticDependency = _valueIntoToken('input.staticDependency'); +input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { if (typeof token === 'object') { -- cgit 1.3.0-6-gf8a5 From b4137b02f09761b78c520e5514381cda714dcf6d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:14:11 -0300 Subject: data: fix calls to oneOf instead of is --- src/data/things/composite.js | 4 ++-- src/data/things/thing.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 293952b7..791b8360 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,10 +4,10 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + is, isArray, isString, isWholeNumber, - oneOf, validateArrayItems, } from '#validators'; @@ -1567,7 +1567,7 @@ export const exposeConstant = templateCompositeFrom({ // const availabilityCheckModeInput = { - validate: oneOf('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy'), defaultValue: 'null', }; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 77f549fe..ef547f74 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,7 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {oneOf} from '#validators'; +import {is} from '#validators'; import { compositeFrom, @@ -460,7 +460,7 @@ export const withResolvedContribs = templateCompositeFrom({ from: input(), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'null', }), }, @@ -562,7 +562,7 @@ export const withResolvedReference = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('null', 'exit'), + validate: is('null', 'exit'), defaultValue: 'null', }), }, @@ -627,7 +627,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'filter', }), }, -- cgit 1.3.0-6-gf8a5 From e304bebf19340b825df10a17315b534f5dca0219 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:15:06 -0300 Subject: data: WIP input validation Static only, as of this commit. --- src/data/things/composite.js | 55 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 791b8360..27b345cd 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -548,7 +548,12 @@ export function templateCompositeFrom(description) { }); const wrongTypeInputNames = []; - const wrongInputCallInputNames = []; + + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; + + const validateFailedInputNames = []; + const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { if (misplacedInputNames.includes(name)) { @@ -559,6 +564,37 @@ export function templateCompositeFrom(description) { wrongTypeInputNames.push(name); continue; } + + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); + + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + } + + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + } + + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } + } + } } if (!empty(misplacedInputNames)) { @@ -569,6 +605,23 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } + if (!empty(expectedStaticDependencyInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } + + if (!empty(expectedStaticValueInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } + + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + inputOptionsAggregate.push(error); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 219596b6d52443d1090c94e50244cf79d548a167 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 25 Sep 2023 08:48:19 -0300 Subject: data, test: exposeConstant, withResultOfAvailabilityCheck --- src/data/things/composite.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 27b345cd..1148687c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -721,7 +721,6 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes const description = getInputTokenValue(description_); const tokenShape = getInputTokenShape(description_); -- cgit 1.3.0-6-gf8a5 From b5cfc2a793f22da60606a4dd7387fcf3d3163843 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 25 Sep 2023 14:23:23 -0300 Subject: data: misc. improvements for input validation & infrastructure --- src/data/things/composite.js | 254 ++++++++++++++++++++++++++++-------------- src/data/things/thing.js | 72 +++++++----- src/data/things/track.js | 17 +-- src/data/things/validators.js | 58 +++++++++- 4 files changed, 278 insertions(+), 123 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1148687c..0f943ec3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,7 +5,6 @@ import {TupleMap} from '#wiki-data'; import { is, - isArray, isString, isWholeNumber, validateArrayItems, @@ -18,6 +17,7 @@ import { openAggregate, stitchArrays, unique, + withAggregate, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -443,13 +443,53 @@ function getStaticInputMetadata(inputOptions) { return metadata; } -export function templateCompositeFrom(description) { - const compositeName = +function getCompositionName(description) { + return ( (description.annotation ? description.annotation - : `unnamed composite`); + : `unnamed composite`)); +} + +function validateInputValue(value, description) { + const tokenValue = getInputTokenValue(description); + + const {acceptsNull, defaultValue, type, validate} = tokenValue || {}; - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + if (value === null || value === undefined) { + if (acceptsNull || defaultValue === null) { + return true; + } else { + throw new TypeError( + (type + ? `Expected ${type}, got ${value}` + : `Expected value, got ${value}`)); + } + } + + if (type) { + // Note: null is already handled earlier in this function, so it won't + // cause any trouble here. + const typeofValue = + (typeof value === 'object' + ? Array.isArray(value) ? 'array' : 'object' + : typeof value); + + if (typeofValue !== type) { + throw new TypeError(`Expected ${type}, got ${typeofValue}`); + } + } + + if (validate) { + validate(value); + } + + return true; +} + +export function templateCompositeFrom(description) { + const compositionName = getCompositionName(description); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); if ('steps' in description) { if (Array.isArray(description.steps)) { @@ -469,7 +509,7 @@ export function templateCompositeFrom(description) { break validateInputs; } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { const missingCallsToInput = []; const wrongCallsToInput = []; @@ -515,7 +555,7 @@ export function templateCompositeFrom(description) { throw new Error(`${value}: Expected "#" at start`); } }), - {message: `Errors in output descriptions for ${compositeName}`}); + {message: `Errors in output descriptions for ${compositionName}`}); } } @@ -527,7 +567,7 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); const providedInputNames = Object.keys(inputOptions); @@ -543,7 +583,6 @@ export function templateCompositeFrom(description) { if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; - if (inputDescription.null === true) return false; return true; }); @@ -655,7 +694,7 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); const misplacedOutputNames = []; const wrongTypeOutputNames = []; @@ -718,28 +757,27 @@ export function templateCompositeFrom(description) { } if ('inputs' in description) { - const finalInputs = {}; - - for (const [name, description_] of Object.entries(description.inputs)) { - const description = getInputTokenValue(description_); - const tokenShape = getInputTokenShape(description_); + const inputMapping = {}; + for (const [name, token] of Object.entries(description.inputs)) { + const tokenValue = getInputTokenValue(token); if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { - finalInputs[name] = input.dependency(inputOptions[name]); + inputMapping[name] = input.dependency(inputOptions[name]); } else { - finalInputs[name] = inputOptions[name]; + inputMapping[name] = inputOptions[name]; } - } else if (description.defaultValue) { - finalInputs[name] = input.value(description.defaultValue); - } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(description.defaultDependency); + } else if (tokenValue.defaultValue) { + inputMapping[name] = input.value(tokenValue.defaultValue); + } else if (tokenValue.defaultDependency) { + inputMapping[name] = input.dependency(tokenValue.defaultDependency); } else { - finalInputs[name] = input.value(null); + inputMapping[name] = input.value(null); } } - finalDescription.inputs = finalInputs; + finalDescription.inputMapping = inputMapping; + finalDescription.inputDescriptions = description.inputs; } if ('outputs' in description) { @@ -768,7 +806,7 @@ export function templateCompositeFrom(description) { const finalDescription = {...ownDescription}; - const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + const aggregate = openAggregate({message: `Errors resolving ${compositionName}`}); const steps = ownDescription.steps(); @@ -804,6 +842,7 @@ export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol' export function compositeFrom(description) { const {annotation} = description; + const compositionName = getCompositionName(description); const debug = fn => { if (compositeFrom.debug === true) { @@ -835,7 +874,7 @@ export function compositeFrom(description) { ? compositeFrom(step.toResolvedComposition()) : step)); - const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + const inputMetadata = getStaticInputMetadata(description.inputMapping ?? {}); function _mapDependenciesToOutputs(providedDependencies) { if (!description.outputs) { @@ -861,7 +900,7 @@ export function compositeFrom(description) { // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => { const tokenShape = getInputTokenShape(token); const tokenValue = getInputTokenValue(token); @@ -884,10 +923,41 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + const inputNames = + Object.keys(description.inputMapping ?? {}); + + const inputSymbols = + inputNames.map(name => input(name)); + + const inputsMayBeDynamicValue = + stitchArrays({ + mappingToken: Object.values(description.inputMapping ?? {}), + descriptionToken: Object.values(description.inputDescriptions ?? {}), + }).map(({mappingToken, descriptionToken}) => { + if (getInputTokenShape(descriptionToken) === 'input.staticValue') return false; + if (getInputTokenShape(mappingToken) === 'input.value') return false; + return true; + }); + + const inputDescriptions = + Object.values(description.inputDescriptions ?? {}); + + /* + const inputsAcceptNull = + Object.values(description.inputDescriptions ?? {}) + .map(token => { + const tokenValue = getInputTokenValue(token); + if (!tokenValue) return false; + if ('acceptsNull' in tokenValue) return tokenValue.acceptsNull; + if ('defaultValue' in tokenValue) return tokenValue.defaultValue === null; + return false; + }); + */ + // Update descriptions passed as the value in an input.updateValue() token, // as provided as inputs for this composition. const inputUpdateDescriptions = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => (getInputTokenShape(token) === 'input.updateValue' ? getInputTokenValue(token) @@ -903,7 +973,6 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - // TODO: Check description.compose ?? true instead. const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); @@ -1141,30 +1210,44 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; const inputValues = - ('inputs' in description - ? Object.fromEntries(Object.entries(description.inputs) - .map(([name, token]) => { - const tokenShape = getInputTokenShape(token); - const tokenValue = getInputTokenValue(token); - switch (tokenShape) { - case 'input.dependency': - return [input(name), initialDependencies[tokenValue]]; - case 'input.value': - return [input(name), tokenValue]; - case 'input.updateValue': - if (!expectingTransform) { - throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); - } - return [input(name), valueSoFar]; - case 'input.myself': - return [input(name), initialDependencies['this']]; - case 'input': - return [input(name), initialDependencies[token]]; - default: - throw new TypeError(`Unexpected input shape ${tokenShape}`); - } - })) - : {}); + Object.values(description.inputMapping ?? {}) + .map(token => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return initialDependencies[tokenValue]; + case 'input.value': + return tokenValue; + case 'input.updateValue': + if (!expectingTransform) + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + return valueSoFar; + case 'input.myself': + return initialDependencies['this']; + case 'input': + return initialDependencies[token]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + }); + + withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + for (const {dynamic, name, value, description} of stitchArrays({ + dynamic: inputsMayBeDynamicValue, + name: inputNames, + value: inputValues, + description: inputDescriptions, + })) { + if (!dynamic) continue; + try { + validateInputValue(value, description); + } catch (error) { + error.message = `${name}: ${error.message}`; + throw error; + } + } + }); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1220,10 +1303,15 @@ export function compositeFrom(description) { let continuationStorage; + const inputDictionary = + Object.fromEntries( + stitchArrays({symbol: inputSymbols, value: inputValues}) + .map(({symbol, value}) => [symbol, value])); + const filterableDependencies = { ...availableDependencies, ...inputMetadata, - ...inputValues, + ...inputDictionary, ... (expectingTransform ? {[input.updateValue()]: valueSoFar} @@ -1568,7 +1656,7 @@ export const exposeDependency = templateCompositeFrom({ compose: false, inputs: { - dependency: input.staticDependency(), + dependency: input.staticDependency({acceptsNull: true}), }, steps: () => [ @@ -1618,17 +1706,17 @@ export const exposeConstant = templateCompositeFrom({ // for values like zero and the empty string! // -const availabilityCheckModeInput = { +const inputAvailabilityCheckMode = () => input({ validate: is('null', 'empty', 'falsy'), defaultValue: 'null', -}; +}); export const withResultOfAvailabilityCheck = templateCompositeFrom({ annotation: `withResultOfAvailabilityCheck`, inputs: { - from: input(), - mode: input(availabilityCheckModeInput), + from: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, outputs: ['#availability'], @@ -1669,8 +1757,8 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ annotation: `exposeDependencyOrContinue`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, steps: () => [ @@ -1700,8 +1788,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckModeInput), - validate: input({type: 'function', null: true}), + mode: inputAvailabilityCheckMode(), + + validate: input({ + type: 'function', + defaultValue: null, + }), }, update: ({ @@ -1725,9 +1817,9 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), - value: input({null: true}), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), }, steps: () => [ @@ -1755,7 +1847,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), value: input({defaultValue: null}), }, @@ -1763,6 +1855,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), + value: input('value'), }), ], }); @@ -1773,8 +1866,8 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1807,7 +1900,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1841,7 +1934,7 @@ export const withPropertyFromObject = templateCompositeFrom({ annotation: `withPropertyFromObject`, inputs: { - object: input({type: 'object', null: true}), + object: input({type: 'object', acceptsNull: true}), property: input({type: 'string'}), }, @@ -1907,19 +2000,13 @@ export const withPropertiesFromObject = templateCompositeFrom({ annotation: `withPropertiesFromObject`, inputs: { - object: input({ - type: 'object', - null: true, - }), + object: input({type: 'object', acceptsNull: true}), properties: input({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2036,10 +2123,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2109,7 +2193,7 @@ export const fillMissingListItems = templateCompositeFrom({ inputs: { list: input({type: 'array'}), - fill: input(), + fill: input({acceptsNull: true}), }, outputs: ({ @@ -2150,8 +2234,8 @@ export const excludeFromList = templateCompositeFrom({ inputs: { list: input(), - item: input({null: true}), - items: input({validate: isArray, null: true}), + item: input({defaultValue: null}), + items: input({type: 'array', defaultValue: null}), }, outputs: ({ diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ef547f74..290be59b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,8 +25,8 @@ import { import { isAdditionalFileList, isBoolean, - isCommentary, isColor, + isCommentary, isContributionList, isDate, isDimensions, @@ -41,12 +41,13 @@ import { validateInstanceOf, validateReference, validateReferenceList, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); + static referenceType = Symbol.for('Thing.referenceType'); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); @@ -283,10 +284,8 @@ export const referenceList = templateCompositeFrom({ inputs: { class: input.staticValue(thingClassInput), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), - - // todo: validate - data: input(), }, update: ({ @@ -316,9 +315,7 @@ export const singleReference = templateCompositeFrom({ inputs: { class: input(thingClassInput), find: input({type: 'function'}), - - // todo: validate - data: input(), + data: inputWikiData({allowMixedTypes: false}), }, update: ({ @@ -347,7 +344,10 @@ export const contribsPresent = templateCompositeFrom({ compose: false, inputs: { - contribs: input({type: 'string'}), + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, + }), }, steps: () => [ @@ -371,9 +371,7 @@ export const reverseReferenceList = templateCompositeFrom({ compose: false, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, @@ -448,6 +446,21 @@ export const commentatorArtists = templateCompositeFrom({ // Compositional utilities +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData(referenceType), + acceptsNull: true, + }); +} + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -456,8 +469,10 @@ export const withResolvedContribs = templateCompositeFrom({ annotation: `withResolvedContribs`, inputs: { - // todo: validate - from: input(), + from: input({ + validate: isContributionList, + acceptsNull: true, + }), notFoundMode: input({ validate: is('exit', 'filter', 'null'), @@ -514,10 +529,12 @@ export const exitWithoutContribs = templateCompositeFrom({ annotation: `exitWithoutContribs`, inputs: { - // todo: validate - contribs: input(), + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), - value: input({null: true}), + value: input({defaultValue: null}), }, steps: () => [ @@ -553,12 +570,9 @@ export const withResolvedReference = templateCompositeFrom({ annotation: `withResolvedReference`, inputs: { - // todo: validate - ref: input(), - - // todo: validate - data: input(), + ref: input({type: 'string', acceptsNull: true}), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -618,12 +632,12 @@ export const withResolvedReferenceList = templateCompositeFrom({ annotation: `withResolvedReferenceList`, inputs: { - // todo: validate - list: input(), - - // todo: validate - data: input(), + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -706,9 +720,7 @@ export const withReverseReferenceList = templateCompositeFrom({ annotation: `withReverseReferenceList`, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, diff --git a/src/data/things/track.js b/src/data/things/track.js index 3e0d95bf..c77bf889 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -18,12 +18,13 @@ import { } from '#composite'; import { + is, isBoolean, isColor, isContributionList, isDate, isFileExtension, - oneOf, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -434,7 +435,7 @@ export const withAlbum = templateCompositeFrom({ inputs: { notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -488,7 +489,7 @@ export const withPropertyFromAlbum = templateCompositeFrom({ property: input.staticValue({type: 'string'}), notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -527,7 +528,7 @@ export const withContainingTrackSection = templateCompositeFrom({ inputs: { notFoundMode: input({ - validate: oneOf('exit', 'null'), + validate: is('exit', 'null'), defaultValue: 'null', }), }, @@ -589,8 +590,10 @@ export const withOriginalRelease = templateCompositeFrom({ inputs: { selfIfOriginal: input({type: 'boolean', defaultValue: false}), - // todo: validate - data: input({defaultDependency: 'trackData'}), + data: input({ + validate: validateWikiData({referenceType: 'track'}), + defaultDependency: 'trackData', + }), }, outputs: ['#originalRelease'], @@ -683,7 +686,7 @@ export const exitWithoutUniqueCoverArt = templateCompositeFrom({ annotation: `exitWithoutUniqueCoverArt`, inputs: { - value: input({null: true}), + value: input({defaultValue: null}), }, steps: () => [ diff --git a/src/data/things/validators.js b/src/data/things/validators.js index cd4c2b46..048f7ebb 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,7 +1,7 @@ import {inspect as nodeInspect} from 'node:util'; import {colors, ENABLE_COLOR} from '#cli'; -import {withAggregate} from '#sugar'; +import {empty, withAggregate} from '#sugar'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -404,6 +404,62 @@ export function validateReferenceList(type = '') { return validateArrayItems(validateReference(type)); } +export function validateWikiData({ + referenceType = '', + allowMixedTypes = false, +}) { + if (referenceType && allowMixedTypes) { + throw new TypeError(`Don't specify both referenceType and allowMixedTypes`); + } + + const isArrayOfObjects = validateArrayItems(isObject); + + return (array) => { + isArrayOfObjects(array); + + if (empty(array)) { + return true; + } + + const allRefTypes = + new Set(array.map(object => + object.constructor[Symbol.for('Thing.referenceType')])); + + if (allRefTypes.has(undefined)) { + if (allRefTypes.size === 1) { + throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + } else { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } + } + + if (allRefTypes.size > 1) { + if (allowMixedTypes) { + return true; + } + + const types = () => Array.from(allRefTypes).join(', '); + + if (referenceType) { + if (allRefTypes.has(referenceType)) { + allRefTypes.remove(referenceType); + throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) + } else { + throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + } + } + + throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); + } + + if (referenceType && !allRefTypes.has(referenceType)) { + throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + } + + return true; + }; +} + // Compositional utilities export function oneOf(...checks) { -- cgit 1.3.0-6-gf8a5 From 747df818115b4aefd2433990f2997fe4c80bc501 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 11:35:11 -0300 Subject: data: refactor most openAggregate calls -> withAggregate --- src/data/things/composite.js | 327 ++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 174 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 0f943ec3..26be4a67 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -489,77 +489,75 @@ function validateInputValue(value, description) { export function templateCompositeFrom(description) { const compositionName = getCompositionName(description); - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); - - if ('steps' in description) { - if (Array.isArray(description.steps)) { - descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); - } else if (typeof description.steps !== 'function') { - descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + withAggregate({message: `Errors in description for ${compositionName}`}, ({map, nest, push}) => { + if ('steps' in description) { + if (Array.isArray(description.steps)) { + push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + push(new TypeError(`Expected steps to be a function (returning an array)`)); + } } - } - validateInputs: - if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); - break validateInputs; - } + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; + } - descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; + nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); + } } - if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { - wrongCallsToInput.push(name); + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - } - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); - } + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + } + }); + } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - }); - } - validateOutputs: - if ('outputs' in description) { - if ( - !Array.isArray(description.outputs) && - typeof description.outputs !== 'function' - ) { - descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); - break validateOutputs; - } - - if (Array.isArray(description.outputs)) { - descriptionAggregate.map( - description.outputs, - decorateErrorWithIndex(value => { - if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) - } else if (!value.startsWith('#')) { - throw new Error(`${value}: Expected "#" at start`); - } - }), - {message: `Errors in output descriptions for ${compositionName}`}); + if (Array.isArray(description.outputs)) { + map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); + } + }), + {message: `Errors in output descriptions for ${compositionName}`}); + } } - } - - descriptionAggregate.close(); + }); const expectedInputNames = (description.inputs @@ -567,106 +565,104 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); - - const providedInputNames = Object.keys(inputOptions); - - const misplacedInputNames = - providedInputNames - .filter(name => !expectedInputNames.includes(name)); - - const missingInputNames = - expectedInputNames - .filter(name => !providedInputNames.includes(name)) - .filter(name => { - const inputDescription = description.inputs[name].value; - if (!inputDescription) return true; - if ('defaultValue' in inputDescription) return false; - if ('defaultDependency' in inputDescription) return false; - return true; - }); + withAggregate({message: `Errors in input options passed to ${compositionName}`}, ({push}) => { + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + return true; + }); - const wrongTypeInputNames = []; + const wrongTypeInputNames = []; - const expectedStaticValueInputNames = []; - const expectedStaticDependencyInputNames = []; + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; - const validateFailedErrors = []; + const validateFailedInputNames = []; + const validateFailedErrors = []; - for (const [name, value] of Object.entries(inputOptions)) { - if (misplacedInputNames.includes(name)) { - continue; - } + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } - if (typeof value !== 'string' && !isInputToken(value)) { - wrongTypeInputNames.push(name); - continue; - } + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } - const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); - const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); - const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } } - } - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } } - } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } } } } - } - - if (!empty(misplacedInputNames)) { - inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); - } - if (!empty(missingInputNames)) { - inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); - } + if (!empty(misplacedInputNames)) { + push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } - if (!empty(expectedStaticDependencyInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); - } + if (!empty(missingInputNames)) { + push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } - if (!empty(expectedStaticValueInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); - } + if (!empty(expectedStaticDependencyInputNames)) { + push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - inputOptionsAggregate.push(error); - } + if (!empty(expectedStaticValueInputNames)) { + push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } - for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); - } + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + push(error); + } - inputOptionsAggregate.close(); + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -694,48 +690,31 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); - - const misplacedOutputNames = []; - const wrongTypeOutputNames = []; - // const notPrivateOutputNames = []; + withAggregate({message: `Errors in output options passed to ${compositionName}`}, ({push}) => { + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } - for (const [name, value] of Object.entries(providedOptions)) { - if (!expectedOutputNames.includes(name)) { - misplacedOutputNames.push(name); - continue; + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } } - if (typeof value !== 'string') { - wrongTypeOutputNames.push(name); - continue; + if (!empty(misplacedOutputNames)) { + push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } - /* - if (!value.startsWith('#')) { - notPrivateOutputNames.push(name); - continue; + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + push(new Error(`${name}: Expected string, got ${type}`)); } - */ - } - - if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); - } - - for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); - } - - /* - for (const name of notPrivateOutputNames) { - const into = providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - */ - - outputOptionsAggregate.close(); + }); Object.assign(outputOptions, providedOptions); return instantiatedTemplate; -- cgit 1.3.0-6-gf8a5 From 1e09cfe3fcaa3f6e020e50ce49ea77c254b04dfd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 11:51:16 -0300 Subject: data: reuse validateInputValue for static inputs --- src/data/things/composite.js | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26be4a67..e58b6524 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,7 +588,6 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { @@ -602,7 +601,6 @@ export function templateCompositeFrom(description) { } const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); @@ -621,14 +619,12 @@ export function templateCompositeFrom(description) { } } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); - } + if (tokenShape === 'input.value') { + try { + validateInputValue(tokenValue, description.inputs[name]); + } catch (error) { + error.message = `${name}: ${error.message}`; + validateFailedErrors.push(error); } } } @@ -649,19 +645,14 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - push(error); - } - for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); } + + for (const error of validateFailedErrors) { + push(error); + } }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -1211,7 +1202,7 @@ export function compositeFrom(description) { } }); - withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + withAggregate({message: `Errors in input values provided to ${compositionName}`}, ({push}) => { for (const {dynamic, name, value, description} of stitchArrays({ dynamic: inputsMayBeDynamicValue, name: inputNames, @@ -1223,7 +1214,7 @@ export function compositeFrom(description) { validateInputValue(value, description); } catch (error) { error.message = `${name}: ${error.message}`; - throw error; + push(error); } } }); -- cgit 1.3.0-6-gf8a5 From d719eff73be9b18a3c83b984e68469c3be91457c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 13:19:52 -0300 Subject: data: compositeFrom: validate static token shapes for normal input --- src/data/things/composite.js | 49 +++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e58b6524..de6827c6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -576,7 +576,7 @@ export function templateCompositeFrom(description) { expectedInputNames .filter(name => !providedInputNames.includes(name)) .filter(name => { - const inputDescription = description.inputs[name].value; + const inputDescription = getInputTokenValue(description.inputs[name]); if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; @@ -587,6 +587,7 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; + const expectedValueProvidingTokenInputNames = []; const validateFailedErrors = []; @@ -605,18 +606,33 @@ export function templateCompositeFrom(description) { const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; - } - } + switch (descriptionShape) { + case'input.staticValue': + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + break; - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; - } + case 'input.staticDependency': + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + break; + + case 'input': + if (typeof value !== 'string' && ![ + 'input', + 'input.value', + 'input.dependency', + 'input.myself', + 'input.updateValue', + ].includes(tokenShape)) { + expectedValueProvidingTokenInputNames.push(name); + continue; + } + break; } if (tokenShape === 'input.value') { @@ -645,6 +661,15 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } + for (const name of expectedValueProvidingTokenInputNames) { + const shapeOrType = + (isInputToken(inputOptions[name]) + ? getInputTokenShape(inputOptions[name]) + : typeof inputOptions[name]); + + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 518647f8b80ffda6d502b1a75656da7f2ae4b9d3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:00:18 -0300 Subject: data: templateCompositeFrom: improve error message consistency --- src/data/things/composite.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index de6827c6..33f49e9b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + typeAppearance, unique, withAggregate, } from '#sugar'; @@ -381,7 +382,9 @@ input.staticDependency = _valueIntoToken('input.staticDependency'); input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { - if (typeof token === 'object') { + if (token === null) { + return false; + } else if (typeof token === 'object') { return token.symbol === Symbol.for('hsmusic.composite.input'); } else if (typeof token === 'symbol') { return token.description.startsWith('hsmusic.composite.input'); @@ -653,26 +656,29 @@ export function templateCompositeFrom(description) { push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } - if (!empty(expectedStaticDependencyInputNames)) { - push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + const inputAppearance = name => + (isInputToken(inputOptions[name]) + ? `${getInputTokenShape(inputOptions[name])}() call` + : `dependency name`); + + for (const name of expectedStaticDependencyInputNames) { + const appearance = inputAppearance(name); + push(new Error(`${name}: Expected dependency name, got ${appearance}`)); } - if (!empty(expectedStaticValueInputNames)) { - push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + for (const name of expectedStaticValueInputNames) { + const appearance = inputAppearance(name) + push(new Error(`${name}: Expected input.value() call, got ${appearance}`)); } for (const name of expectedValueProvidingTokenInputNames) { - const shapeOrType = - (isInputToken(inputOptions[name]) - ? getInputTokenShape(inputOptions[name]) - : typeof inputOptions[name]); - - push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + const appearance = getInputTokenShape(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${appearance}`)); } for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - push(new Error(`${name}: Expected string or input() call, got ${type}`)); + const type = typeAppearance(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or input() call, got ${type}`)); } for (const error of validateFailedErrors) { -- cgit 1.3.0-6-gf8a5 From 411c053dc4b314b2bc0d58d3899fd796ad0054c2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:11:02 -0300 Subject: data, util: use typeAppearance in more places --- src/data/things/composite.js | 30 +++++++++++++++--------------- src/data/things/validators.js | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 33f49e9b..b6009525 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -395,7 +395,7 @@ function isInputToken(token) { function getInputTokenShape(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -407,7 +407,7 @@ function getInputTokenShape(token) { function getInputTokenValue(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -464,8 +464,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${value}` - : `Expected value, got ${value}`)); + ? `Expected ${type}, got ${typeAppearance(value)}` + : `Expected value, got ${typeAppearance(value)}`)); } } @@ -478,7 +478,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeofValue}`); + throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); } } @@ -503,11 +503,11 @@ export function templateCompositeFrom(description) { validateInputs: if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + if ( + Array.isArray(description.inputs) || + typeof description.inputs !== 'object' + ) { + push(new Error(`Expected inputs to be object, got ${typeAppearance(description.inputs)}`)); break validateInputs; } @@ -543,7 +543,7 @@ export function templateCompositeFrom(description) { !Array.isArray(description.outputs) && typeof description.outputs !== 'function' ) { - push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + push(new Error(`Expected outputs to be array or function, got ${typeAppearance(description.outputs)}`)); break validateOutputs; } @@ -552,7 +552,7 @@ export function templateCompositeFrom(description) { description.outputs, decorateErrorWithIndex(value => { if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) + throw new Error(`${value}: Expected string, got ${typeAppearance(value)}`) } else if (!value.startsWith('#')) { throw new Error(`${value}: Expected "#" at start`); } @@ -733,8 +733,8 @@ export function templateCompositeFrom(description) { } for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - push(new Error(`${name}: Expected string, got ${type}`)); + const appearance = typeAppearance(providedOptions[name]); + push(new Error(`${name}: Expected string, got ${appearance}`)); } }); @@ -865,7 +865,7 @@ export function compositeFrom(description) { if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof description.steps}` + + `Expected steps to be array, got ${typeAppearance(description.steps)}` + (annotation ? ` (${annotation})` : '')); } diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 048f7ebb..ba62fb84 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,7 +1,7 @@ import {inspect as nodeInspect} from 'node:util'; import {colors, ENABLE_COLOR} from '#cli'; -import {empty, withAggregate} from '#sugar'; +import {empty, typeAppearance, withAggregate} from '#sugar'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -15,7 +15,7 @@ export function a(noun) { export function isType(value, type) { if (typeof value !== type) - throw new TypeError(`Expected ${a(type)}, got ${typeof value}`); + throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`); return true; } @@ -132,7 +132,7 @@ export function isObject(value) { export function isArray(value) { if (typeof value !== 'object' || value === null || !Array.isArray(value)) - throw new TypeError(`Expected an array, got ${value}`); + throw new TypeError(`Expected an array, got ${typeAppearance(value)}`); return true; } -- cgit 1.3.0-6-gf8a5 From f7376bb5eb2671de7242872ec0c4615b5e244aba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:12:56 -0300 Subject: data: misc minor fixes --- src/data/things/composite.js | 6 +----- src/data/things/homepage-layout.js | 1 - src/data/things/thing.js | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b6009525..eb93bd7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -976,8 +976,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; - const exposeDependencies = new Set(); - // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. const stepsExpose = @@ -1101,7 +1099,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, - expose: stepExposeDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -1110,7 +1107,6 @@ export function compositeFrom(description) { for (let i = 0; i < stepEntries.length; i++) { const { step, - expose, stepComposes, stepComputes, stepTransforms, @@ -2046,7 +2042,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ '#entries', ], - compute: ({ + compute: (continuation, { [input.staticDependency('object')]: object, [input.staticValue('properties')]: properties, [input.staticValue('prefix')]: prefix, diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bade280c..bcf99e80 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,7 +1,6 @@ import find from '#find'; import { - compositeFrom, exposeDependency, input, } from '#composite'; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 290be59b..f1302e17 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -456,7 +456,7 @@ export function inputWikiData({ allowMixedTypes = false, } = {}) { return input({ - validate: validateWikiData(referenceType), + validate: validateWikiData({referenceType, allowMixedTypes}), acceptsNull: true, }); } -- cgit 1.3.0-6-gf8a5 From ea02f6453f697d1e9fc6cfef2cdcf454c3f4286e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 29 Sep 2023 10:09:20 -0300 Subject: data: fix & tidy dynamic outputs in utilities --- src/data/things/composite.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index eb93bd7c..7a3a8319 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1938,15 +1938,12 @@ export const withPropertyFromObject = templateCompositeFrom({ outputs: ({ [input.staticDependency('object')]: object, [input.staticValue('property')]: property, - }) => { - return [ - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - ]; - }, + }) => + (object && property + ? (object.startsWith('#') + ? [`${object}.${property}`] + : [`#${object}.${property}`]) + : ['#value']), steps: () => [ { @@ -2018,7 +2015,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ : object ? `${object}.${property}` : `#object.${property}`)) - : '#object'), + : ['#object']), steps: () => [ { @@ -2135,7 +2132,7 @@ export const withPropertiesFromList = templateCompositeFrom({ : list ? `${list}.${property}` : `#list.${property}`)) - : '#lists'), + : ['#lists']), steps: () => [ { -- cgit 1.3.0-6-gf8a5 From e4dc2be4c12a5578bfb5d5945a592907aed1cb4f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 29 Sep 2023 10:36:59 -0300 Subject: data, test: type validation message adjustments --- src/data/things/composite.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a3a8319..c03f8833 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,6 +4,7 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + a, is, isString, isWholeNumber, @@ -464,8 +465,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${typeAppearance(value)}` - : `Expected value, got ${typeAppearance(value)}`)); + ? `Expected ${a(type)}, got ${typeAppearance(value)}` + : `Expected a value, got ${typeAppearance(value)}`)); } } @@ -478,7 +479,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); + throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`); } } @@ -1997,6 +1998,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ object: input({type: 'object', acceptsNull: true}), properties: input({ + type: 'array', validate: validateArrayItems(isString), }), -- cgit 1.3.0-6-gf8a5 From 6eaa070e5c036ba8cd45f79c16dc2732b40ea480 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 30 Sep 2023 09:14:29 -0300 Subject: data, util: hsmusic.sugar.index -> hsmusic.decorate.indexInSourceArray --- src/data/things/validators.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index ba62fb84..bdb22058 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -174,7 +174,8 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${colors.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)}) ${error.message}`; + error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index; throw error; } }; -- cgit 1.3.0-6-gf8a5 From ab7591e45e7e31b4e2c0e2f81e224672145993fa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:01:21 -0300 Subject: data, test: refactor utilities into own file Primarily for more precies test coverage mapping, but also to make navigation a bit easier and consolidate complex functions with lots of imports out of the same space as other, more simple or otherwise specialized files. --- .../control-flow/exitWithoutDependency.js | 35 + .../control-flow/exitWithoutUpdateValue.js | 24 + src/data/composite/control-flow/exposeConstant.js | 26 + .../composite/control-flow/exposeDependency.js | 28 + .../control-flow/exposeDependencyOrContinue.js | 34 + .../control-flow/exposeUpdateValueOrContinue.js | 40 ++ src/data/composite/control-flow/index.js | 9 + .../control-flow/inputAvailabilityCheckMode.js | 9 + .../control-flow/raiseOutputWithoutDependency.js | 39 ++ .../control-flow/raiseOutputWithoutUpdateValue.js | 47 ++ .../control-flow/withResultOfAvailabilityCheck.js | 66 ++ src/data/composite/data/excludeFromList.js | 56 ++ src/data/composite/data/fillMissingListItems.js | 51 ++ src/data/composite/data/index.js | 8 + src/data/composite/data/withFlattenedList.js | 47 ++ src/data/composite/data/withPropertiesFromList.js | 92 +++ .../composite/data/withPropertiesFromObject.js | 87 +++ src/data/composite/data/withPropertyFromList.js | 56 ++ src/data/composite/data/withPropertyFromObject.js | 69 ++ src/data/composite/data/withUnflattenedList.js | 62 ++ src/data/composite/things/album/index.js | 2 + .../composite/things/album/withTrackSections.js | 119 ++++ src/data/composite/things/album/withTracks.js | 51 ++ .../things/track/exitWithoutUniqueCoverArt.js | 26 + src/data/composite/things/track/index.js | 9 + .../things/track/inheritFromOriginalRelease.js | 43 ++ .../things/track/trackReverseReferenceList.js | 38 ++ src/data/composite/things/track/withAlbum.js | 57 ++ .../things/track/withAlwaysReferenceByDirectory.js | 52 ++ .../things/track/withContainingTrackSection.js | 63 ++ .../things/track/withHasUniqueCoverArt.js | 61 ++ .../composite/things/track/withOriginalRelease.js | 59 ++ .../composite/things/track/withOtherReleases.js | 40 ++ .../things/track/withPropertyFromAlbum.js | 49 ++ .../composite/wiki-data/exitWithoutContribs.js | 47 ++ src/data/composite/wiki-data/index.js | 7 + src/data/composite/wiki-data/inputThingClass.js | 23 + src/data/composite/wiki-data/inputWikiData.js | 17 + .../composite/wiki-data/withResolvedContribs.js | 77 +++ .../composite/wiki-data/withResolvedReference.js | 73 +++ .../wiki-data/withResolvedReferenceList.js | 101 +++ .../wiki-data/withReverseReferenceList.js | 40 ++ .../composite/wiki-properties/additionalFiles.js | 30 + src/data/composite/wiki-properties/color.js | 12 + src/data/composite/wiki-properties/commentary.js | 12 + .../wiki-properties/commentatorArtists.js | 55 ++ .../composite/wiki-properties/contribsPresent.js | 30 + .../composite/wiki-properties/contributionList.js | 35 + src/data/composite/wiki-properties/dimensions.js | 13 + src/data/composite/wiki-properties/directory.js | 23 + src/data/composite/wiki-properties/duration.js | 13 + .../composite/wiki-properties/externalFunction.js | 11 + .../composite/wiki-properties/fileExtension.js | 13 + src/data/composite/wiki-properties/flag.js | 19 + src/data/composite/wiki-properties/index.js | 20 + src/data/composite/wiki-properties/name.js | 11 + .../composite/wiki-properties/referenceList.js | 47 ++ .../wiki-properties/reverseReferenceList.js | 30 + src/data/composite/wiki-properties/simpleDate.js | 14 + src/data/composite/wiki-properties/simpleString.js | 14 + .../composite/wiki-properties/singleReference.js | 47 ++ src/data/composite/wiki-properties/urls.js | 14 + src/data/composite/wiki-properties/wikiData.js | 17 + src/data/things/album.js | 157 +---- src/data/things/art-tag.js | 10 +- src/data/things/artist.js | 6 +- src/data/things/composite.js | 727 +-------------------- src/data/things/flash.js | 6 +- src/data/things/group.js | 6 +- src/data/things/homepage-layout.js | 16 +- src/data/things/language.js | 9 +- src/data/things/news-entry.js | 6 +- src/data/things/static-page.js | 6 +- src/data/things/thing.js | 713 +------------------- src/data/things/track.js | 471 +------------ src/data/things/wiki-info.js | 6 +- 76 files changed, 2516 insertions(+), 2042 deletions(-) create mode 100644 src/data/composite/control-flow/exitWithoutDependency.js create mode 100644 src/data/composite/control-flow/exitWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/exposeConstant.js create mode 100644 src/data/composite/control-flow/exposeDependency.js create mode 100644 src/data/composite/control-flow/exposeDependencyOrContinue.js create mode 100644 src/data/composite/control-flow/exposeUpdateValueOrContinue.js create mode 100644 src/data/composite/control-flow/index.js create mode 100644 src/data/composite/control-flow/inputAvailabilityCheckMode.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutDependency.js create mode 100644 src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js create mode 100644 src/data/composite/control-flow/withResultOfAvailabilityCheck.js create mode 100644 src/data/composite/data/excludeFromList.js create mode 100644 src/data/composite/data/fillMissingListItems.js create mode 100644 src/data/composite/data/index.js create mode 100644 src/data/composite/data/withFlattenedList.js create mode 100644 src/data/composite/data/withPropertiesFromList.js create mode 100644 src/data/composite/data/withPropertiesFromObject.js create mode 100644 src/data/composite/data/withPropertyFromList.js create mode 100644 src/data/composite/data/withPropertyFromObject.js create mode 100644 src/data/composite/data/withUnflattenedList.js create mode 100644 src/data/composite/things/album/index.js create mode 100644 src/data/composite/things/album/withTrackSections.js create mode 100644 src/data/composite/things/album/withTracks.js create mode 100644 src/data/composite/things/track/exitWithoutUniqueCoverArt.js create mode 100644 src/data/composite/things/track/index.js create mode 100644 src/data/composite/things/track/inheritFromOriginalRelease.js create mode 100644 src/data/composite/things/track/trackReverseReferenceList.js create mode 100644 src/data/composite/things/track/withAlbum.js create mode 100644 src/data/composite/things/track/withAlwaysReferenceByDirectory.js create mode 100644 src/data/composite/things/track/withContainingTrackSection.js create mode 100644 src/data/composite/things/track/withHasUniqueCoverArt.js create mode 100644 src/data/composite/things/track/withOriginalRelease.js create mode 100644 src/data/composite/things/track/withOtherReleases.js create mode 100644 src/data/composite/things/track/withPropertyFromAlbum.js create mode 100644 src/data/composite/wiki-data/exitWithoutContribs.js create mode 100644 src/data/composite/wiki-data/index.js create mode 100644 src/data/composite/wiki-data/inputThingClass.js create mode 100644 src/data/composite/wiki-data/inputWikiData.js create mode 100644 src/data/composite/wiki-data/withResolvedContribs.js create mode 100644 src/data/composite/wiki-data/withResolvedReference.js create mode 100644 src/data/composite/wiki-data/withResolvedReferenceList.js create mode 100644 src/data/composite/wiki-data/withReverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/additionalFiles.js create mode 100644 src/data/composite/wiki-properties/color.js create mode 100644 src/data/composite/wiki-properties/commentary.js create mode 100644 src/data/composite/wiki-properties/commentatorArtists.js create mode 100644 src/data/composite/wiki-properties/contribsPresent.js create mode 100644 src/data/composite/wiki-properties/contributionList.js create mode 100644 src/data/composite/wiki-properties/dimensions.js create mode 100644 src/data/composite/wiki-properties/directory.js create mode 100644 src/data/composite/wiki-properties/duration.js create mode 100644 src/data/composite/wiki-properties/externalFunction.js create mode 100644 src/data/composite/wiki-properties/fileExtension.js create mode 100644 src/data/composite/wiki-properties/flag.js create mode 100644 src/data/composite/wiki-properties/index.js create mode 100644 src/data/composite/wiki-properties/name.js create mode 100644 src/data/composite/wiki-properties/referenceList.js create mode 100644 src/data/composite/wiki-properties/reverseReferenceList.js create mode 100644 src/data/composite/wiki-properties/simpleDate.js create mode 100644 src/data/composite/wiki-properties/simpleString.js create mode 100644 src/data/composite/wiki-properties/singleReference.js create mode 100644 src/data/composite/wiki-properties/urls.js create mode 100644 src/data/composite/wiki-properties/wikiData.js (limited to 'src/data') diff --git a/src/data/composite/control-flow/exitWithoutDependency.js b/src/data/composite/control-flow/exitWithoutDependency.js new file mode 100644 index 00000000..c660a7ef --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutDependency.js @@ -0,0 +1,35 @@ +// Early exits if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutDependency`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/control-flow/exitWithoutUpdateValue.js b/src/data/composite/control-flow/exitWithoutUpdateValue.js new file mode 100644 index 00000000..244b3233 --- /dev/null +++ b/src/data/composite/control-flow/exitWithoutUpdateValue.js @@ -0,0 +1,24 @@ +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import exitWithoutDependency from './exitWithoutDependency.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/control-flow/exposeConstant.js b/src/data/composite/control-flow/exposeConstant.js new file mode 100644 index 00000000..e0435478 --- /dev/null +++ b/src/data/composite/control-flow/exposeConstant.js @@ -0,0 +1,26 @@ +// Exposes a constant value exactly as it is; like exposeDependency, this +// is typically the base of a composition serving as a particular property +// descriptor. It generally follows steps which will conditionally early +// exit with some other value, with the exposeConstant base serving as the +// fallback default value. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeConstant`, + + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependency.js b/src/data/composite/control-flow/exposeDependency.js new file mode 100644 index 00000000..3aa3d03a --- /dev/null +++ b/src/data/composite/control-flow/exposeDependency.js @@ -0,0 +1,28 @@ +// Exposes a dependency exactly as it is; this is typically the base of a +// composition which was created to serve as one property's descriptor. +// +// Please note that this *doesn't* verify that the dependency exists, so +// if you provide the wrong name or it hasn't been set by a previous +// compositional step, the property will be exposed as undefined instead +// of null. + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `exposeDependency`, + + compose: false, + + inputs: { + dependency: input.staticDependency({acceptsNull: true}), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeDependencyOrContinue.js b/src/data/composite/control-flow/exposeDependencyOrContinue.js new file mode 100644 index 00000000..0f7f223e --- /dev/null +++ b/src/data/composite/control-flow/exposeDependencyOrContinue.js @@ -0,0 +1,34 @@ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('dependency')], + compute: (continuation, { + ['#availability']: availability, + [input('dependency')]: dependency, + }) => + (availability + ? continuation.exit(dependency) + : continuation()), + }, + ], +}); diff --git a/src/data/composite/control-flow/exposeUpdateValueOrContinue.js b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js new file mode 100644 index 00000000..1f94b332 --- /dev/null +++ b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js @@ -0,0 +1,40 @@ +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. +// +// See withResultOfAvailabilityCheck for {mode} options. +// +// Provide {validate} here to conveniently set a custom validation check +// for this property's update value. +// + +import {input, templateCompositeFrom} from '#composite'; + +import exposeDependencyOrContinue from './exposeDependencyOrContinue.js'; +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `exposeUpdateValueOrContinue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + + validate: input({ + type: 'function', + defaultValue: null, + }), + }, + + update: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), + + steps: () => [ + exposeDependencyOrContinue({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js new file mode 100644 index 00000000..dfc53db7 --- /dev/null +++ b/src/data/composite/control-flow/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutDependency} from './exitWithoutDependency.js'; +export {default as exitWithoutUpdateValue} from './exitWithoutUpdateValue.js'; +export {default as exposeConstant} from './exposeConstant.js'; +export {default as exposeDependency} from './exposeDependency.js'; +export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js'; +export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js'; +export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js'; +export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js'; +export {default as withResultOfAvailabilityCheck} from './withResultOfAvailabilityCheck.js'; diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js new file mode 100644 index 00000000..d74a1149 --- /dev/null +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -0,0 +1,9 @@ +import {input} from '#composite'; +import {is} from '#validators'; + +export default function inputAvailabilityCheckMode() { + return input({ + validate: is('null', 'empty', 'falsy'), + defaultValue: 'null', + }); +} diff --git a/src/data/composite/control-flow/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js new file mode 100644 index 00000000..03d8036a --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js @@ -0,0 +1,39 @@ +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `raiseOutputWithoutDependency`, + + inputs: { + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + output: input.staticValue({defaultValue: {}}), + }, + + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), + }, + ], +}); diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js new file mode 100644 index 00000000..3c39f5ba --- /dev/null +++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js @@ -0,0 +1,47 @@ +// Raises if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `raiseOutputWithoutUpdateValue`, + + inputs: { + mode: inputAvailabilityCheckMode(), + output: input.staticValue({defaultValue: {}}), + }, + + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input.updateValue(), + mode: input('mode'), + }), + + // TODO: A bit of a kludge, below. Other "do something with the update + // value" type functions can get by pretty much just passing that value + // as an input (input.updateValue()) into the corresponding "do something + // with a dependency/arbitrary value" function. But we can't do that here, + // because the special behavior, raiseOutputAbove(), only works to raise + // output above the composition it's *directly* nested in. Other languages + // have a throw/catch system that might serve as inspiration for something + // better here. + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), + }, + ], +}); diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js new file mode 100644 index 00000000..bcbd0b37 --- /dev/null +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -0,0 +1,66 @@ +// Checks the availability of a dependency and provides the result to later +// steps under '#availability' (by default). This is mainly intended for use +// by the more specific utilities, which you should consider using instead. +// +// Customize {mode} to select one of these modes, or default to 'null': +// +// * 'null': Check that the value isn't null (and not undefined either). +// * 'empty': Check that the value is neither null, undefined, nor an empty +// array. +// * 'falsy': Check that the value isn't false when treated as a boolean +// (nor an empty array). Keep in mind this will also be false +// for values like zero and the empty string! +// +// See also: +// - exitWithoutDependency +// - exitWithoutUpdateValue +// - exposeDependencyOrContinue +// - exposeUpdateValueOrContinue +// - raiseOutputWithoutDependency +// - raiseOutputWithoutUpdateValue +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +export default templateCompositeFrom({ + annotation: `withResultOfAvailabilityCheck`, + + inputs: { + from: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + }, + + outputs: ['#availability'], + + steps: () => [ + { + dependencies: [input('from'), input('mode')], + + compute: (continuation, { + [input('from')]: value, + [input('mode')]: mode, + }) => { + let availability; + + switch (mode) { + case 'null': + availability = value !== undefined && value !== null; + break; + + case 'empty': + availability = value !== undefined && !empty(value); + break; + + case 'falsy': + availability = !!value && (!Array.isArray(value) || !empty(value)); + break; + } + + return continuation({'#availability': availability}); + }, + }, + ], +}); diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js new file mode 100644 index 00000000..718f2294 --- /dev/null +++ b/src/data/composite/data/excludeFromList.js @@ -0,0 +1,56 @@ +// Filters particular values out of a list. Note that this will always +// completely skip over null, but can be used to filter out any other +// primitive or object value. +// +// See also: +// - fillMissingListItems +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +export default templateCompositeFrom({ + annotation: `excludeFromList`, + + inputs: { + list: input(), + + item: input({defaultValue: null}), + items: input({type: 'array', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + + steps: () => [ + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('item'), + input('items'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: listName, + [input('list')]: listContents, + [input('item')]: excludeItem, + [input('items')]: excludeItems, + }) => continuation({ + [listName ?? '#list']: + listContents.filter(item => { + if (excludeItem !== null && item === excludeItem) return false; + if (!empty(excludeItems) && excludeItems.includes(item)) return false; + return true; + }), + }), + }, + ], +}); diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js new file mode 100644 index 00000000..c06eceda --- /dev/null +++ b/src/data/composite/data/fillMissingListItems.js @@ -0,0 +1,51 @@ +// Replaces items of a list, which are null or undefined, with some fallback +// value. By default, this replaces the passed dependency. +// +// See also: +// - excludeFromList +// +// More list utilities: +// - withFlattenedList +// - withPropertyFromList +// - withPropertiesFromList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `fillMissingListItems`, + + inputs: { + list: input({type: 'array'}), + fill: input({acceptsNull: true}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + + steps: () => [ + { + dependencies: [input('list'), input('fill')], + compute: (continuation, { + [input('list')]: list, + [input('fill')]: fill, + }) => continuation({ + ['#filled']: + list.map(item => item ?? fill), + }), + }, + + { + dependencies: [input.staticDependency('list'), '#filled'], + compute: (continuation, { + [input.staticDependency('list')]: list, + ['#filled']: filled, + }) => continuation({ + [list ?? '#list']: + filled, + }), + }, + ], +}); diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js new file mode 100644 index 00000000..ecd05129 --- /dev/null +++ b/src/data/composite/data/index.js @@ -0,0 +1,8 @@ +export {default as excludeFromList} from './excludeFromList.js'; +export {default as fillMissingListItems} from './fillMissingListItems.js'; +export {default as withFlattenedList} from './withFlattenedList.js'; +export {default as withPropertiesFromList} from './withPropertiesFromList.js'; +export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; +export {default as withPropertyFromList} from './withPropertyFromList.js'; +export {default as withPropertyFromObject} from './withPropertyFromObject.js'; +export {default as withUnflattenedList} from './withUnflattenedList.js'; diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js new file mode 100644 index 00000000..b08edb4e --- /dev/null +++ b/src/data/composite/data/withFlattenedList.js @@ -0,0 +1,47 @@ +// Flattens an array with one level of nested arrays, providing as dependencies +// both the flattened array as well as the original starting indices of each +// successive source array. +// +// See also: +// - withFlattenedList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withPropertyFromList +// - withPropertiesFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withFlattenedList`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: ['#flattenedList', '#flattenedIndices'], + + steps: () => [ + { + dependencies: [input('list')], + compute(continuation, { + [input('list')]: sourceList, + }) { + const flattenedList = sourceList.flat(); + const indices = []; + let lastEndIndex = 0; + for (const {length} of sourceList) { + indices.push(lastEndIndex); + lastEndIndex += length; + } + + return continuation({ + ['#flattenedList']: flattenedList, + ['#flattenedIndices']: indices, + }); + }, + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js new file mode 100644 index 00000000..76ba696c --- /dev/null +++ b/src/data/composite/data/withPropertiesFromList.js @@ -0,0 +1,92 @@ +// Gets the listed properties from each of a list of objects, providing lists +// of property values each into a dependency prefixed with the same name as the +// list (by default). +// +// Like withPropertyFromList, this doesn't alter indices. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withPropertiesFromList`, + + inputs: { + list: input({type: 'array'}), + + properties: input({ + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`)) + : ['#lists']), + + steps: () => [ + { + dependencies: [input('list'), input('properties')], + compute: (continuation, { + [input('list')]: list, + [input('properties')]: properties, + }) => continuation({ + ['#lists']: + Object.fromEntries( + properties.map(property => [ + property, + list.map(item => item[property] ?? null), + ])), + }), + }, + + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#lists', + ], + + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#lists']: lists, + }) => + (properties + ? continuation( + Object.fromEntries( + properties.map(property => [ + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`), + lists[property], + ]))) + : continuation({'#lists': lists})), + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromObject.js b/src/data/composite/data/withPropertiesFromObject.js new file mode 100644 index 00000000..21726b58 --- /dev/null +++ b/src/data/composite/data/withPropertiesFromObject.js @@ -0,0 +1,87 @@ +// Gets the listed properties from some object, providing each property's value +// as a dependency prefixed with the same name as the object (by default). +// If the object itself is null, all provided dependencies will be null; +// if it's missing only select properties, those will be provided as null. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// + +import {input, templateCompositeFrom} from '#composite'; +import {isString, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withPropertiesFromObject`, + + inputs: { + object: input({type: 'object', acceptsNull: true}), + + properties: input({ + type: 'array', + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`)) + : ['#object']), + + steps: () => [ + { + dependencies: [input('object'), input('properties')], + compute: (continuation, { + [input('object')]: object, + [input('properties')]: properties, + }) => continuation({ + ['#entries']: + (object === null + ? properties.map(property => [property, null]) + : properties.map(property => [property, object[property]])), + }), + }, + + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#entries', + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#entries']: entries, + }) => + (properties + ? continuation( + Object.fromEntries( + entries.map(([property, value]) => [ + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`), + value ?? null, + ]))) + : continuation({ + ['#object']: + Object.fromEntries(entries), + })), + }, + ], +}); diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js new file mode 100644 index 00000000..3ce05fdf --- /dev/null +++ b/src/data/composite/data/withPropertyFromList.js @@ -0,0 +1,56 @@ +// Gets a property from each of a list of objects (in a dependency) and +// provides the results. +// +// This doesn't alter any list indices, so positions which were null in the +// original list are kept null here. Objects which don't have the specified +// property are retained in-place as null. +// +// See also: +// - withPropertiesFromList +// - withPropertyFromObject +// +// More list utilities: +// - excludeFromList +// - fillMissingListItems +// - withFlattenedList +// - withUnflattenedList +// + +import {empty} from '#sugar'; + +// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL +export default function({ + list, + property, + into = null, +}) { + into ??= + (list.startsWith('#') + ? `${list}.${property}` + : `#${list}.${property}`); + + return { + annotation: `withPropertyFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {property}, + + compute(continuation, {list, '#options': {property}}) { + if (list === undefined || empty(list)) { + return continuation({into: []}); + } + + return continuation({ + into: + list.map(item => + (item === null || item === undefined + ? null + : item[property] ?? null)), + }); + }, + }, + }; +} diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js new file mode 100644 index 00000000..b31bab15 --- /dev/null +++ b/src/data/composite/data/withPropertyFromObject.js @@ -0,0 +1,69 @@ +// Gets a property of some object (in a dependency) and provides that value. +// If the object itself is null, or the object doesn't have the listed property, +// the provided dependency will also be null. +// +// See also: +// - withPropertiesFromObject +// - withPropertyFromList +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withPropertyFromObject`, + + inputs: { + object: input({type: 'object', acceptsNull: true}), + property: input({type: 'string'}), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => + (object && property + ? (object.startsWith('#') + ? [`${object}.${property}`] + : [`#${object}.${property}`]) + : ['#value']), + + steps: () => [ + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => continuation({ + '#output': + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + }), + }, + + { + dependencies: [ + '#output', + input('object'), + input('property'), + ], + + compute: (continuation, { + ['#output']: output, + [input('object')]: object, + [input('property')]: property, + }) => continuation({ + [output]: + (object === null + ? null + : object[property] ?? null), + }), + }, + ], +}); diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js new file mode 100644 index 00000000..3cfc247b --- /dev/null +++ b/src/data/composite/data/withUnflattenedList.js @@ -0,0 +1,62 @@ +// After mapping the contents of a flattened array in-place (being careful to +// retain the original indices by replacing unmatched results with null instead +// of filtering them out), this function allows for recombining them. It will +// filter out null and undefined items by default (pass {filter: false} to +// disable this). + +import {input, templateCompositeFrom} from '#composite'; +import {isWholeNumber, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withUnflattenedList`, + + inputs: { + list: input({ + type: 'array', + defaultDependency: '#flattenedList', + }), + + indices: input({ + validate: validateArrayItems(isWholeNumber), + defaultDependency: '#flattenedIndices', + }), + + filter: input({ + type: 'boolean', + defaultValue: true, + }), + }, + + outputs: ['#unflattenedList'], + + steps: () => [ + { + dependencies: [input('list'), input('indices'), input('filter')], + compute(continuation, { + [input('list')]: list, + [input('indices')]: indices, + [input('filter')]: filter, + }) { + const unflattenedList = []; + + for (let i = 0; i < indices.length; i++) { + const startIndex = indices[i]; + const endIndex = + (i === indices.length - 1 + ? list.length + : indices[i + 1]); + + const values = list.slice(startIndex, endIndex); + unflattenedList.push( + (filter + ? values.filter(value => value !== null && value !== undefined) + : values)); + } + + return continuation({ + ['#unflattenedList']: unflattenedList, + }); + }, + }, + ], +}); diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js new file mode 100644 index 00000000..8139f10e --- /dev/null +++ b/src/data/composite/things/album/index.js @@ -0,0 +1,2 @@ +export {default as withTracks} from './withTracks.js'; +export {default as withTrackSections} from './withTrackSections.js'; diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js new file mode 100644 index 00000000..c99b94d2 --- /dev/null +++ b/src/data/composite/things/album/withTrackSections.js @@ -0,0 +1,119 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {empty, stitchArrays} from '#sugar'; +import {isTrackSectionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import {exitWithoutDependency, exitWithoutUpdateValue} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +import { + fillMissingListItems, + withFlattenedList, + withPropertiesFromList, + withUnflattenedList, +} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withTrackSections`, + + outputs: ['#trackSections'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + exitWithoutUpdateValue({ + mode: input.value('empty'), + value: input.value([]), + }), + + // TODO: input.updateValue description down here is a kludge. + withPropertiesFromList({ + list: input.updateValue({ + validate: isTrackSectionList, + }), + prefix: input.value('#sections'), + properties: input.value([ + 'tracks', + 'dateOriginallyReleased', + 'isDefaultTrackSection', + 'color', + ]), + }), + + fillMissingListItems({ + list: '#sections.tracks', + fill: input.value([]), + }), + + fillMissingListItems({ + list: '#sections.isDefaultTrackSection', + fill: input.value(false), + }), + + fillMissingListItems({ + list: '#sections.color', + fill: input.dependency('color'), + }), + + withFlattenedList({ + list: '#sections.tracks', + }).outputs({ + ['#flattenedList']: '#trackRefs', + ['#flattenedIndices']: '#sections.startIndex', + }), + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + notFoundMode: input.value('null'), + find: input.value(find.track), + }).outputs({ + ['#resolvedReferenceList']: '#tracks', + }), + + withUnflattenedList({ + list: '#tracks', + indices: '#sections.startIndex', + }).outputs({ + ['#unflattenedList']: '#sections.tracks', + }), + + { + dependencies: [ + '#sections.tracks', + '#sections.color', + '#sections.dateOriginallyReleased', + '#sections.isDefaultTrackSection', + '#sections.startIndex', + ], + + compute: (continuation, { + '#sections.tracks': tracks, + '#sections.color': color, + '#sections.dateOriginallyReleased': dateOriginallyReleased, + '#sections.isDefaultTrackSection': isDefaultTrackSection, + '#sections.startIndex': startIndex, + }) => { + filterMultipleArrays( + tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks => !empty(tracks)); + + return continuation({ + ['#trackSections']: + stitchArrays({ + tracks, + color, + dateOriginallyReleased, + isDefaultTrackSection, + startIndex, + }), + }); + }, + }, + ], +}); diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js new file mode 100644 index 00000000..dcea6593 --- /dev/null +++ b/src/data/composite/things/album/withTracks.js @@ -0,0 +1,51 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; + +import {exitWithoutDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withTracks`, + + outputs: ['#tracks'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: 'trackSections', + mode: input.value('empty'), + output: input.value({ + ['#tracks']: [], + }), + }), + + { + dependencies: ['trackSections'], + compute: (continuation, {trackSections}) => + continuation({ + '#trackRefs': trackSections + .flatMap(section => section.tracks ?? []), + }), + }, + + withResolvedReferenceList({ + list: '#trackRefs', + data: 'trackData', + find: input.value(find.track), + }), + + { + dependencies: ['#resolvedReferenceList'], + compute: (continuation, { + ['#resolvedReferenceList']: resolvedReferenceList, + }) => continuation({ + ['#tracks']: resolvedReferenceList, + }) + }, + ], +}); diff --git a/src/data/composite/things/track/exitWithoutUniqueCoverArt.js b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js new file mode 100644 index 00000000..f47086d9 --- /dev/null +++ b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js @@ -0,0 +1,26 @@ +// Shorthand for checking if the track has unique cover art and exposing a +// fallback value if it isn't. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withHasUniqueCoverArt from './withHasUniqueCoverArt.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutUniqueCoverArt`, + + inputs: { + value: input({defaultValue: null}), + }, + + steps: () => [ + withHasUniqueCoverArt(), + + exitWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js new file mode 100644 index 00000000..3354b1c4 --- /dev/null +++ b/src/data/composite/things/track/index.js @@ -0,0 +1,9 @@ +export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; +export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js'; +export {default as trackReverseReferenceList} from './trackReverseReferenceList.js'; +export {default as withAlbum} from './withAlbum.js'; +export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js'; +export {default as withContainingTrackSection} from './withContainingTrackSection.js'; +export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; +export {default as withOtherReleases} from './withOtherReleases.js'; +export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js new file mode 100644 index 00000000..a9d57f86 --- /dev/null +++ b/src/data/composite/things/track/inheritFromOriginalRelease.js @@ -0,0 +1,43 @@ +// Early exits with a value inherited from the original release, if +// this track is a rerelease, and otherwise continues with no further +// dependencies provided. If allowOverride is true, then the continuation +// will also be called if the original release exposed the requested +// property as null. + +import {input, templateCompositeFrom} from '#composite'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `inheritFromOriginalRelease`, + + inputs: { + property: input({type: 'string'}), + allowOverride: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ + withOriginalRelease(), + + { + dependencies: [ + '#originalRelease', + input('property'), + input('allowOverride'), + ], + + compute: (continuation, { + ['#originalRelease']: originalRelease, + [input('property')]: originalProperty, + [input('allowOverride')]: allowOverride, + }) => { + if (!originalRelease) return continuation(); + + const value = originalRelease[originalProperty]; + if (allowOverride && value === null) return continuation(); + + return continuation.exit(value); + }, + }, + ], +}); diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js new file mode 100644 index 00000000..e7bfedf3 --- /dev/null +++ b/src/data/composite/things/track/trackReverseReferenceList.js @@ -0,0 +1,38 @@ +// Like a normal reverse reference list ("objects which reference this object +// under a specified property"), only excluding re-releases from the possible +// outputs. While it's useful to travel from a re-release to the tracks it +// references, re-releases aren't generally relevant from the perspective of +// the tracks *being* referenced. Apart from hiding re-releases from lists on +// the site, it also excludes keeps them from relational data processing, such +// as on the "Tracks - by Times Referenced" listing page. + +import {input, templateCompositeFrom} from '#composite'; +import {withReverseReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `trackReverseReferenceList`, + + compose: false, + + inputs: { + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: 'trackData', + list: input('list'), + }), + + { + flags: {expose: true}, + expose: { + dependencies: ['#reverseReferenceList'], + compute: ({ + ['#reverseReferenceList']: reverseReferenceList, + }) => + reverseReferenceList.filter(track => !track.originalReleaseTrack), + }, + }, + ], +}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js new file mode 100644 index 00000000..34845ab0 --- /dev/null +++ b/src/data/composite/things/track/withAlbum.js @@ -0,0 +1,57 @@ +// Gets the track's album. This will early exit if albumData is missing. +// By default, if there's no album whose list of tracks includes this track, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withAlbum`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#album'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'albumData', + mode: input.value('empty'), + output: input.value({ + ['#album']: null, + }), + }), + + { + dependencies: [input.myself(), 'albumData'], + compute: (continuation, { + [input.myself()]: track, + ['albumData']: albumData, + }) => + continuation({ + ['#album']: + albumData.find(album => album.tracks.includes(track)), + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({ + ['#album']: null, + }), + }), + + { + dependencies: ['#album'], + compute: (continuation, {'#album': album}) => + continuation.raiseOutput({'#album': album}), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js new file mode 100644 index 00000000..0aeac788 --- /dev/null +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -0,0 +1,52 @@ +// Controls how find.track works - it'll never be matched by a reference +// just to the track's name, which means you don't have to always reference +// some *other* (much more commonly referenced) track by directory instead +// of more naturally by name. + +import {input, templateCompositeFrom} from '#composite'; +import {isBoolean} from '#validators'; + +import {exitWithoutDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {excludeFromList, withPropertyFromObject} from '#composite/data'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withAlwaysReferenceByDirectory`, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + excludeFromList({ + list: 'trackData', + item: input.myself(), + }), + + withOriginalRelease({ + data: '#trackData', + }), + + exitWithoutDependency({ + dependency: '#originalRelease', + value: input.value(false), + }), + + withPropertyFromObject({ + object: '#originalRelease', + property: input.value('name'), + }), + + { + dependencies: ['name', '#originalRelease.name'], + compute: (continuation, { + name, + ['#originalRelease.name']: originalName, + }) => continuation({ + ['#alwaysReferenceByDirectory']: name === originalName, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js new file mode 100644 index 00000000..b2e5f2b3 --- /dev/null +++ b/src/data/composite/things/track/withContainingTrackSection.js @@ -0,0 +1,63 @@ +// Gets the track section containing this track from its album's track list. +// If notFoundMode is set to 'exit', this will early exit if the album can't be +// found or if none of its trackSections includes the track for some reason. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withContainingTrackSection`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#trackSection'], + + steps: () => [ + withPropertyFromAlbum({ + property: input.value('trackSections'), + notFoundMode: input('notFoundMode'), + }), + + { + dependencies: [ + input.myself(), + input('notFoundMode'), + '#album.trackSections', + ], + + compute(continuation, { + [input.myself()]: track, + [input('notFoundMode')]: notFoundMode, + ['#album.trackSections']: trackSections, + }) { + if (!trackSections) { + return continuation.raiseOutput({ + ['#trackSection']: null, + }); + } + + const trackSection = + trackSections.find(({tracks}) => tracks.includes(track)); + + if (trackSection) { + return continuation.raiseOutput({ + ['#trackSection']: trackSection, + }); + } else if (notFoundMode === 'exit') { + return continuation.exit(null); + } else { + return continuation.raiseOutput({ + ['#trackSection']: null, + }); + } + }, + }, + ], +}); diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js new file mode 100644 index 00000000..96078d5f --- /dev/null +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -0,0 +1,61 @@ +// Whether or not the track has "unique" cover artwork - a cover which is +// specifically associated with this track in particular, rather than with +// the track's album as a whole. This is typically used to select between +// displaying the track artwork and a fallback, such as the album artwork +// or a placeholder. (This property is named hasUniqueCoverArt instead of +// the usual hasCoverArt to emphasize that it does not inherit from the +// album.) + +import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; + +import {withResolvedContribs} from '#composite/wiki-data'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: 'withHasUniqueCoverArt', + + outputs: ['#hasUniqueCoverArt'], + + steps: () => [ + { + dependencies: ['disableUniqueCoverArt'], + compute: (continuation, {disableUniqueCoverArt}) => + (disableUniqueCoverArt + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: false, + }) + : continuation()), + }, + + withResolvedContribs({from: 'coverArtistContribs'}), + + { + dependencies: ['#resolvedContribs'], + compute: (continuation, { + ['#resolvedContribs']: contribsFromTrack, + }) => + (empty(contribsFromTrack) + ? continuation() + : continuation.raiseOutput({ + ['#hasUniqueCoverArt']: true, + })), + }, + + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + + { + dependencies: ['#album.trackCoverArtistContribs'], + compute: (continuation, { + ['#album.trackCoverArtistContribs']: contribsFromAlbum, + }) => + continuation.raiseOutput({ + ['#hasUniqueCoverArt']: + !empty(contribsFromAlbum), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withOriginalRelease.js new file mode 100644 index 00000000..d2ee39df --- /dev/null +++ b/src/data/composite/things/track/withOriginalRelease.js @@ -0,0 +1,59 @@ +// Just includes the original release of this track as a dependency. +// If this track isn't a rerelease, then it'll provide null, unless the +// {selfIfOriginal} option is set, in which case it'll provide this track +// itself. Note that this will early exit if the original release is +// specified by reference and that reference doesn't resolve to anything. +// Outputs to '#originalRelease' by default. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {validateWikiData} from '#validators'; + +import {withResolvedReference} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withOriginalRelease`, + + inputs: { + selfIfOriginal: input({type: 'boolean', defaultValue: false}), + + data: input({ + validate: validateWikiData({referenceType: 'track'}), + defaultDependency: 'trackData', + }), + }, + + outputs: ['#originalRelease'], + + steps: () => [ + withResolvedReference({ + ref: 'originalReleaseTrack', + data: input('data'), + find: input.value(find.track), + notFoundMode: input.value('exit'), + }).outputs({ + ['#resolvedReference']: '#originalRelease', + }), + + { + dependencies: [ + input.myself(), + input('selfIfOriginal'), + '#originalRelease', + ], + + compute: (continuation, { + [input.myself()]: track, + [input('selfIfOriginal')]: selfIfOriginal, + ['#originalRelease']: originalRelease, + }) => + continuation({ + ['#originalRelease']: + (originalRelease ?? + (selfIfOriginal + ? track + : null)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js new file mode 100644 index 00000000..84420cf8 --- /dev/null +++ b/src/data/composite/things/track/withOtherReleases.js @@ -0,0 +1,40 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withOriginalRelease from './withOriginalRelease.js'; + +export default templateCompositeFrom({ + annotation: `withOtherReleases`, + + outputs: ['#otherReleases'], + + steps: () => [ + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + }), + + withOriginalRelease({ + selfIfOriginal: input.value(true), + }), + + { + dependencies: [input.myself(), '#originalRelease', 'trackData'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['#originalRelease']: originalRelease, + trackData, + }) => continuation({ + ['#otherReleases']: + (originalRelease === thisTrack + ? [] + : [originalRelease]) + .concat(trackData.filter(track => + track !== originalRelease && + track !== thisTrack && + track.originalReleaseTrack === originalRelease)), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js new file mode 100644 index 00000000..b236a6e8 --- /dev/null +++ b/src/data/composite/things/track/withPropertyFromAlbum.js @@ -0,0 +1,49 @@ +// Gets a single property from this track's album, providing it as the same +// property name prefixed with '#album.' (by default). If the track's album +// isn't available, then by default, the property will be provided as null; +// set {notFoundMode: 'exit'} to early exit instead. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {withPropertyFromObject} from '#composite/data'; + +import withAlbum from './withAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withPropertyFromAlbum`, + + inputs: { + property: input.staticValue({type: 'string'}), + + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ({ + [input.staticValue('property')]: property, + }) => ['#album.' + property], + + steps: () => [ + withAlbum({ + notFoundMode: input('notFoundMode'), + }), + + withPropertyFromObject({ + object: '#album', + property: input('property'), + }), + + { + dependencies: ['#value', input.staticValue('property')], + compute: (continuation, { + ['#value']: value, + [input.staticValue('property')]: property, + }) => continuation({ + ['#album.' + property]: value, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js new file mode 100644 index 00000000..2c8219fc --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -0,0 +1,47 @@ +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), + + value: input({defaultValue: null}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + // TODO: Fairly certain exitWithoutDependency would be sufficient here. + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js new file mode 100644 index 00000000..1d0400fc --- /dev/null +++ b/src/data/composite/wiki-data/index.js @@ -0,0 +1,7 @@ +export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as inputThingClass} from './inputThingClass.js'; +export {default as inputWikiData} from './inputWikiData.js'; +export {default as withResolvedContribs} from './withResolvedContribs.js'; +export {default as withResolvedReference} from './withResolvedReference.js'; +export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; +export {default as withReverseReferenceList} from './withReverseReferenceList.js'; diff --git a/src/data/composite/wiki-data/inputThingClass.js b/src/data/composite/wiki-data/inputThingClass.js new file mode 100644 index 00000000..d70480e6 --- /dev/null +++ b/src/data/composite/wiki-data/inputThingClass.js @@ -0,0 +1,23 @@ +// Please note that this input, used in a variety of #composite/wiki-data +// utilities, is basically always a kludge. Any usage of it depends on +// referencing Thing class values defined outside of the #composite folder. + +import {input} from '#composite'; +import {isType} from '#validators'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default function inputThingClass() { + return input.staticValue({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }); +} diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js new file mode 100644 index 00000000..cf7a7c2c --- /dev/null +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -0,0 +1,17 @@ +import {input} from '#composite'; +import {validateWikiData} from '#validators'; + +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export default function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData({referenceType, allowMixedTypes}), + acceptsNull: true, + }); +} diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js new file mode 100644 index 00000000..eda24160 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -0,0 +1,77 @@ +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {stitchArrays} from '#sugar'; +import {is, isContributionList} from '#validators'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import { + withPropertiesFromList, +} from '#composite/data'; + +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + from: input({ + validate: isContributionList, + acceptsNull: true, + }), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedContribs'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedContribs']: [], + }), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + find: input.value(find.artist), + notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + ['#resolvedContribs']: stitchArrays({who, what}), + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js new file mode 100644 index 00000000..0fa5c554 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -0,0 +1,73 @@ +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + ref: input({type: 'string', acceptsNull: true}), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedReference'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({ + ['#resolvedReference']: null, + }), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute(continuation, { + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raiseOutput({ + ['#resolvedReference']: match ?? null, + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js new file mode 100644 index 00000000..1d39e5b2 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -0,0 +1,101 @@ +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). + +import {input, templateCompositeFrom} from '#composite'; +import {is, isString, validateArrayItems} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: ['#resolvedReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedReferenceList']: [], + }), + }), + + { + dependencies: [input('list'), input('data'), input('find')], + compute: (continuation, { + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, + + { + dependencies: ['#matches'], + compute: (continuation, {'#matches': matches}) => + (matches.every(match => match) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) + : continuation()), + }, + + { + dependencies: ['#matches', input('notFoundMode')], + compute(continuation, { + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); + + case 'null': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js new file mode 100644 index 00000000..113a6c40 --- /dev/null +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -0,0 +1,40 @@ +// Check out the info on reverseReferenceList! +// This is its composable form. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + outputs: ['#reverseReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + { + dependencies: [input.myself(), input('data'), input('list')], + + compute: (continuation, { + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, + }) => + continuation({ + ['#reverseReferenceList']: + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js new file mode 100644 index 00000000..6760527a --- /dev/null +++ b/src/data/composite/wiki-properties/additionalFiles.js @@ -0,0 +1,30 @@ +// This is a somewhat more involved data structure - it's for additional +// or "bonus" files associated with albums or tracks (or anything else). +// It's got this form: +// +// [ +// {title: 'Booklet', files: ['Booklet.pdf']}, +// { +// title: 'Wallpaper', +// description: 'Cool Wallpaper!', +// files: ['1440x900.png', '1920x1080.png'] +// }, +// {title: 'Alternate Covers', description: null, files: [...]}, +// ... +// ] +// + +import {isAdditionalFileList} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], + }, + }; +} diff --git a/src/data/composite/wiki-properties/color.js b/src/data/composite/wiki-properties/color.js new file mode 100644 index 00000000..1bc9888b --- /dev/null +++ b/src/data/composite/wiki-properties/color.js @@ -0,0 +1,12 @@ +// A color! This'll be some CSS-ready value. + +import {isColor} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js new file mode 100644 index 00000000..fbea9d5c --- /dev/null +++ b/src/data/composite/wiki-properties/commentary.js @@ -0,0 +1,12 @@ +// Artist commentary! Generally present on tracks and albums. + +import {isCommentary} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js new file mode 100644 index 00000000..52aeb868 --- /dev/null +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -0,0 +1,55 @@ +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {unique} from '#sugar'; + +import {exitWithoutDependency} from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), + + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), + }, + + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, + }, + ], +}); diff --git a/src/data/composite/wiki-properties/contribsPresent.js b/src/data/composite/wiki-properties/contribsPresent.js new file mode 100644 index 00000000..24f302a5 --- /dev/null +++ b/src/data/composite/wiki-properties/contribsPresent.js @@ -0,0 +1,30 @@ +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `contribsPresent`, + + compose: false, + + inputs: { + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, + }), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('contribs'), + mode: input.value('empty'), + }), + + exposeDependency({dependency: '#availability'}), + ], +}); diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js new file mode 100644 index 00000000..8fde2caa --- /dev/null +++ b/src/data/composite/wiki-properties/contributionList.js @@ -0,0 +1,35 @@ +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow'; +import {withResolvedContribs} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `contributionList`, + + compose: false, + + update: {validate: isContributionList}, + + steps: () => [ + withResolvedContribs({from: input.updateValue()}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: input.value([])}), + ], +}); diff --git a/src/data/composite/wiki-properties/dimensions.js b/src/data/composite/wiki-properties/dimensions.js new file mode 100644 index 00000000..57a01279 --- /dev/null +++ b/src/data/composite/wiki-properties/dimensions.js @@ -0,0 +1,13 @@ +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. + +import {isDimensions} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js new file mode 100644 index 00000000..0b2181c9 --- /dev/null +++ b/src/data/composite/wiki-properties/directory.js @@ -0,0 +1,23 @@ +// The all-encompassing "directory" property, used as the unique identifier for +// almost any data object. Also corresponds to a part of the URL which pages of +// such objects are visited at. + +import {isDirectory} from '#validators'; +import {getKebabCase} from '#wiki-data'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, + }, + }; +} diff --git a/src/data/composite/wiki-properties/duration.js b/src/data/composite/wiki-properties/duration.js new file mode 100644 index 00000000..827f282d --- /dev/null +++ b/src/data/composite/wiki-properties/duration.js @@ -0,0 +1,13 @@ +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. + +import {isDuration} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} diff --git a/src/data/composite/wiki-properties/externalFunction.js b/src/data/composite/wiki-properties/externalFunction.js new file mode 100644 index 00000000..c388da6c --- /dev/null +++ b/src/data/composite/wiki-properties/externalFunction.js @@ -0,0 +1,11 @@ +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} diff --git a/src/data/composite/wiki-properties/fileExtension.js b/src/data/composite/wiki-properties/fileExtension.js new file mode 100644 index 00000000..c926fa8b --- /dev/null +++ b/src/data/composite/wiki-properties/fileExtension.js @@ -0,0 +1,13 @@ +// A file extension! Or the default, if provided when calling this. + +import {isFileExtension} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} diff --git a/src/data/composite/wiki-properties/flag.js b/src/data/composite/wiki-properties/flag.js new file mode 100644 index 00000000..076e663f --- /dev/null +++ b/src/data/composite/wiki-properties/flag.js @@ -0,0 +1,19 @@ +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! + +import {isBoolean} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: The description is a lie. This defaults to false. Bad. + +export default function(defaultValue = false) { + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } + + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js new file mode 100644 index 00000000..2462b047 --- /dev/null +++ b/src/data/composite/wiki-properties/index.js @@ -0,0 +1,20 @@ +export {default as additionalFiles} from './additionalFiles.js'; +export {default as color} from './color.js'; +export {default as commentary} from './commentary.js'; +export {default as commentatorArtists} from './commentatorArtists.js'; +export {default as contribsPresent} from './contribsPresent.js'; +export {default as contributionList} from './contributionList.js'; +export {default as dimensions} from './dimensions.js'; +export {default as directory} from './directory.js'; +export {default as duration} from './duration.js'; +export {default as externalFunction} from './externalFunction.js'; +export {default as fileExtension} from './fileExtension.js'; +export {default as flag} from './flag.js'; +export {default as name} from './name.js'; +export {default as referenceList} from './referenceList.js'; +export {default as reverseReferenceList} from './reverseReferenceList.js'; +export {default as simpleDate} from './simpleDate.js'; +export {default as simpleString} from './simpleString.js'; +export {default as singleReference} from './singleReference.js'; +export {default as urls} from './urls.js'; +export {default as wikiData} from './wikiData.js'; diff --git a/src/data/composite/wiki-properties/name.js b/src/data/composite/wiki-properties/name.js new file mode 100644 index 00000000..5146488b --- /dev/null +++ b/src/data/composite/wiki-properties/name.js @@ -0,0 +1,11 @@ +// A wiki data object's name! Its directory (i.e. unique identifier) will be +// computed based on this value if not otherwise specified. + +import {isName} from '#validators'; + +export default function(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js new file mode 100644 index 00000000..f5b6c58e --- /dev/null +++ b/src/data/composite/wiki-properties/referenceList.js @@ -0,0 +1,47 @@ +// Stores and exposes a list of references to other data objects; all items +// must be references to the same type, which is specified on the class input. +// +// See also: +// - singleReference +// - withResolvedReferenceList +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReferenceList} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReferenceList} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: inputThingClass(), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + + steps: () => [ + withResolvedReferenceList({ + list: input.updateValue(), + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js new file mode 100644 index 00000000..84ba67df --- /dev/null +++ b/src/data/composite/wiki-properties/reverseReferenceList.js @@ -0,0 +1,30 @@ +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. + +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `reverseReferenceList`, + + compose: false, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/simpleDate.js b/src/data/composite/wiki-properties/simpleDate.js new file mode 100644 index 00000000..f08d8323 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleDate.js @@ -0,0 +1,14 @@ +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. + +import {isDate} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} diff --git a/src/data/composite/wiki-properties/simpleString.js b/src/data/composite/wiki-properties/simpleString.js new file mode 100644 index 00000000..18d65146 --- /dev/null +++ b/src/data/composite/wiki-properties/simpleString.js @@ -0,0 +1,14 @@ +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. + +import {isString} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js new file mode 100644 index 00000000..34bd2e6d --- /dev/null +++ b/src/data/composite/wiki-properties/singleReference.js @@ -0,0 +1,47 @@ +// Stores and exposes one connection, or reference, to another data object. +// The reference must be to a specific type, which is specified on the class +// input. +// +// See also: +// - referenceList +// - withResolvedReference +// + +import {input, templateCompositeFrom} from '#composite'; +import {validateReference} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputThingClass, inputWikiData, withResolvedReference} + from '#composite/wiki-data'; + +// TODO: Kludge. +import Thing from '../../things/thing.js'; + +export default templateCompositeFrom({ + annotation: `singleReference`, + + compose: false, + + inputs: { + class: inputThingClass(), + find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: false}), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; + }, + + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedReference'}), + ], +}); diff --git a/src/data/composite/wiki-properties/urls.js b/src/data/composite/wiki-properties/urls.js new file mode 100644 index 00000000..3160a0bf --- /dev/null +++ b/src/data/composite/wiki-properties/urls.js @@ -0,0 +1,14 @@ +// A list of URLs! This will always be present on the data object, even if set +// to an empty array or null. + +import {isURL, validateArrayItems} from '#validators'; + +// TODO: Not templateCompositeFrom. + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: value => value ?? []}, + }; +} diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js new file mode 100644 index 00000000..4ea47785 --- /dev/null +++ b/src/data/composite/wiki-properties/wikiData.js @@ -0,0 +1,17 @@ +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. + +import {validateArrayItems, validateInstanceOf} from '#validators'; + +// TODO: Not templateCompositeFrom. + +// TODO: This should validate with validateWikiData. + +export default function(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; +} diff --git a/src/data/things/album.js b/src/data/things/album.js index fd8a71d3..e3ac1651 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,21 +1,12 @@ +import {input} from '#composite'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; -import {isDate, isTrackSectionList} from '#validators'; -import {filterMultipleArrays} from '#wiki-data'; +import {isDate} from '#validators'; + +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {exitWithoutContribs} from '#composite/wiki-data'; import { - exitWithoutDependency, - exitWithoutUpdateValue, - exposeDependency, - exposeUpdateValueOrContinue, - input, - fillMissingListItems, - withFlattenedList, - withPropertiesFromList, - withUnflattenedList, -} from '#composite'; - -import Thing, { additionalFiles, commentary, color, @@ -24,7 +15,6 @@ import Thing, { contributionList, dimensions, directory, - exitWithoutContribs, fileExtension, flag, name, @@ -33,8 +23,14 @@ import Thing, { simpleString, urls, wikiData, - withResolvedReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + withTracks, + withTrackSections, +} from '#composite/things/album'; + +import Thing from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -101,100 +97,8 @@ export class Album extends Thing { additionalFiles: additionalFiles(), trackSections: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutUpdateValue({ - mode: input.value('empty'), - value: input.value([]), - }), - - withPropertiesFromList({ - list: input.updateValue(), - prefix: input.value('#sections'), - properties: input.value([ - 'tracks', - 'dateOriginallyReleased', - 'isDefaultTrackSection', - 'color', - ]), - }), - - fillMissingListItems({ - list: '#sections.tracks', - fill: input.value([]), - }), - - fillMissingListItems({ - list: '#sections.isDefaultTrackSection', - fill: input.value(false), - }), - - fillMissingListItems({ - list: '#sections.color', - fill: input.dependency('color'), - }), - - withFlattenedList({ - list: '#sections.tracks', - }).outputs({ - ['#flattenedList']: '#trackRefs', - ['#flattenedIndices']: '#sections.startIndex', - }), - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - notFoundMode: input.value('null'), - find: input.value(find.track), - }).outputs({ - ['#resolvedReferenceList']: '#tracks', - }), - - withUnflattenedList({ - list: '#tracks', - indices: '#sections.startIndex', - }).outputs({ - ['#unflattenedList']: '#sections.tracks', - }), - - { - flags: {update: true, expose: true}, - - update: {validate: isTrackSectionList}, - - expose: { - dependencies: [ - '#sections.tracks', - '#sections.color', - '#sections.dateOriginallyReleased', - '#sections.isDefaultTrackSection', - '#sections.startIndex', - ], - - transform(trackSections, { - '#sections.tracks': tracks, - '#sections.color': color, - '#sections.dateOriginallyReleased': dateOriginallyReleased, - '#sections.isDefaultTrackSection': isDefaultTrackSection, - '#sections.startIndex': startIndex, - }) { - filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, - tracks => !empty(tracks)); - - return stitchArrays({ - tracks, - color, - dateOriginallyReleased, - isDefaultTrackSection, - startIndex, - }); - } - }, - }, + withTrackSections(), + exposeDependency({dependency: '#trackSections'}), ], artistContribs: contributionList(), @@ -231,33 +135,8 @@ export class Album extends Thing { hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), tracks: [ - exitWithoutDependency({ - dependency: 'trackData', - value: input.value([]), - }), - - exitWithoutDependency({ - dependency: 'trackSections', - mode: input.value('empty'), - value: input.value([]), - }), - - { - dependencies: ['trackSections'], - compute: (continuation, {trackSections}) => - continuation({ - '#trackRefs': trackSections - .flatMap(section => section.tracks ?? []), - }), - }, - - withResolvedReferenceList({ - list: '#trackRefs', - data: 'trackData', - find: input.value(find.track), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), + withTracks(), + exposeDependency({dependency: '#tracks'}), ], }); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index ba3cbd0d..1266a4e0 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,14 +1,18 @@ -import {exposeUpdateValueOrContinue, input} from '#composite'; +import {input} from '#composite'; import {sortAlbumsTracksChronologically} from '#wiki-data'; import {isName} from '#validators'; -import Thing, { +import {exposeUpdateValueOrContinue} from '#composite/control-flow'; + +import { color, directory, flag, name, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 085e5663..ff9f8aee 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -2,7 +2,7 @@ import {input} from '#composite'; import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing, { +import { directory, fileExtension, flag, @@ -11,7 +11,9 @@ import Thing, { singleReference, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c03f8833..7e068dce 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2,14 +2,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; - -import { - a, - is, - isString, - isWholeNumber, - validateArrayItems, -} from '#validators'; +import {a} from '#validators'; import { decorateErrorWithIndex, @@ -1639,721 +1632,3 @@ export function debugComposite(fn) { compositeFrom.debug = false; return value; } - -// Exposes a dependency exactly as it is; this is typically the base of a -// composition which was created to serve as one property's descriptor. -// -// Please note that this *doesn't* verify that the dependency exists, so -// if you provide the wrong name or it hasn't been set by a previous -// compositional step, the property will be exposed as undefined instead -// of null. -// -export const exposeDependency = templateCompositeFrom({ - annotation: `exposeDependency`, - - compose: false, - - inputs: { - dependency: input.staticDependency({acceptsNull: true}), - }, - - steps: () => [ - { - dependencies: [input('dependency')], - compute: ({ - [input('dependency')]: dependency - }) => dependency, - }, - ], -}); - -// Exposes a constant value exactly as it is; like exposeDependency, this -// is typically the base of a composition serving as a particular property -// descriptor. It generally follows steps which will conditionally early -// exit with some other value, with the exposeConstant base serving as the -// fallback default value. -export const exposeConstant = templateCompositeFrom({ - annotation: `exposeConstant`, - - compose: false, - - inputs: { - value: input.staticValue(), - }, - - steps: () => [ - { - dependencies: [input('value')], - compute: ({ - [input('value')]: value, - }) => value, - }, - ], -}); - -// Checks the availability of a dependency and provides the result to later -// steps under '#availability' (by default). This is mainly intended for use -// by the more specific utilities, which you should consider using instead. -// Customize {mode} to select one of these modes, or default to 'null': -// -// * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null, undefined, nor an empty -// array. -// * 'falsy': Check that the value isn't false when treated as a boolean -// (nor an empty array). Keep in mind this will also be false -// for values like zero and the empty string! -// - -const inputAvailabilityCheckMode = () => input({ - validate: is('null', 'empty', 'falsy'), - defaultValue: 'null', -}); - -export const withResultOfAvailabilityCheck = templateCompositeFrom({ - annotation: `withResultOfAvailabilityCheck`, - - inputs: { - from: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - outputs: ['#availability'], - - steps: () => [ - { - dependencies: [input('from'), input('mode')], - - compute: (continuation, { - [input('from')]: value, - [input('mode')]: mode, - }) => { - let availability; - - switch (mode) { - case 'null': - availability = value !== undefined && value !== null; - break; - - case 'empty': - availability = value !== undefined && !empty(value); - break; - - case 'falsy': - availability = !!value && (!Array.isArray(value) || !empty(value)); - break; - } - - return continuation({'#availability': availability}); - }, - }, - ], -}); - -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('dependency')], - compute: (continuation, { - ['#availability']: availability, - [input('dependency')]: dependency, - }) => - (availability - ? continuation.exit(dependency) - : continuation()), - }, - ], -}); - -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! Also provide {validate} here to conveniently -// set a custom validation check for this property's update value. -export const exposeUpdateValueOrContinue = templateCompositeFrom({ - annotation: `exposeUpdateValueOrContinue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - - validate: input({ - type: 'function', - defaultValue: null, - }), - }, - - update: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - - steps: () => [ - exposeDependencyOrContinue({ - dependency: input.updateValue(), - mode: input('mode'), - }), - ], -}); - -// Early exits if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutDependency = templateCompositeFrom({ - annotation: `exitWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutUpdateValue = templateCompositeFrom({ - annotation: `exitWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue(), - mode: input('mode'), - value: input('value'), - }), - ], -}); - -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutDependency = templateCompositeFrom({ - annotation: `raiseOutputWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Raises if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ - annotation: `raiseOutputWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input.updateValue(), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, or the object doesn't have the listed property, -// the provided dependency will also be null. -export const withPropertyFromObject = templateCompositeFrom({ - annotation: `withPropertyFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - property: input({type: 'string'}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), - - steps: () => [ - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), - }, - - { - dependencies: [ - '#output', - input('object'), - input('property'), - ], - - compute: (continuation, { - ['#output']: output, - [input('object')]: object, - [input('property')]: property, - }) => continuation({ - [output]: - (object === null - ? null - : object[property] ?? null), - }), - }, - ], -}); - -// Gets the listed properties from some object, providing each property's value -// as a dependency prefixed with the same name as the object (by default). -// If the object itself is null, all provided dependencies will be null; -// if it's missing only select properties, those will be provided as null. -export const withPropertiesFromObject = templateCompositeFrom({ - annotation: `withPropertiesFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - - properties: input({ - type: 'array', - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`)) - : ['#object']), - - steps: () => [ - { - dependencies: [input('object'), input('properties')], - compute: (continuation, { - [input('object')]: object, - [input('properties')]: properties, - }) => continuation({ - ['#entries']: - (object === null - ? properties.map(property => [property, null]) - : properties.map(property => [property, object[property]])), - }), - }, - - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#entries', - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#entries']: entries, - }) => - (properties - ? continuation( - Object.fromEntries( - entries.map(([property, value]) => [ - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`), - value ?? null, - ]))) - : continuation({ - ['#object']: - Object.fromEntries(entries), - })), - }, - ], -}); - -// Gets a property from each of a list of objects (in a dependency) and -// provides the results. This doesn't alter any list indices, so positions -// which were null in the original list are kept null here. Objects which don't -// have the specified property are retained in-place as null. -export function withPropertyFromList({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; -} - -// Gets the listed properties from each of a list of objects, providing lists -// of property values each into a dependency prefixed with the same name as the -// list (by default). Like withPropertyFromList, this doesn't alter indices. -export const withPropertiesFromList = templateCompositeFrom({ - annotation: `withPropertiesFromList`, - - inputs: { - list: input({type: 'array'}), - - properties: input({ - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`)) - : ['#lists']), - - steps: () => [ - { - dependencies: [input('list'), input('properties')], - compute: (continuation, { - [input('list')]: list, - [input('properties')]: properties, - }) => continuation({ - ['#lists']: - Object.fromEntries( - properties.map(property => [ - property, - list.map(item => item[property] ?? null), - ])), - }), - }, - - { - dependencies: [ - input.staticDependency('list'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#lists', - ], - - compute: (continuation, { - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#lists']: lists, - }) => - (properties - ? continuation( - Object.fromEntries( - properties.map(property => [ - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`), - lists[property], - ]))) - : continuation({'#lists': lists})), - }, - ], -}); - -// Replaces items of a list, which are null or undefined, with some fallback -// value. By default, this replaces the passed dependency. -export const fillMissingListItems = templateCompositeFrom({ - annotation: `fillMissingListItems`, - - inputs: { - list: input({type: 'array'}), - fill: input({acceptsNull: true}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [input('list'), input('fill')], - compute: (continuation, { - [input('list')]: list, - [input('fill')]: fill, - }) => continuation({ - ['#filled']: - list.map(item => item ?? fill), - }), - }, - - { - dependencies: [input.staticDependency('list'), '#filled'], - compute: (continuation, { - [input.staticDependency('list')]: list, - ['#filled']: filled, - }) => continuation({ - [list ?? '#list']: - filled, - }), - }, - ], -}); - -// Filters particular values out of a list. Note that this will always -// completely skip over null, but can be used to filter out any other -// primitive or object value. -export const excludeFromList = templateCompositeFrom({ - annotation: `excludeFromList`, - - inputs: { - list: input(), - - item: input({defaultValue: null}), - items: input({type: 'array', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [ - input.staticDependency('list'), - input('list'), - input('item'), - input('items'), - ], - - compute: (continuation, { - [input.staticDependency('list')]: listName, - [input('list')]: listContents, - [input('item')]: excludeItem, - [input('items')]: excludeItems, - }) => continuation({ - [listName ?? '#list']: - listContents.filter(item => { - if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && excludeItems.includes(item)) return false; - return true; - }), - }), - }, - ], -}); - -// Flattens an array with one level of nested arrays, providing as dependencies -// both the flattened array as well as the original starting indices of each -// successive source array. -export const withFlattenedList = templateCompositeFrom({ - annotation: `withFlattenedList`, - - inputs: { - list: input({type: 'array'}), - }, - - outputs: ['#flattenedList', '#flattenedIndices'], - - steps: () => [ - { - dependencies: [input('list')], - compute(continuation, { - [input('list')]: sourceList, - }) { - const flattenedList = sourceList.flat(); - const indices = []; - let lastEndIndex = 0; - for (const {length} of sourceList) { - indices.push(lastEndIndex); - lastEndIndex += length; - } - - return continuation({ - ['#flattenedList']: flattenedList, - ['#flattenedIndices']: indices, - }); - }, - }, - ], -}); - -// After mapping the contents of a flattened array in-place (being careful to -// retain the original indices by replacing unmatched results with null instead -// of filtering them out), this function allows for recombining them. It will -// filter out null and undefined items by default (pass {filter: false} to -// disable this). -export const withUnflattenedList = templateCompositeFrom({ - annotation: `withUnflattenedList`, - - inputs: { - list: input({ - type: 'array', - defaultDependency: '#flattenedList', - }), - - indices: input({ - validate: validateArrayItems(isWholeNumber), - defaultDependency: '#flattenedIndices', - }), - - filter: input({ - type: 'boolean', - defaultValue: true, - }), - }, - - outputs: ['#unflattenedList'], - - steps: () => [ - { - dependencies: [input('list'), input('indices'), input('filter')], - compute(continuation, { - [input('list')]: list, - [input('indices')]: indices, - [input('filter')]: filter, - }) { - const unflattenedList = []; - - for (let i = 0; i < indices.length; i++) { - const startIndex = indices[i]; - const endIndex = - (i === indices.length - 1 - ? list.length - : indices[i + 1]); - - const values = list.slice(startIndex, endIndex); - unflattenedList.push( - (filter - ? values.filter(value => value !== null && value !== undefined) - : values)); - } - - return continuation({ - ['#unflattenedList']: unflattenedList, - }); - }, - }, - ], -}); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index c3f90260..8fb1edfa 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,7 +9,7 @@ import { oneOf, } from '#validators'; -import Thing, { +import { color, contributionList, fileExtension, @@ -19,7 +19,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; diff --git a/src/data/things/group.js b/src/data/things/group.js index 0b117801..d5ae03e7 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,7 +1,7 @@ import {input} from '#composite'; import find from '#find'; -import Thing, { +import { color, directory, name, @@ -9,7 +9,9 @@ import Thing, { simpleString, urls, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bcf99e80..de9d0e50 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -1,10 +1,6 @@ +import {input} from '#composite'; import find from '#find'; -import { - exposeDependency, - input, -} from '#composite'; - import { is, isCountingNumber, @@ -16,14 +12,18 @@ import { validateReference, } from '#validators'; -import Thing, { +import {exposeDependency} from '#composite/control-flow'; +import {withResolvedReference} from '#composite/wiki-data'; + +import { color, name, referenceList, simpleString, wikiData, - withResolvedReference, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ diff --git a/src/data/things/language.js b/src/data/things/language.js index a325d6a6..fe74f7bf 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,13 +1,14 @@ import {Tag} from '#html'; import {isLanguageCode} from '#validators'; -import CacheableObject from './cacheable-object.js'; - -import Thing, { +import { externalFunction, flag, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 6984874e..ba065c25 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,9 +1,11 @@ -import Thing, { +import { directory, name, simpleDate, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 0133e0b6..f03e4405 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,10 +1,12 @@ import {isName} from '#validators'; -import Thing, { +import { directory, name, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1302e17..a47f8506 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1,48 +1,9 @@ -// Thing: base class for wiki data types, providing wiki-specific utility -// functions on top of essential CacheableObject behavior. +// Thing: base class for wiki data types, providing interfaces generally useful +// to all wiki data objects on top of foundational CacheableObject behavior. import {inspect} from 'node:util'; import {colors} from '#cli'; -import find from '#find'; -import {stitchArrays, unique} from '#sugar'; -import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {is} from '#validators'; - -import { - compositeFrom, - exitWithoutDependency, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withResultOfAvailabilityCheck, - withPropertiesFromList, -} from '#composite'; - -import { - isAdditionalFileList, - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDimensions, - isDirectory, - isDuration, - isFileExtension, - isName, - isString, - isType, - isURL, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, - validateWikiData, -} from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -77,673 +38,3 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } - -// Property descriptor templates -// -// Regularly reused property descriptors, for ease of access and generally -// duplicating less code across wiki data types. These are specialized utility -// functions, so check each for how its own arguments behave! - -export function name(defaultName) { - return { - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }; -} - -export function color() { - return { - flags: {update: true, expose: true}, - update: {validate: isColor}, - }; -} - -export function directory() { - return { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }; -} - -export function urls() { - return { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }; -} - -// A file extension! Or the default, if provided when calling this. -export function fileExtension(defaultFileExtension = null) { - return { - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }; -} - -// Plain ol' image dimensions. This is a two-item array of positive integers, -// corresponding to width and height respectively. -export function dimensions() { - return { - flags: {update: true, expose: true}, - update: {validate: isDimensions}, - }; -} - -// Duration! This is a number of seconds, possibly floating point, always -// at minimum zero. -export function duration() { - return { - flags: {update: true, expose: true}, - update: {validate: isDuration}, - }; -} - -// Straightforward flag descriptor for a variety of property purposes. -// Provide a default value, true or false! -export function flag(defaultValue = false) { - // TODO: ^ Are you actually kidding me - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; -} - -// General date type, used as the descriptor for a bunch of properties. -// This isn't dynamic though - it won't inherit from a date stored on -// another object, for example. -export function simpleDate() { - return { - flags: {update: true, expose: true}, - update: {validate: isDate}, - }; -} - -// General string type. This should probably generally be avoided in favor -// of more specific validation, but using it makes it easy to find where we -// might want to improve later, and it's a useful shorthand meanwhile. -export function simpleString() { - return { - flags: {update: true, expose: true}, - update: {validate: isString}, - }; -} - -// External function. These should only be used as dependencies for other -// properties, so they're left unexposed. -export function externalFunction() { - return { - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }; -} - -// Strong 'n sturdy contribution list, rolling a list of references (provided -// as this property's update value) and the resolved results (as get exposed) -// into one property. Update value will look something like this: -// -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] -// -// ...typically as processed from YAML, spreadsheet, or elsewhere. -// Exposes as the same, but with the "who" replaced with matches found in -// artistData - which means this always depends on an `artistData` property -// also existing on this object! -// -export function contributionList() { - return compositeFrom({ - annotation: `contributionList`, - - compose: false, - - update: {validate: isContributionList}, - - steps: [ - withResolvedContribs({from: input.updateValue()}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: input.value([])}), - ], - }); -} - -// Artist commentary! Generally present on tracks and albums. -export function commentary() { - return { - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }; -} - -// This is a somewhat more involved data structure - it's for additional -// or "bonus" files associated with albums or tracks (or anything else). -// It's got this form: -// -// [ -// {title: 'Booklet', files: ['Booklet.pdf']}, -// { -// title: 'Wallpaper', -// description: 'Cool Wallpaper!', -// files: ['1440x900.png', '1920x1080.png'] -// }, -// {title: 'Alternate Covers', description: null, files: [...]}, -// ... -// ] -// -export function additionalFiles() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }; -} - -const thingClassInput = { - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, -}; - -// A reference list! Keep in mind this is for general references to wiki -// objects of (usually) other Thing subclasses, not specifically leitmotif -// references in tracks (although that property uses referenceList too!). -// -// The underlying function validateReferenceList expects a string like -// 'artist' or 'track', but this utility keeps from having to hard-code the -// string in multiple places by referencing the value saved on the class -// instead. -export const referenceList = templateCompositeFrom({ - annotation: `referenceList`, - - compose: false, - - inputs: { - class: input.staticValue(thingClassInput), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, - - steps: () => [ - withResolvedReferenceList({ - list: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), - ], -}); - -// Corresponding function for a single reference. -export const singleReference = templateCompositeFrom({ - annotation: `singleReference`, - - compose: false, - - inputs: { - class: input(thingClassInput), - find: input({type: 'function'}), - data: inputWikiData({allowMixedTypes: false}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, - - steps: () => [ - withResolvedReference({ - ref: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReference'}), - ], -}); - -// Nice 'n simple shorthand for an exposed-only flag which is true when any -// contributions are present in the specified property. -export const contribsPresent = templateCompositeFrom({ - annotation: `contribsPresent`, - - compose: false, - - inputs: { - contribs: input.staticDependency({ - validate: isContributionList, - acceptsNull: true, - }), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('contribs'), - mode: input.value('empty'), - }), - - exposeDependency({dependency: '#availability'}), - ], -}); - -// Neat little shortcut for "reversing" the reference lists stored on other -// things - for example, tracks specify a "referenced tracks" property, and -// you would use this to compute a corresponding "referenced *by* tracks" -// property. Naturally, the passed ref list property is of the things in the -// wiki data provided, not the requesting Thing itself. -export const reverseReferenceList = templateCompositeFrom({ - annotation: `reverseReferenceList`, - - compose: false, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: input('data'), - list: input('list'), - }), - - exposeDependency({dependency: '#reverseReferenceList'}), - ], -}); - -// General purpose wiki data constructor, for properties like artistData, -// trackData, etc. -export function wikiData(thingClass) { - return { - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }; -} - -// This one's kinda tricky: it parses artist "references" from the -// commentary content, and finds the matching artist for each reference. -// This is mostly useful for credits and listings on artist pages. -export const commentatorArtists = templateCompositeFrom({ - annotation: `commentatorArtists`, - - compose: false, - - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: input.value('falsy'), - value: input.value([]), - }), - - { - dependencies: ['commentary'], - compute: (continuation, {commentary}) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, - - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - find: input.value(find.artist), - }).outputs({ - '#resolvedReferenceList': '#artists', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, - }, - ], -}); - -// Compositional utilities - -// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] -// value because classes aren't initialized by when templateCompositeFrom gets -// called (see: circular imports). So the reference types have to be hard-coded, -// which somewhat defeats the point of storing them on the class in the first -// place... -export function inputWikiData({ - referenceType = '', - allowMixedTypes = false, -} = {}) { - return input({ - validate: validateWikiData({referenceType, allowMixedTypes}), - acceptsNull: true, - }); -} - -// Resolves the contribsByRef contained in the provided dependency, -// providing (named by the second argument) the result. "Resolving" -// means mapping the "who" reference of each contribution to an artist -// object, and filtering out those whose "who" doesn't match any artist. -export const withResolvedContribs = templateCompositeFrom({ - annotation: `withResolvedContribs`, - - inputs: { - from: input({ - validate: isContributionList, - acceptsNull: true, - }), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedContribs'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('from'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedContribs']: [], - }), - }), - - withPropertiesFromList({ - list: input('from'), - properties: input.value(['who', 'what']), - prefix: input.value('#contribs'), - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input('notFoundMode'), - }).outputs({ - ['#resolvedReferenceList']: '#contribs.who', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - - compute(continuation, { - ['#contribs.who']: who, - ['#contribs.what']: what, - }) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - ['#resolvedContribs']: stitchArrays({who, what}), - }); - }, - }, - ], -}); - -// Shorthand for exiting if the contribution list (usually a property's update -// value) resolves to empty - ensuring that the later computed results are only -// returned if these contributions are present. -export const exitWithoutContribs = templateCompositeFrom({ - annotation: `exitWithoutContribs`, - - inputs: { - contribs: input({ - validate: isContributionList, - acceptsNull: true, - }), - - value: input({defaultValue: null}), - }, - - steps: () => [ - withResolvedContribs({ - from: input('contribs'), - }), - - withResultOfAvailabilityCheck({ - from: '#resolvedContribs', - mode: input.value('empty'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if notFoundMode is set to 'exit', if the find -// function doesn't match anything for the reference. Otherwise, the data -// object is provided on the output dependency; or null, if the reference -// doesn't match anything or itself was null to begin with. -export const withResolvedReference = templateCompositeFrom({ - annotation: `withResolvedReference`, - - inputs: { - ref: input({type: 'string', acceptsNull: true}), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('null', 'exit'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedReference'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('ref'), - output: input.value({ - ['#resolvedReference']: null, - }), - }), - - exitWithoutDependency({ - dependency: input('data'), - }), - - { - dependencies: [ - input('ref'), - input('data'), - input('find'), - input('notFoundMode'), - ], - - compute(continuation, { - [input('ref')]: ref, - [input('data')]: data, - [input('find')]: findFunction, - [input('notFoundMode')]: notFoundMode, - }) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raiseOutput({ - ['#resolvedReference']: match ?? null, - }); - }, - }, - ], -}); - -// Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export const withResolvedReferenceList = templateCompositeFrom({ - annotation: `withResolvedReferenceList`, - - inputs: { - list: input({ - validate: validateArrayItems(isString), - acceptsNull: true, - }), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), - }, - - outputs: ['#resolvedReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - raiseOutputWithoutDependency({ - dependency: input('list'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedReferenceList']: [], - }), - }), - - { - dependencies: [input('list'), input('data'), input('find')], - compute: (continuation, { - [input('list')]: list, - [input('data')]: data, - [input('find')]: findFunction, - }) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, - - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedReferenceList']: matches, - }) - : continuation()), - }, - - { - dependencies: ['#matches', input('notFoundMode')], - compute(continuation, { - ['#matches']: matches, - [input('notFoundMode')]: notFoundMode, - }) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.filter(match => match), - }); - - case 'null': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.map(match => match ?? null), - }); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], -}); - -// Check out the info on reverseReferenceList! -// This is its composable form. -export const withReverseReferenceList = templateCompositeFrom({ - annotation: `withReverseReferenceList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: thisThing, - [input('data')]: data, - [input('list')]: refListProperty, - }) => - continuation({ - ['#reverseReferenceList']: - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], -}); diff --git a/src/data/things/track.js b/src/data/things/track.js index c77bf889..193ad891 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,35 +1,28 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {input} from '#composite'; import find from '#find'; -import {empty} from '#sugar'; import { - exitWithoutDependency, - excludeFromList, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - exposeUpdateValueOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withPropertyFromObject, -} from '#composite'; - -import { - is, - isBoolean, isColor, isContributionList, isDate, isFileExtension, - validateWikiData, } from '#validators'; -import CacheableObject from './cacheable-object.js'; +import {withPropertyFromObject} from '#composite/data'; +import {withResolvedContribs} from '#composite/wiki-data'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; -import Thing, { +import { additionalFiles, commentary, commentatorArtists, @@ -45,10 +38,22 @@ import Thing, { simpleString, urls, wikiData, - withResolvedContribs, - withResolvedReference, - withReverseReferenceList, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import { + exitWithoutUniqueCoverArt, + inheritFromOriginalRelease, + trackReverseReferenceList, + withAlbum, + withAlwaysReferenceByDirectory, + withContainingTrackSection, + withHasUniqueCoverArt, + withOtherReleases, + withPropertyFromAlbum, +} from '#composite/things/track'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Track extends Thing { static [Thing.referenceType] = 'track'; @@ -84,39 +89,9 @@ export class Track extends Thing { exposeDependency({dependency: '#album.color'}), ], - // Controls how find.track works - it'll never be matched by a reference - // just to the track's name, which means you don't have to always reference - // some *other* (much more commonly referenced) track by directory instead - // of more naturally by name. alwaysReferenceByDirectory: [ - exposeUpdateValueOrContinue({ - validate: input.value(isBoolean), - }), - - excludeFromList({ - list: 'trackData', - item: input.myself(), - }), - - withOriginalRelease({ - data: '#trackData', - }), - - exitWithoutDependency({ - dependency: '#originalRelease', - value: input.value(false), - }), - - withPropertyFromObject({ - object: '#originalRelease', - property: input.value('name'), - }), - - { - dependencies: ['name', '#originalRelease.name'], - compute: ({name, '#originalRelease.name': originalName}) => - name === originalName, - }, + withAlwaysReferenceByDirectory(), + exposeDependency({dependency: '#alwaysReferenceByDirectory'}), ], // Disables presenting the track as though it has its own unique artwork. @@ -298,61 +273,20 @@ export class Track extends Thing { exposeDependency({dependency: '#album.date'}), ], - // Whether or not the track has "unique" cover artwork - a cover which is - // specifically associated with this track in particular, rather than with - // the track's album as a whole. This is typically used to select between - // displaying the track artwork and a fallback, such as the album artwork - // or a placeholder. (This property is named hasUniqueCoverArt instead of - // the usual hasCoverArt to emphasize that it does not inherit from the - // album.) hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), ], otherReleases: [ - exitWithoutDependency({ - dependency: 'trackData', - mode: input.value('empty'), - }), - - withOriginalRelease({ - selfIfOriginal: input.value(true), - }), - - { - flags: {expose: true}, - expose: { - dependencies: [input.myself(), '#originalRelease', 'trackData'], - compute: ({ - [input.myself()]: thisTrack, - ['#originalRelease']: originalRelease, - trackData, - }) => - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), - }, - }, + withOtherReleases(), + exposeDependency({dependency: '#otherReleases'}), ], - // Specifically exclude re-releases from this list - while it's useful to - // get from a re-release to the tracks it references, re-releases aren't - // generally relevant from the perspective of the tracks being referenced. - // Filtering them from data here hides them from the corresponding field - // on the site (obviously), and has the bonus of not counting them when - // counting the number of times a track has been referenced, for use in - // the "Tracks - by Times Referenced" listing page (or other data - // processing). referencedByTracks: trackReverseReferenceList({ list: input.value('referencedTracks'), }), - // For the same reasoning, exclude re-releases from sampled tracks too. sampledByTracks: trackReverseReferenceList({ list: input.value('sampledTracks'), }), @@ -386,344 +320,3 @@ export class Track extends Thing { return parts.join(''); } } - -// Early exits with a value inherited from the original release, if -// this track is a rerelease, and otherwise continues with no further -// dependencies provided. If allowOverride is true, then the continuation -// will also be called if the original release exposed the requested -// property as null. -export const inheritFromOriginalRelease = templateCompositeFrom({ - annotation: `Track.inheritFromOriginalRelease`, - - inputs: { - property: input({type: 'string'}), - allowOverride: input({type: 'boolean', defaultValue: false}), - }, - - steps: () => [ - withOriginalRelease(), - - { - dependencies: [ - '#originalRelease', - input('property'), - input('allowOverride'), - ], - - compute: (continuation, { - ['#originalRelease']: originalRelease, - [input('property')]: originalProperty, - [input('allowOverride')]: allowOverride, - }) => { - if (!originalRelease) return continuation(); - - const value = originalRelease[originalProperty]; - if (allowOverride && value === null) return continuation(); - - return continuation.exit(value); - }, - }, - ], -}); - -// Gets the track's album. This will early exit if albumData is missing. -// By default, if there's no album whose list of tracks includes this track, -// the output dependency will be null; set {notFoundMode: 'exit'} to early -// exit instead. -export const withAlbum = templateCompositeFrom({ - annotation: `Track.withAlbum`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#album'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: [input.myself(), 'albumData'], - compute: (continuation, { - [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)), - }), - }, - - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), - }, - ], -}); - -// Gets a single property from this track's album, providing it as the same -// property name prefixed with '#album.' (by default). If the track's album -// isn't available, then by default, the property will be provided as null; -// set {notFoundMode: 'exit'} to early exit instead. -export const withPropertyFromAlbum = templateCompositeFrom({ - annotation: `withPropertyFromAlbum`, - - inputs: { - property: input.staticValue({type: 'string'}), - - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ({ - [input.staticValue('property')]: property, - }) => ['#album.' + property], - - steps: () => [ - withAlbum({ - notFoundMode: input('notFoundMode'), - }), - - withPropertyFromObject({ - object: '#album', - property: input('property'), - }), - - { - dependencies: ['#value', input.staticValue('property')], - compute: (continuation, { - ['#value']: value, - [input.staticValue('property')]: property, - }) => continuation({ - ['#album.' + property]: value, - }), - }, - ], -}); - -// Gets the track section containing this track from its album's track list. -// If notFoundMode is set to 'exit', this will early exit if the album can't be -// found or if none of its trackSections includes the track for some reason. -export const withContainingTrackSection = templateCompositeFrom({ - annotation: `withContainingTrackSection`, - - inputs: { - notFoundMode: input({ - validate: is('exit', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#trackSection'], - - steps: () => [ - withPropertyFromAlbum({ - property: input.value('trackSections'), - notFoundMode: input('notFoundMode'), - }), - - { - dependencies: [ - input.myself(), - input('notFoundMode'), - '#album.trackSections', - ], - - compute(continuation, { - [input.myself()]: track, - [input('notFoundMode')]: notFoundMode, - ['#album.trackSections']: trackSections, - }) { - if (!trackSections) { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - - const trackSection = - trackSections.find(({tracks}) => tracks.includes(track)); - - if (trackSection) { - return continuation.raiseOutput({ - ['#trackSection']: trackSection, - }); - } else if (notFoundMode === 'exit') { - return continuation.exit(null); - } else { - return continuation.raiseOutput({ - ['#trackSection']: null, - }); - } - }, - }, - ], -}); - -// Just includes the original release of this track as a dependency. -// If this track isn't a rerelease, then it'll provide null, unless the -// {selfIfOriginal} option is set, in which case it'll provide this track -// itself. Note that this will early exit if the original release is -// specified by reference and that reference doesn't resolve to anything. -// Outputs to '#originalRelease' by default. -export const withOriginalRelease = templateCompositeFrom({ - annotation: `withOriginalRelease`, - - inputs: { - selfIfOriginal: input({type: 'boolean', defaultValue: false}), - - data: input({ - validate: validateWikiData({referenceType: 'track'}), - defaultDependency: 'trackData', - }), - }, - - outputs: ['#originalRelease'], - - steps: () => [ - withResolvedReference({ - ref: 'originalReleaseTrack', - data: input('data'), - find: input.value(find.track), - notFoundMode: input.value('exit'), - }).outputs({ - ['#resolvedReference']: '#originalRelease', - }), - - { - dependencies: [ - input.myself(), - input('selfIfOriginal'), - '#originalRelease', - ], - - compute: (continuation, { - [input.myself()]: track, - [input('selfIfOriginal')]: selfIfOriginal, - ['#originalRelease']: originalRelease, - }) => - continuation({ - ['#originalRelease']: - (originalRelease ?? - (selfIfOriginal - ? track - : null)), - }), - }, - ], -}); - -// The algorithm for checking if a track has unique cover art is used in a -// couple places, so it's defined in full as a compositional step. -export const withHasUniqueCoverArt = templateCompositeFrom({ - annotation: 'withHasUniqueCoverArt', - - outputs: ['#hasUniqueCoverArt'], - - steps: () => [ - { - dependencies: ['disableUniqueCoverArt'], - compute: (continuation, {disableUniqueCoverArt}) => - (disableUniqueCoverArt - ? continuation.raiseOutput({ - ['#hasUniqueCoverArt']: false, - }) - : continuation()), - }, - - withResolvedContribs({from: 'coverArtistContribs'}), - - { - dependencies: ['#resolvedContribs'], - compute: (continuation, { - ['#resolvedContribs']: contribsFromTrack, - }) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raiseOutput({ - ['#hasUniqueCoverArt']: true, - })), - }, - - withPropertyFromAlbum({ - property: input.value('trackCoverArtistContribs'), - }), - - { - dependencies: ['#album.trackCoverArtistContribs'], - compute: (continuation, { - ['#album.trackCoverArtistContribs']: contribsFromAlbum, - }) => - continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - !empty(contribsFromAlbum), - }), - }, - ], -}); - -// Shorthand for checking if the track has unique cover art and exposing a -// fallback value if it isn't. -export const exitWithoutUniqueCoverArt = templateCompositeFrom({ - annotation: `exitWithoutUniqueCoverArt`, - - inputs: { - value: input({defaultValue: null}), - }, - - steps: () => [ - withHasUniqueCoverArt(), - - exitWithoutDependency({ - dependency: '#hasUniqueCoverArt', - mode: input.value('falsy'), - value: input('value'), - }), - ], -}); - -export const trackReverseReferenceList = templateCompositeFrom({ - annotation: `trackReverseReferenceList`, - - compose: false, - - inputs: { - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: 'trackData', - list: input('list'), - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({ - ['#reverseReferenceList']: reverseReferenceList, - }) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ], -}); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index c764b528..0460f272 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -2,14 +2,16 @@ import {input} from '#composite'; import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing, { +import { color, flag, name, referenceList, simpleString, wikiData, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import Thing from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ -- cgit 1.3.0-6-gf8a5 From d2174a01dda63ba233cbcdf48bb70ed50127d54d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:30:39 -0300 Subject: data: obliterate composite.js explainer Poor (read: largely outdated) code documentation is worse than no code documentation. The various infrastructural systems specially designed for hsmusic should get more dedicated reference material, but that can't well be written before the systems are tested and used for longer. The compositional data processing style has just about settled, but it's still very young (compared to, say, the overarching data- to-page flow, content functions, or the HTML and content template systems). --- src/data/things/composite.js | 333 ------------------------------------------- 1 file changed, 333 deletions(-) (limited to 'src/data') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7e068dce..51525bc1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -15,339 +15,6 @@ import { withAggregate, } from '#sugar'; -// Composes multiple compositional "steps" and a "base" to form a property -// descriptor out of modular building blocks. This is an extension to the -// more general-purpose CacheableObject property descriptor syntax, and -// aims to make modular data processing - which lends to declarativity - -// much easier, without fundamentally altering much of the typical syntax -// or terminology, nor building on it to an excessive degree. -// -// Think of a composition as being a chain of steps which lead into a final -// base property, which is usually responsible for returning the value that -// will actually get exposed when the property being described is accessed. -// -// == The compositional base: == -// -// The final item in a compositional list is its base, and it identifies -// the essential qualities of the property descriptor. The compositional -// steps preceding it may exit early, in which case the expose function -// defined on the base won't be called; or they will provide dependencies -// that the base may use to compute the final value that gets exposed for -// this property. -// -// The base indicates the capabilities of the composition as a whole. -// It should be {expose: true}, since that's the only area that preceding -// compositional steps (currently) can actually influence. If it's also -// {update: true}, then the composition as a whole accepts an update value -// just like normal update-flag property descriptors - meaning it can be -// set with `thing.someProperty = value` and that value will be paseed -// into each (implementing) step's transform() function, as well as the -// base. Bases usually aren't {compose: true}, but can be - check out the -// section on "nesting compositions" for details about that. -// -// Every composition always has exactly one compositional base, and it's -// always the last item in the composition list. All items preceding it -// are compositional steps, described below. -// -// == Compositional steps: == -// -// Compositional steps are, in essence, typical property descriptors with -// the extra flag {compose: true}. They operate on existing dependencies, -// and are typically dynamically constructed by "utility" functions (but -// can also be manually declared within the step list of a composition). -// Compositional steps serve two purposes: -// -// 1. exit early, if some condition is matched, returning and exposing -// some value directly from that step instead of continuing further -// down the step list; -// -// 2. and/or provide new, dynamically created "private" dependencies which -// can be accessed by further steps down the list, or at the base at -// the bottom, modularly supplying information that will contribute to -// the final value exposed for this property. -// -// Usually it's just one of those two, but it's fine for a step to perform -// both jobs if the situation benefits. -// -// Compositional steps are the real "modular" or "compositional" part of -// this data processing style - they're designed to be combined together -// in dynamic, versatile ways, as each property demands it. You usually -// define a compositional step to be returned by some ordinary static -// property-descriptor-returning function (customarily namespaced under -// the relevant Thing class's static `composite` field) - that lets you -// reuse it in multiple compositions later on. -// -// Compositional steps are implemented with "continuation passing style", -// meaning the connection to the next link on the chain is passed right to -// each step's compute (or transform) function, and the implementation gets -// to decide whether to continue on that chain or exit early by returning -// some other value. -// -// Every step along the chain, apart from the base at the bottom, has to -// have the {compose: true} step. That means its compute() or transform() -// function will be passed an extra argument at the end, `continuation`. -// To provide new dependencies to items further down the chain, just pass -// them directly to this continuation() function, customarily with a hash -// ('#') prefixing each name - for example: -// -// compute({..some dependencies..}, continuation) { -// return continuation({ -// '#excitingProperty': (..a value made from dependencies..), -// }); -// } -// -// Performing an early exit is as simple as returning some other value, -// instead of the continuation. You may also use `continuation.exit(value)` -// to perform the exact same kind of early exit - it's just a different -// syntax that might fit in better in certain longer compositions. -// -// It may be fine to simply provide new dependencies under a hard-coded -// name, such as '#excitingProperty' above, but if you're writing a utility -// that dynamically returns the compositional step and you suspect you -// might want to use this step multiple times in a single composition, -// it's customary to accept a name for the result. -// -// Here's a detailed example showing off early exit, dynamically operating -// on a provided dependency name, and then providing a result in another -// also-provided dependency name: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: [contribsByRefDependency, 'artistData'], -// compute({ -// [contribsByRefDependency]: contribsByRef, -// artistData, -// }, continuation) { -// if (!artistData) return null; /* early exit! */ -// return continuation({ -// [outputDependency]: /* this is the important part */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// And how you might work that into a composition: -// -// Track.coverArtists = -// compositeFrom([ -// doSomethingWhichMightEarlyExit(), -// -// withResolvedContribs({ -// from: 'coverArtistContribsByRef', -// into: '#coverArtistContribs', -// }), -// -// { -// flags: {expose: true}, -// expose: { -// dependencies: ['#coverArtistContribs'], -// compute: ({'#coverArtistContribs': coverArtistContribs}) => -// coverArtistContribs.map(({who}) => who), -// }, -// }, -// ]); -// -// One last note! A super common code pattern when creating more complex -// compositions is to have several steps which *only* expose and compose. -// As a syntax shortcut, you can skip the outer section. It's basically -// like writing out just the {expose: {...}} part. Remember that this -// indicates that the step you're defining is compositional, so you have -// to specify the flags manually for the base, even if this property isn't -// going to get an {update: true} flag. -// -// == Cache-safe dependency names: == -// -// [Disclosure: The caching engine hasn't actually been implemented yet. -// As such, this section is subject to change, and simply provides sound -// forward-facing advice and interfaces.] -// -// It's a good idea to write individual compositional steps in such a way -// that they're "cache-safe" - meaning the same input (dependency) values -// will always result in the same output (continuation or early exit). -// -// In order to facilitate this, compositional step descriptors may specify -// unique `mapDependencies`, `mapContinuation`, and `options` values. -// -// Consider the `withResolvedContribs` example adjusted to make use of -// two of these options below: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {contribsByRef: contribsByRefDependency}, -// mapContinuation: {outputDependency}, -// compute({ -// contribsByRef, /* no longer in square brackets */ -// artistData, -// }, continuation) { -// if (!artistData) return null; -// return continuation({ -// outputDependency: /* no longer in square brackets */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// With a little destructuring and restructuring JavaScript sugar, the -// above can be simplified some more: -// -// withResolvedContribs = ({from, to}) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {from}, -// mapContinuation: {into}, -// compute({artistData, from: contribsByRef}, continuation) { -// if (!artistData) return null; -// return continuation({ -// into: (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// These two properties let you separate the name-mapping behavior (for -// dependencies and the continuation) from the main body of the compute -// function. That means the compute function will *always* get inputs in -// the same form (dependencies 'artistData' and 'from' above), and will -// *always* provide its output in the same form (early return or 'to'). -// -// Thanks to that, this `compute` function is cache-safe! Its outputs can -// be cached corresponding to each set of mapped inputs. So it won't matter -// whether the `from` dependency is named `coverArtistContribsByRef` or -// `contributorContribsByRef` or something else - the compute function -// doesn't care, and only expects that value to be provided via its `from` -// argument. Likewise, it doesn't matter if the output should be sent to -// '#coverArtistContribs` or `#contributorContribs` or some other name; -// the mapping is handled automatically outside, and compute will always -// output its value to the continuation's `to`. -// -// Note that `mapDependencies` and `mapContinuation` should be objects of -// the same "shape" each run - that is, the values will change depending on -// outside context, but the keys are always the same. You shouldn't use -// `mapDependencies` to dynamically select more or fewer dependencies. -// If you need to dynamically select a range of dependencies, just specify -// them in the `dependencies` array like usual. The caching engine will -// understand that differently named `dependencies` indicate separate -// input-output caches should be used. -// -// The 'options' property makes it possible to specify external arguments -// that fundamentally change the behavior of the `compute` function, while -// still remaining cache-safe. It indicates that the caching engine should -// use a completely different input-to-output cache for each permutation -// of the 'options' values. This way, those functions are still cacheable -// at all; they'll just be cached separately for each set of option values. -// Values on the 'options' property will always be provided in compute's -// dependencies under '#options' (to avoid name conflicts with other -// dependencies). -// -// == To compute or to transform: == -// -// A compositional step can work directly on a property's stored update -// value, transforming it in place and either early exiting with it or -// passing it on (via continuation) to the next item(s) in the -// compositional step list. (If needed, these can provide dependencies -// the same way as compute functions too - just pass that object after -// the updated (or same) transform value in your call to continuation().) -// -// But in order to make them more versatile, compositional steps have an -// extra trick up their sleeve. If a compositional step implements compute -// and *not* transform, it can still be used in a composition targeting a -// property which updates! These retain their full dependency-providing and -// early exit functionality - they just won't be provided the update value. -// If a compute-implementing step returns its continuation, then whichever -// later step (or the base) next implements transform() will receive the -// update value that had so far been running - as well as any dependencies -// the compute() step returned, of course! -// -// Please note that a compositional step which transforms *should not* -// specify, in its flags, {update: true}. Just provide the transform() -// function in its expose descriptor; it will be automatically detected -// and used when appropriate. -// -// It's actually possible for a step to specify both transform and compute, -// in which case the transform() implementation will only be selected if -// the composition's base is {update: true}. It's not exactly known why you -// would want to specify unique-but-related transform and compute behavior, -// but the basic possibility was too cool to skip out on. -// -// == Nesting compositions: == -// -// Compositional steps are so convenient that you just might want to bundle -// them together, and form a whole new step-shaped unit of its own! -// -// In order to allow for this while helping to ensure internal dependencies -// remain neatly isolated from the composition which nests your bundle, -// the compositeFrom() function will accept and adapt to a base that -// specifies the {compose: true} flag, just like the steps preceding it. -// -// The continuation function that gets provided to the base will be mildly -// special - after all, nothing follows the base within the composition's -// own list! Instead of appending dependencies alongside any previously -// provided ones to be available to the next step, the base's continuation -// function should be used to define "exports" of the composition as a -// whole. It's similar to the usual behavior of the continuation, just -// expanded to the scope of the composition instead of following steps. -// -// For example, suppose your composition (which you expect to include in -// other compositions) brings about several private, hash-prefixed -// dependencies to contribute to its own results. Those dependencies won't -// end up "bleeding" into the dependency list of whichever composition is -// nesting this one - they will totally disappear once all the steps in -// the nested composition have finished up. -// -// To "export" the results of processing all those dependencies (provided -// that's something you want to do and this composition isn't used purely -// for a conditional early-exit), you'll want to define them in the -// continuation passed to the base. (Customarily, those should start with -// a hash just like the exports from any other compositional step; they're -// still dynamically provided dependencies!) -// -// Another way to "export" dependencies is by using calling *any* step's -// `continuation.raise()` function. This is sort of like early exiting, -// but instead of quitting out the whole entire property, it will just -// break out of the current, nested composition's list of steps, acting -// as though the composition had finished naturally. The dependencies -// passed to `raise` will be the ones which get exported. -// -// Since `raise` is another way to export dependencies, if you're using -// dynamic export names, you should specify `mapContinuation` on the step -// calling `continuation.raise` as well. -// -// An important note on `mapDependencies` here: A nested composition gets -// free access to all the ordinary properties defined on the thing it's -// working on, but if you want it to depend on *private* dependencies - -// ones prefixed with '#' - which were provided by some other compositional -// step preceding wherever this one gets nested, then you *have* to use -// `mapDependencies` to gain access. Check out the section on "cache-safe -// dependency names" for information on this syntax! -// -// Also - on rare occasion - you might want to make a reusable composition -// that itself causes the composition *it's* nested in to raise. If that's -// the case, give `composition.raiseAbove()` a go! This effectively means -// kicking out of *two* layers of nested composition - the one including -// the step with the `raiseAbove` call, and the composition which that one -// is nested within. You don't need to use `raiseAbove` if the reusable -// utility function just returns a single compositional step, but if you -// want to make use of other compositional steps, it gives you access to -// the same conditional-raise capabilities. -// -// Have some syntax sugar! Since nested compositions are defined by having -// the base be {compose: true}, the composition will infer as much if you -// don't specifying the base's flags at all. Simply use the same shorthand -// syntax as for other compositional steps, and it'll work out cleanly! -// - const globalCompositeCache = {}; const _valueIntoToken = shape => -- cgit 1.3.0-6-gf8a5 From a60c8906ed7580a21527c9f96cd0e6e277978263 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:58:56 -0300 Subject: data, test: expose track section names --- src/data/composite/things/album/withTrackSections.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js index c99b94d2..baa3cb4a 100644 --- a/src/data/composite/things/album/withTrackSections.js +++ b/src/data/composite/things/album/withTrackSections.js @@ -41,6 +41,7 @@ export default templateCompositeFrom({ 'tracks', 'dateOriginallyReleased', 'isDefaultTrackSection', + 'name', 'color', ]), }), @@ -55,6 +56,11 @@ export default templateCompositeFrom({ fill: input.value(false), }), + fillMissingListItems({ + list: '#sections.name', + fill: input.value('Unnamed Track Section'), + }), + fillMissingListItems({ list: '#sections.color', fill: input.dependency('color'), @@ -86,6 +92,7 @@ export default templateCompositeFrom({ { dependencies: [ '#sections.tracks', + '#sections.name', '#sections.color', '#sections.dateOriginallyReleased', '#sections.isDefaultTrackSection', @@ -94,19 +101,21 @@ export default templateCompositeFrom({ compute: (continuation, { '#sections.tracks': tracks, + '#sections.name': name, '#sections.color': color, '#sections.dateOriginallyReleased': dateOriginallyReleased, '#sections.isDefaultTrackSection': isDefaultTrackSection, '#sections.startIndex': startIndex, }) => { filterMultipleArrays( - tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, + tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex, tracks => !empty(tracks)); return continuation({ ['#trackSections']: stitchArrays({ tracks, + name, color, dateOriginallyReleased, isDefaultTrackSection, -- cgit 1.3.0-6-gf8a5 From 963e04f124f98464a986487208ee4f9edd893984 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:59:25 -0300 Subject: data: misc. composite fixes --- src/data/composite/things/track/withAlwaysReferenceByDirectory.js | 2 ++ src/data/composite/wiki-data/withReverseReferenceList.js | 1 + src/data/things/album.js | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 0aeac788..7c59393c 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -15,6 +15,8 @@ import withOriginalRelease from './withOriginalRelease.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, + outputs: ['#alwaysReferenceByDirectory'], + steps: () => [ exposeUpdateValueOrContinue({ validate: input.value(isBoolean), diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 113a6c40..a025b5ed 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -21,6 +21,7 @@ export default templateCompositeFrom({ exitWithoutDependency({ dependency: input('data'), value: input.value([]), + mode: input.value('empty'), }), { diff --git a/src/data/things/album.js b/src/data/things/album.js index e3ac1651..f451a7e9 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -182,7 +182,7 @@ export class Album extends Thing { export class TrackSectionHelper extends Thing { static [Thing.getPropertyDescriptors] = () => ({ - name: name('Unnamed Track Group'), + name: name('Unnamed Track Section'), color: color(), dateOriginallyReleased: simpleDate(), isDefaultTrackGroup: flag(false), -- cgit 1.3.0-6-gf8a5 From 72cc4d62a32e40c1dcb75e868c51991075cc03e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 2 Oct 2023 10:40:27 -0300 Subject: data: withAlwaysReferenceByDirectory: kludge to avoid infinite recursion --- .../things/track/withAlwaysReferenceByDirectory.js | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'src/data') diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 7c59393c..d27f7b23 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -2,15 +2,22 @@ // just to the track's name, which means you don't have to always reference // some *other* (much more commonly referenced) track by directory instead // of more naturally by name. +// +// See the implementation for an important caveat about matching the original +// track against other tracks, which uses a custom implementation pulling (and +// duplicating) details from #find instead of using withOriginalRelease and the +// usual withResolvedReference / find.track() utilities. +// import {input, templateCompositeFrom} from '#composite'; import {isBoolean} from '#validators'; import {exitWithoutDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; -import {excludeFromList, withPropertyFromObject} from '#composite/data'; +import {withPropertyFromObject} from '#composite/data'; -import withOriginalRelease from './withOriginalRelease.js'; +// TODO: Kludge. (The usage of this, not so much the import.) +import CacheableObject from '../../../things/cacheable-object.js'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, @@ -22,15 +29,45 @@ export default templateCompositeFrom({ validate: input.value(isBoolean), }), - excludeFromList({ - list: 'trackData', - item: input.myself(), + // Remaining code is for defaulting to true if this track is a rerelease of + // another with the same name, so everything further depends on access to + // trackData as well as originalReleaseTrack. + + exitWithoutDependency({ + dependency: 'trackData', + mode: input.value('empty'), + value: input.value(false), }), - withOriginalRelease({ - data: '#trackData', + exitWithoutDependency({ + dependency: 'originalReleaseTrack', + value: input.value(false), }), + // "Slow" / uncached, manual search from trackData (with this track + // excluded). Otherwise there end up being pretty bad recursion issues + // (track1.alwaysReferencedByDirectory depends on searching through data + // including track2, which depends on evaluating track2.alwaysReferenced- + // ByDirectory, which depends on searcing through data including track1...) + // That said, this is 100% a kludge, since it involves duplicating find + // logic on a completely unrelated context. + { + dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'], + compute: (continuation, { + [input.myself()]: thisTrack, + ['trackData']: trackData, + ['originalReleaseTrack']: ref, + }) => continuation({ + ['#originalRelease']: + (ref.startsWith('track:') + ? trackData.find(track => track.directory === ref.slice('track:'.length)) + : trackData.find(track => + track !== thisTrack && + !CacheableObject.getUpdateValue(track, 'originalReleaseTrack') && + track.name.toLowerCase() === ref.toLowerCase())), + }) + }, + exitWithoutDependency({ dependency: '#originalRelease', value: input.value(false), -- cgit 1.3.0-6-gf8a5 From 44e47fb3316c5452d277166215bc7522b404047f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 2 Oct 2023 10:41:42 -0300 Subject: data: custom cache for validateWikiData --- src/data/things/validators.js | 72 ++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 29 deletions(-) (limited to 'src/data') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index bdb22058..ee301f15 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -405,6 +405,8 @@ export function validateReferenceList(type = '') { return validateArrayItems(validateReference(type)); } +const validateWikiData_cache = {}; + export function validateWikiData({ referenceType = '', allowMixedTypes = false, @@ -413,51 +415,63 @@ export function validateWikiData({ throw new TypeError(`Don't specify both referenceType and allowMixedTypes`); } + validateWikiData_cache[referenceType] ??= {}; + validateWikiData_cache[referenceType][allowMixedTypes] ??= new WeakMap(); + const isArrayOfObjects = validateArrayItems(isObject); return (array) => { - isArrayOfObjects(array); + const subcache = validateWikiData_cache[referenceType][allowMixedTypes]; + if (subcache.has(array)) return subcache.get(array); - if (empty(array)) { - return true; - } + let OK = false; - const allRefTypes = - new Set(array.map(object => - object.constructor[Symbol.for('Thing.referenceType')])); + try { + isArrayOfObjects(array); - if (allRefTypes.has(undefined)) { - if (allRefTypes.size === 1) { - throw new TypeError(`Expected array of wiki data objects, got array of other objects`); - } else { - throw new TypeError(`Expected array of wiki data objects, got mixed items`); + if (empty(array)) { + OK = true; return true; } - } - if (allRefTypes.size > 1) { - if (allowMixedTypes) { - return true; + const allRefTypes = + new Set(array.map(object => + object.constructor[Symbol.for('Thing.referenceType')])); + + if (allRefTypes.has(undefined)) { + if (allRefTypes.size === 1) { + throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + } else { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } } - const types = () => Array.from(allRefTypes).join(', '); + if (allRefTypes.size > 1) { + if (allowMixedTypes) { + OK = true; return true; + } - if (referenceType) { - if (allRefTypes.has(referenceType)) { - allRefTypes.remove(referenceType); - throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) - } else { - throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + const types = () => Array.from(allRefTypes).join(', '); + + if (referenceType) { + if (allRefTypes.has(referenceType)) { + allRefTypes.remove(referenceType); + throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`) + } else { + throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`); + } } + + throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); } - throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); - } + if (referenceType && !allRefTypes.has(referenceType)) { + throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + } - if (referenceType && !allRefTypes.has(referenceType)) { - throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + OK = true; return true; + } finally { + subcache.set(array, OK); } - - return true; }; } -- cgit 1.3.0-6-gf8a5 From b37d81240307f3e38faaa781c3932feff53e9aac Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 6 Oct 2023 12:06:15 -0300 Subject: data, test: fix track contribs not inheriting properly --- src/data/things/track.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/data') diff --git a/src/data/things/track.js b/src/data/things/track.js index 193ad891..db325a17 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -178,7 +178,10 @@ export class Track extends Thing { '#resolvedContribs': '#artistContribs', }), - exposeDependencyOrContinue({dependency: '#artistContribs'}), + exposeDependencyOrContinue({ + dependency: '#artistContribs', + mode: input.value('empty'), + }), withPropertyFromAlbum({ property: input.value('artistContribs'), @@ -199,7 +202,9 @@ export class Track extends Thing { // typically varies by release and isn't defined by the musical qualities // of the track. coverArtistContribs: [ - exitWithoutUniqueCoverArt(), + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), @@ -207,7 +212,10 @@ export class Track extends Thing { '#resolvedContribs': '#coverArtistContribs', }), - exposeDependencyOrContinue({dependency: '#coverArtistContribs'}), + exposeDependencyOrContinue({ + dependency: '#coverArtistContribs', + mode: input.value('empty'), + }), withPropertyFromAlbum({ property: input.value('trackCoverArtistContribs'), -- cgit 1.3.0-6-gf8a5 From 3cd6f9edc58171e33ed6af565db84113e2488f25 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Oct 2023 14:58:14 -0300 Subject: data: language: allow passing multiple key parts directly --- src/data/things/language.js | 66 ++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 22 deletions(-) (limited to 'src/data') diff --git a/src/data/things/language.js b/src/data/things/language.js index fe74f7bf..646eb6d1 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -101,6 +101,7 @@ export class Language extends Thing { }, }, + // TODO: This currently isn't used. Is it still needed? strings_htmlEscaped: { flags: {expose: true}, expose: { @@ -130,8 +131,8 @@ export class Language extends Thing { }; } - $(key, args = {}) { - return this.formatString(key, args); + $(...args) { + return this.formatString(...args); } assertIntlAvailable(property) { @@ -145,8 +146,20 @@ export class Language extends Thing { return this.intl_pluralCardinal.select(value); } - formatString(key, args = {}) { - const strings = this.strings_htmlEscaped; + formatString(...args) { + const hasOptions = + typeof args.at(-1) === 'object' && + args.at(-1) !== null; + + const key = + (hasOptions ? args.slice(0, -1) : args) + .filter(Boolean) + .join('.'); + + const options = + (hasOptions + ? args.at(-1) + : null); if (!this.strings) { throw new Error(`Strings unavailable`); @@ -158,27 +171,36 @@ export class Language extends Thing { const template = this.strings[key]; - // Convert the keys on the args dict from camelCase to CONSTANT_CASE. - // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut - // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. Also strip HTML from arguments - // that are literal strings - real HTML content should always be proper - // HTML objects (see html.js). - const processedArgs = - Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - this.#sanitizeStringArg(v), - ]); - - // Replacement time! Woot. Reduce comes in handy here! - const output = - processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template); + let output; + + if (hasOptions) { + // Convert the keys on the options dict from camelCase to CONSTANT_CASE. + // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut + // like, who cares, dude?) Also, this is an array, 8ecause it's handy + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedOptions = + Object.entries(options).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); + + // Replacement time! Woot. Reduce comes in handy here! + output = + processedOptions.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); + } else { + // Without any options provided, just use the template as-is. This will + // still error if the template expected arguments, and otherwise will be + // the right value. + output = template; + } // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. - if (output.match(/\{[A-Z_]+\}/)) { + if (output.match(/\{[A-Z][A-Z0-9_]*\}/)) { throw new Error(`Args in ${key} were missing - output: ${output}`); } -- cgit 1.3.0-6-gf8a5 From 3a871cf43a11b87392d26320c736b516925da684 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 11 Oct 2023 14:32:28 -0300 Subject: implement flash act pages, rework flash sidebar layout --- src/data/things/flash.js | 6 +++++- src/data/yaml.js | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'src/data') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 8fb1edfa..52e30f88 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -12,6 +12,7 @@ import { import { color, contributionList, + directory, fileExtension, name, referenceList, @@ -117,12 +118,15 @@ export class Flash extends Thing { } export class FlashAct extends Thing { + static [Thing.referenceType] = 'flash-act'; + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: name('Unnamed Flash Act'), + directory: directory(), color: color(), - anchor: simpleString(), + jump: simpleString(), jumpColor: { diff --git a/src/data/yaml.js b/src/data/yaml.js index c799be5f..0ecc1f1e 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -434,8 +434,10 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { export const processFlashActDocument = makeProcessDocument(T.FlashAct, { propertyFieldMapping: { name: 'Act', + directory: 'Directory', + color: 'Color', - anchor: 'Anchor', + jump: 'Jump', jumpColor: 'Jump Color', }, -- cgit 1.3.0-6-gf8a5 From e842ce93e6405334b6ef475ec1db41e051cfd2b5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 11 Oct 2023 14:49:43 -0300 Subject: data: use flash act directory for better determinism --- src/data/yaml.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index 0ecc1f1e..a2811d43 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1391,6 +1391,7 @@ export function filterDuplicateDirectories(wikiData) { 'albumData', 'artTagData', 'flashData', + 'flashActData', 'groupData', 'newsData', 'trackData', -- cgit 1.3.0-6-gf8a5 From 4e2dae523e7bf8b49272bd6afcba86a8157af4a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:25:27 -0300 Subject: data: add [Thing.friendlyName] property to some Thing subclasses --- src/data/things/album.js | 2 ++ src/data/things/art-tag.js | 1 + src/data/things/flash.js | 1 + src/data/things/group.js | 2 ++ src/data/things/homepage-layout.js | 6 ++++++ src/data/things/news-entry.js | 1 + src/data/things/static-page.js | 1 + src/data/things/thing.js | 1 + src/data/things/wiki-info.js | 2 ++ 9 files changed, 17 insertions(+) (limited to 'src/data') diff --git a/src/data/things/album.js b/src/data/things/album.js index f451a7e9..546fda3b 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -181,6 +181,8 @@ export class Album extends Thing { } export class TrackSectionHelper extends Thing { + static [Thing.friendlyName] = `Track Section`; + static [Thing.getPropertyDescriptors] = () => ({ name: name('Unnamed Track Section'), color: color(), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 1266a4e0..6503beec 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -16,6 +16,7 @@ import Thing from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; + static [Thing.friendlyName] = `Art Tag`; static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 52e30f88..e3ef9f5c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -119,6 +119,7 @@ export class Flash extends Thing { export class FlashAct extends Thing { static [Thing.referenceType] = 'flash-act'; + static [Thing.friendlyName] = `Flash Act`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/group.js b/src/data/things/group.js index d5ae03e7..8764a9db 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -83,6 +83,8 @@ export class Group extends Thing { } export class GroupCategory extends Thing { + static [Thing.friendlyName] = `Group Category`; + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index de9d0e50..bfa971ca 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -26,6 +26,8 @@ import { import Thing from './thing.js'; export class HomepageLayout extends Thing { + static [Thing.friendlyName] = `Homepage Layout`; + static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose @@ -47,6 +49,8 @@ export class HomepageLayout extends Thing { } export class HomepageLayoutRow extends Thing { + static [Thing.friendlyName] = `Homepage Row`; + static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose @@ -75,6 +79,8 @@ export class HomepageLayoutRow extends Thing { } export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { + static [Thing.friendlyName] = `Homepage Albums Row`; + static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index ba065c25..36da0299 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -9,6 +9,7 @@ import Thing from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; + static [Thing.friendlyName] = `News Entry`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index f03e4405..ab9c5f98 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -10,6 +10,7 @@ import Thing from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; + static [Thing.friendlyName] = `Static Page`; static [Thing.getPropertyDescriptors] = () => ({ // Update & expose diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a47f8506..def7e914 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,6 +9,7 @@ import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { static referenceType = Symbol.for('Thing.referenceType'); + static friendlyName = Symbol.for(`Thing.friendlyName`); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 0460f272..6286a267 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -14,6 +14,8 @@ import { import Thing from './thing.js'; export class WikiInfo extends Thing { + static [Thing.friendlyName] = `Wiki Info`; + static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose -- cgit 1.3.0-6-gf8a5 From 645a127bef38c3a7a2ef1b94d23b25fb7bdc4191 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:26:49 -0300 Subject: data, test: wrap property value errors with proper class & cause --- src/data/things/cacheable-object.js | 19 ++++++++++++------- src/data/things/index.js | 6 +++++- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src/data') diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 4bc3668d..9fda865e 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -179,13 +179,8 @@ export default class CacheableObject { } else if (result !== true) { throw new TypeError(`Validation failed for value ${newValue}`); } - } catch (error) { - error.message = [ - `Property ${colors.green(property)}`, - `(${inspect(this[property])} -> ${inspect(newValue)}):`, - error.message - ].join(' '); - throw error; + } catch (caughtError) { + throw new CacheableObjectPropertyValueError(property, this[property], newValue, caughtError); } } @@ -359,3 +354,13 @@ export default class CacheableObject { return object.#propertyUpdateValues[key] ?? null; } } + +export class CacheableObjectPropertyValueError extends Error { + constructor(property, oldValue, newValue, error) { + super( + `Error setting ${colors.green(property)} (${inspect(oldValue)} -> ${inspect(newValue)})`, + {cause: error}); + + this.property = property; + } +} diff --git a/src/data/things/index.js b/src/data/things/index.js index 77e5fa76..4ea1f007 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -21,7 +21,11 @@ import * as trackClasses from './track.js'; import * as wikiInfoClasses from './wiki-info.js'; export {default as Thing} from './thing.js'; -export {default as CacheableObject} from './cacheable-object.js'; + +export { + default as CacheableObject, + CacheableObjectPropertyValueError, +} from './cacheable-object.js'; const allClassLists = { 'album.js': albumClasses, -- cgit 1.3.0-6-gf8a5 From 167e3ba07b54e6b9b780258fe8c10abd1ad80c2f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:30:21 -0300 Subject: yaml: cosmetic code clean-up --- src/data/yaml.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index a2811d43..33ca736d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -59,7 +59,7 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; // document and apply the configuration passed to makeProcessDocument in order // to construct a Thing subclass. function makeProcessDocument( - thingClass, + thingConstructor, { // Optional early step for transforming field values before providing them // to the Thing's update() method. This is useful when the input format @@ -110,7 +110,7 @@ function makeProcessDocument( invalidFieldCombinations = [], } ) { - if (!thingClass) { + if (!thingConstructor) { throw new Error(`Missing Thing class`); } @@ -152,7 +152,7 @@ function makeProcessDocument( .filter((field) => !knownFields.includes(field)); if (!empty(unknownFields)) { - throw new makeProcessDocument.UnknownFieldsError(unknownFields); + aggregate.push(new UnknownFieldsError(unknownFields)); } const presentFields = Object.keys(document); @@ -162,18 +162,16 @@ function makeProcessDocument( for (const {message, fields} of invalidFieldCombinations) { const fieldsPresent = presentFields.filter(field => fields.includes(field)); - if (fieldsPresent.length <= 1) { - continue; + if (fieldsPresent.length >= 2) { + fieldCombinationErrors.push( + new FieldCombinationError( + filterProperties(document, fieldsPresent), + message)); } - - fieldCombinationErrors.push( - new makeProcessDocument.FieldCombinationError( - filterProperties(document, fieldsPresent), - message)); } if (!empty(fieldCombinationErrors)) { - throw new makeProcessDocument.FieldCombinationsError(fieldCombinationErrors); + aggregate.push(new FieldCombinationAggregateError(fieldCombinationErrors)); } const fieldValues = {}; @@ -193,9 +191,9 @@ function makeProcessDocument( sourceProperties[property] = value; } - const thing = Reflect.construct(thingClass, []); + const thing = Reflect.construct(thingConstructor, []); - withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => { + withAggregate({message: `Errors applying ${colors.green(thingConstructor.name)} properties`}, ({call}) => { for (const [property, value] of Object.entries(sourceProperties)) { call(() => (thing[property] = value)); } @@ -212,20 +210,20 @@ function makeProcessDocument( return fn; } -makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error { +export class UnknownFieldsError extends Error { constructor(fields) { super(`Unknown fields present: ${fields.join(', ')}`); this.fields = fields; } -}; +} -makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extends AggregateError { +export class FieldCombinationAggregateError extends AggregateError { constructor(errors) { super(errors, `Errors in combinations of fields present`); } -}; +} -makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { +export class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; @@ -933,6 +931,10 @@ export const dataSteps = [ { title: `Process homepage layout file`, + + // Kludge: This benefits from the same headerAndEntries style messaging as + // albums and tracks (for example), but that document mode is designed to + // support multiple files, and only one is actually getting processed here. files: [HOMEPAGE_LAYOUT_DATA_FILE], documentMode: documentModes.headerAndEntries, -- cgit 1.3.0-6-gf8a5 From d8d877b63eec2e7c1d1afbca84b7f3cf6d24ca35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:32:00 -0300 Subject: yaml: filter and skip properties, not entire documents --- src/data/yaml.js | 260 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 96 deletions(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index 33ca736d..06ef5546 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -10,7 +10,12 @@ import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; -import T, {CacheableObject, Thing} from '#things'; + +import T, { + CacheableObject, + CacheableObjectPropertyValueError, + Thing, +} from '#things'; import { conditionallySuppressError, @@ -144,6 +149,25 @@ function makeProcessDocument( }; const fn = decorateErrorWithName((document) => { + const nameField = propertyFieldMapping['name']; + const namePart = + (nameField + ? (document[nameField] + ? ` named ${colors.green(`"${document[nameField]}"`)}` + : ` (name field, "${nameField}", not specified)`) + : ``); + + const constructorPart = + (thingConstructor[Thing.friendlyName] + ? colors.green(thingConstructor[Thing.friendlyName]) + : thingConstructor.name + ? colors.green(thingConstructor.name) + : `document`); + + const aggregate = openAggregate({ + message: `Errors processing ${constructorPart}` + namePart, + }); + const documentEntries = Object.entries(document) .filter(([field]) => !ignoredFields.includes(field)); @@ -187,19 +211,31 @@ function makeProcessDocument( const sourceProperties = {}; for (const [field, value] of Object.entries(fieldValues)) { - const property = fieldPropertyMapping[field]; - sourceProperties[property] = value; + if (Object.hasOwn(fieldPropertyMapping, field)) { + const property = fieldPropertyMapping[field]; + sourceProperties[property] = value; + } } const thing = Reflect.construct(thingConstructor, []); - withAggregate({message: `Errors applying ${colors.green(thingConstructor.name)} properties`}, ({call}) => { - for (const [property, value] of Object.entries(sourceProperties)) { - call(() => (thing[property] = value)); + const fieldValueErrors = []; + + // This for loop would like to certify itself as "not into capitalism". + for (const [property, value] of Object.entries(sourceProperties)) { + const field = propertyFieldMapping[property]; + try { + thing[property] = value; + } catch (caughtError) { + fieldValueErrors.push(new FieldValueError(field, property, value, caughtError)); } - }); + } - return thing; + if (!empty(fieldValueErrors)) { + aggregate.push(new FieldValueAggregateError(thingConstructor, fieldValueErrors)); + } + + return {thing, aggregate}; }); Object.assign(fn, { @@ -212,7 +248,7 @@ function makeProcessDocument( export class UnknownFieldsError extends Error { constructor(fields) { - super(`Unknown fields present: ${fields.join(', ')}`); + super(`Unknown fields present: ${fields.map(field => colors.red(field)).join(', ')}`); this.fields = fields; } } @@ -240,6 +276,25 @@ export class FieldCombinationError extends Error { } } +export class FieldValueAggregateError extends AggregateError { + constructor(thingConstructor, errors) { + super(errors, `Errors processing field values for ${colors.green(thingConstructor.name)}`); + } +} + +export class FieldValueError extends Error { + constructor(field, property, value, caughtError) { + const cause = + (caughtError instanceof CacheableObjectPropertyValueError + ? caughtError.cause + : caughtError); + + super( + `Failed to set ${colors.green(`"${field}"`)} field (${colors.green(property)}) to ${inspect(value)}`, + {cause}); + } +} + export const processAlbumDocument = makeProcessDocument(T.Album, { fieldTransformations: { 'Artists': parseContributors, @@ -1023,8 +1078,8 @@ export async function loadAndProcessDataDocuments({dataPath}) { for (const dataStep of dataSteps) { await processDataAggregate.nestAsync( - {message: `Errors during data step: ${dataStep.title}`}, - async ({call, callAsync, map, mapAsync, nest}) => { + {message: `Errors during data step: ${colors.bright(dataStep.title)}`}, + async ({call, callAsync, map, mapAsync, push, nest}) => { const {documentMode} = dataStep; if (!Object.values(documentModes).includes(documentMode)) { @@ -1149,32 +1204,52 @@ export async function loadAndProcessDataDocuments({dataPath}) { return; } - const yamlResult = - documentMode === documentModes.oneDocumentTotal - ? call(yaml.load, readResult) - : call(yaml.loadAll, readResult); + let processResults; - if (!yamlResult) { - return; - } + switch (documentMode) { + case documentModes.oneDocumentTotal: { + const yamlResult = call(yaml.load, readResult); - let processResults; + if (!yamlResult) { + processResults = null; + break; + } + + const {thing, aggregate} = + dataStep.processDocument(yamlResult); + + processResults = thing; + + call(() => aggregate.close()); - if (documentMode === documentModes.oneDocumentTotal) { - nest({message: `Errors processing document`}, ({call}) => { - processResults = call(dataStep.processDocument, yamlResult); - }); - } else { - const {documents, aggregate: aggregate1} = filterBlankDocuments(yamlResult); - call(aggregate1.close); - - const {result, aggregate: aggregate2} = mapAggregate( - documents, - decorateErrorWithIndex(dataStep.processDocument), - {message: `Errors processing documents`}); - call(aggregate2.close); - - processResults = result; + break; + } + + case documentModes.allInOne: { + const yamlResults = call(yaml.loadAll, readResult); + + if (!yamlResults) { + processResults = []; + return; + } + + const {documents, aggregate: filterAggregate} = + filterBlankDocuments(yamlResults); + + call(filterAggregate.close); + + processResults = []; + + map(documents, decorateErrorWithIndex(document => { + const {thing, aggregate} = + dataStep.processDocument(document); + + processResults.push(thing); + aggregate.close(); + }), {message: `Errors processing documents`}); + + break; + } } if (!processResults) return; @@ -1232,81 +1307,74 @@ export async function loadAndProcessDataDocuments({dataPath}) { return {file, documents: filteredDocuments}; }); - let processResults; + const processResults = []; - if (documentMode === documentModes.headerAndEntries) { - nest({message: `Errors processing data files as valid documents`}, ({call, map}) => { - processResults = []; + switch (documentMode) { + case documentModes.headerAndEntries: + map(yamlResults, decorateErrorWithFile(({documents}) => { + const headerDocument = documents[0]; + const entryDocuments = documents.slice(1).filter(Boolean); - yamlResults.forEach(({file, documents}) => { - const [headerDocument, ...entryDocuments] = documents; + if (!headerDocument) + throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - if (!headerDocument) { - call(decorateErrorWithFile(() => { - throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - }), {file}); - return; - } + // This'll be decorated with the file, and groups together any + // errors from processing the header and entry documents. + const fileAggregate = + openAggregate({message: `Errors processing documents`}); - const header = call( - decorateErrorWithFile(({document}) => - dataStep.processHeaderDocument(document)), - {file, document: headerDocument}); + const {thing: headerObject, aggregate: headerAggregate} = + dataStep.processHeaderDocument(headerDocument); - // Don't continue processing files whose header - // document is invalid - the entire file is excempt - // from data in this case. - if (!header) { - return; + try { + headerAggregate.close() + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`header`)}) ${caughtError.message}`; + fileAggregate.push(caughtError); } - const entries = map( - entryDocuments - .filter(Boolean) - .map((document) => ({file, document})), - decorateErrorWithFile( - decorateErrorWithIndex(({document}) => - dataStep.processEntryDocument(document))), - {message: `Errors processing entry documents`}); - - // Entries may be incomplete (i.e. any errored - // documents won't have a processed output - // represented here) - this is intentional! By - // principle, partial output is preferred over - // erroring an entire file. - processResults.push({header, entries}); - }); - }); - } + const entryObjects = []; - if (documentMode === documentModes.onePerFile) { - nest({message: `Errors processing data files as valid documents`}, ({call}) => { - processResults = []; + for (let index = 0; index < entryDocuments.length; index++) { + const entryDocument = entryDocuments[index]; - yamlResults.forEach(({file, documents}) => { - if (documents.length > 1) { - call(decorateErrorWithFile(() => { - throw new Error(`Only expected one document to be present per file`); - }), {file}); - return; - } else if (empty(documents) || !documents[0]) { - call(decorateErrorWithFile(() => { - throw new Error(`Expected a document, this file is empty`); - }), {file}); - } + const {thing: entryObject, aggregate: entryAggregate} = + dataStep.processEntryDocument(entryDocument); - const result = call( - decorateErrorWithFile(({document}) => - dataStep.processDocument(document)), - {file, document: documents[0]}); + entryObjects.push(entryObject); - if (!result) { - return; + try { + entryAggregate.close(); + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`entry #${index + 1}`)}) ${caughtError.message}`; + fileAggregate.push(caughtError); + } } - processResults.push(result); - }); - }); + processResults.push({ + header: headerObject, + entries: entryObjects, + }); + + fileAggregate.close(); + }), {message: `Errors processing documents in data files`}); + break; + + case documentModes.onePerFile: + map(yamlResults, decorateErrorWithFile(({documents}) => { + if (documents.length > 1) + throw new Error(`Only expected one document to be present per file, got ${documents.length} here`); + + if (empty(documents) || !documents[0]) + throw new Error(`Expected a document, this file is empty`); + + const {thing, aggregate} = + dataStep.processDocument(documents[0]); + + processResults.push(thing); + aggregate.close(); + }), {message: `Errors processing data files as valid documents`}); + break; } const saveResult = call(dataStep.save, processResults); -- cgit 1.3.0-6-gf8a5 From 84d22c117d6deabd53aaee1546e3a99f5d6049c7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:54:38 -0300 Subject: yaml: track skipped fields separately & report summary at bottom --- src/data/yaml.js | 80 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 16 deletions(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index 06ef5546..f49f48dd 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -171,12 +171,18 @@ function makeProcessDocument( const documentEntries = Object.entries(document) .filter(([field]) => !ignoredFields.includes(field)); + const skippedFields = new Set(); + const unknownFields = documentEntries .map(([field]) => field) .filter((field) => !knownFields.includes(field)); if (!empty(unknownFields)) { aggregate.push(new UnknownFieldsError(unknownFields)); + + for (const field of unknownFields) { + skippedFields.add(field); + } } const presentFields = Object.keys(document); @@ -187,10 +193,17 @@ function makeProcessDocument( const fieldsPresent = presentFields.filter(field => fields.includes(field)); if (fieldsPresent.length >= 2) { - fieldCombinationErrors.push( - new FieldCombinationError( - filterProperties(document, fieldsPresent), - message)); + const filteredDocument = + filterProperties( + document, + fieldsPresent, + {preserveOriginalOrder: true}); + + fieldCombinationErrors.push(new FieldCombinationError(filteredDocument, message)); + + for (const field of Object.keys(filteredDocument)) { + skippedFields.add(field); + } } } @@ -201,6 +214,7 @@ function makeProcessDocument( const fieldValues = {}; for (const [field, value] of documentEntries) { + if (skippedFields.has(field)) continue; if (Object.hasOwn(fieldTransformations, field)) { fieldValues[field] = fieldTransformations[field](value); } else { @@ -211,10 +225,8 @@ function makeProcessDocument( const sourceProperties = {}; for (const [field, value] of Object.entries(fieldValues)) { - if (Object.hasOwn(fieldPropertyMapping, field)) { - const property = fieldPropertyMapping[field]; - sourceProperties[property] = value; - } + const property = fieldPropertyMapping[field]; + sourceProperties[property] = value; } const thing = Reflect.construct(thingConstructor, []); @@ -227,6 +239,7 @@ function makeProcessDocument( try { thing[property] = value; } catch (caughtError) { + skippedFields.add(field); fieldValueErrors.push(new FieldValueError(field, property, value, caughtError)); } } @@ -235,6 +248,15 @@ function makeProcessDocument( aggregate.push(new FieldValueAggregateError(thingConstructor, fieldValueErrors)); } + if (skippedFields.size >= 1) { + aggregate.push( + new SkippedFieldsSummaryError( + filterProperties( + document, + Array.from(skippedFields), + {preserveOriginalOrder: true}))); + } + return {thing, aggregate}; }); @@ -248,30 +270,37 @@ function makeProcessDocument( export class UnknownFieldsError extends Error { constructor(fields) { - super(`Unknown fields present: ${fields.map(field => colors.red(field)).join(', ')}`); + super(`Unknown fields ignored: ${fields.map(field => colors.red(field)).join(', ')}`); this.fields = fields; } } export class FieldCombinationAggregateError extends AggregateError { constructor(errors) { - super(errors, `Errors in combinations of fields present`); + super(errors, `Invalid field combinations - all involved fields ignored`); } } export class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; - const messagePart = + const mainMessage = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; + + const causeMessage = (typeof message === 'function' - ? `: ${message(fields)}` + ? message(fields) : typeof message === 'string' - ? `: ${message}` - : ``); + ? message + : null); + + super(mainMessage, { + cause: + (causeMessage + ? new Error(causeMessage) + : null), + }); - super(combinePart + messagePart); this.fields = fields; } } @@ -295,6 +324,25 @@ export class FieldValueError extends Error { } } +export class SkippedFieldsSummaryError extends Error { + constructor(filteredDocument) { + const entries = Object.entries(filteredDocument); + + const lines = + entries.map(([field, value]) => + ` - ${field}: ` + + inspect(value) + .split('\n') + .map((line, index) => index === 0 ? line : ` ${line}`) + .join('\n')); + + super( + colors.bright(colors.yellow(`Altogether, skipped ${entries.length === 1 ? `1 field` : `${entries.length} fields`}:\n`)) + + lines.join('\n') + '\n' + + colors.bright(colors.yellow(`See above errors for details.`))); + } +} + export const processAlbumDocument = makeProcessDocument(T.Album, { fieldTransformations: { 'Artists': parseContributors, -- cgit 1.3.0-6-gf8a5 From 1511c0b0fd35d4f368e72288d48958cb3eff778f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 22 Oct 2023 14:44:42 -0300 Subject: data: fix bad dependency for Artist.albumsAsCommentator --- src/data/things/artist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/things/artist.js b/src/data/things/artist.js index ff9f8aee..ea19d2ba 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -100,7 +100,7 @@ export class Artist extends Thing { flags: {expose: true}, expose: { - dependencies: [this, 'albumData'], + dependencies: ['this', 'albumData'], compute: ({this: artist, albumData}) => albumData?.filter(({commentatorArtists}) => -- cgit 1.3.0-6-gf8a5 From 66d529179fc1896141876988dbe2a037f58b393b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 24 Oct 2023 09:25:12 -0300 Subject: yaml: remove cruft, support blank list items --- src/data/yaml.js | 58 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 26 deletions(-) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index f49f48dd..bf63f05d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -213,13 +213,36 @@ function makeProcessDocument( const fieldValues = {}; - for (const [field, value] of documentEntries) { + for (const [field, documentValue] of documentEntries) { if (skippedFields.has(field)) continue; - if (Object.hasOwn(fieldTransformations, field)) { - fieldValues[field] = fieldTransformations[field](value); - } else { - fieldValues[field] = value; + + // This variable would like to certify itself as "not into capitalism". + let propertyValue = + (Object.hasOwn(fieldTransformations, field) + ? fieldTransformations[field](documentValue) + : documentValue); + + // Completely blank items in a YAML list are read as null. + // They're handy to have around when filling out a document and shouldn't + // be considered an error (or data at all). + if (Array.isArray(propertyValue)) { + const wasEmpty = empty(propertyValue); + + propertyValue = + propertyValue.filter(item => item !== null); + + const isEmpty = empty(propertyValue); + + // Don't set arrays which are empty as a result of the above filter. + // Arrays which were originally empty, i.e. `Field: []`, are still + // valid data, but if it's just an array not containing any filled out + // items, it should be treated as a placeholder and skipped over. + if (isEmpty && !wasEmpty) { + propertyValue = null; + } } + + fieldValues[field] = propertyValue; } const sourceProperties = {}; @@ -233,7 +256,6 @@ function makeProcessDocument( const fieldValueErrors = []; - // This for loop would like to certify itself as "not into capitalism". for (const [property, value] of Object.entries(sourceProperties)) { const field = propertyFieldMapping[property]; try { @@ -700,33 +722,17 @@ export function parseContributors(contributors) { return contributors; } - if (contributors.length === 1 && contributors[0].startsWith('')) { - const arr = []; - arr.textContent = contributors[0]; - return arr; - } - contributors = contributors.map((contrib) => { - // 8asically, the format is "Who (What)", or just "Who". 8e sure to - // keep in mind that "what" doesn't necessarily have a value! + if (typeof contrib !== 'string') return contrib; + const match = contrib.match(/^(.*?)( \((.*)\))?$/); - if (!match) { - return contrib; - } + if (!match) return contrib; + const who = match[1]; const what = match[3] || null; return {who, what}; }); - const badContributor = contributors.find((val) => typeof val === 'string'); - if (badContributor) { - throw new Error(`Incorrectly formatted contribution: "${badContributor}".`); - } - - if (contributors.length === 1 && contributors[0].who === 'none') { - return null; - } - return contributors; } -- cgit 1.3.0-6-gf8a5 From f461941ac3d39b307ac32e21f9ff41b47fba638b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 24 Oct 2023 10:47:40 -0300 Subject: data, yaml: new flash act field List Termonology / listTerminology --- src/data/things/flash.js | 1 + src/data/yaml.js | 1 + 2 files changed, 2 insertions(+) (limited to 'src/data') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index e3ef9f5c..511ff19c 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -127,6 +127,7 @@ export class FlashAct extends Thing { name: name('Unnamed Flash Act'), directory: directory(), color: color(), + listTerminology: simpleString(), jump: simpleString(), diff --git a/src/data/yaml.js b/src/data/yaml.js index bf63f05d..f12c4c31 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -560,6 +560,7 @@ export const processFlashActDocument = makeProcessDocument(T.FlashAct, { directory: 'Directory', color: 'Color', + listTerminology: 'List Terminology', jump: 'Jump', jumpColor: 'Jump Color', -- cgit 1.3.0-6-gf8a5 From 6c0c1525a0e924896a2a593fc05633e442a80413 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 24 Oct 2023 10:49:05 -0300 Subject: yaml: check artists for duplicate directories --- src/data/yaml.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/data') diff --git a/src/data/yaml.js b/src/data/yaml.js index f12c4c31..16303a64 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1515,6 +1515,7 @@ export function filterDuplicateDirectories(wikiData) { const deduplicateSpec = [ 'albumData', 'artTagData', + 'artistData', 'flashData', 'flashActData', 'groupData', -- cgit 1.3.0-6-gf8a5 From a80dcfd176c41cef1995f5349a1464d4746badbd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 24 Oct 2023 11:33:25 -0300 Subject: data, yaml: new flash Color / color field --- src/data/things/flash.js | 28 +++++++++++++++++----------- src/data/yaml.js | 1 + 2 files changed, 18 insertions(+), 11 deletions(-) (limited to 'src/data') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 511ff19c..71174931 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,6 +9,10 @@ import { oneOf, } from '#validators'; +import { + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + import { color, contributionList, @@ -58,6 +62,19 @@ export class Flash extends Thing { }, }, + color: [ + exposeUpdateValueOrContinue({ + validate: input.value(isColor), + }), + + { + flags: {expose: true}, + dependencies: ['this', 'flashActData'], + compute: ({this: flash, flashActData}) => + flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, + }, + ], + date: simpleDate(), coverArtFileExtension: fileExtension('jpg'), @@ -90,17 +107,6 @@ export class Flash extends Thing { flashActData.find((act) => act.flashes.includes(flash)) ?? null, }, }, - - color: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'flashActData'], - - compute: ({this: flash, flashActData}) => - flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, - }, - }, }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/yaml.js b/src/data/yaml.js index 16303a64..f7856cb7 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -544,6 +544,7 @@ export const processFlashDocument = makeProcessDocument(T.Flash, { name: 'Flash', directory: 'Directory', page: 'Page', + color: 'Color', urls: 'URLs', date: 'Date', -- cgit 1.3.0-6-gf8a5 From c7e21005beb8807216aac6ed3ae54029575007a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 18:03:24 -0300 Subject: data: Track.withAlbum: bulkily match documented early exit behavior --- src/data/composite/things/track/withAlbum.js | 45 +++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) (limited to 'src/data') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 34845ab0..0e85cee9 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,10 +4,9 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; +import {empty} from '#sugar'; import {is} from '#validators'; -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - export default templateCompositeFrom({ annotation: `withAlbum`, @@ -21,13 +20,20 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ - raiseOutputWithoutDependency({ - dependency: 'albumData', - mode: input.value('empty'), - output: input.value({ - ['#album']: null, - }), - }), + { + dependencies: [input('notFoundMode'), 'albumData'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['albumData']: albumData, + }) => + (albumData === null + ? continuation.exit(null) + : empty(albumData) + ? (notFoundMode === 'exit' + ? continuation.exit(null) + : continuation.raiseOutput({'#album': null})) + : continuation()), + }, { dependencies: [input.myself(), 'albumData'], @@ -37,21 +43,20 @@ export default templateCompositeFrom({ }) => continuation({ ['#album']: - albumData.find(album => album.tracks.includes(track)), + albumData.find(album => album.tracks.includes(track)) + ?? null, }), }, - raiseOutputWithoutDependency({ - dependency: '#album', - output: input.value({ - ['#album']: null, - }), - }), - { - dependencies: ['#album'], - compute: (continuation, {'#album': album}) => - continuation.raiseOutput({'#album': album}), + dependencies: [input('notFoundMode'), '#album'], + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + ['#album']: album, + }) => + ((album === null && notFoundMode === 'exit') + ? continuation.exit(null) + : continuation.raiseOutput({'#album': album})), }, ], }); -- cgit 1.3.0-6-gf8a5 From ecac8276182436897dffff02aaf1a7d268738cba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 18:47:08 -0300 Subject: data: update withPropertyFromList --- src/data/composite/data/withPropertyFromList.js | 100 +++++++++++++++--------- 1 file changed, 63 insertions(+), 37 deletions(-) (limited to 'src/data') diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index 3ce05fdf..1983ebbc 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -16,41 +16,67 @@ // - withUnflattenedList // -import {empty} from '#sugar'; - -// todo: OUHHH THIS ONE'S NOT UPDATED YET LOL -export default function({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; +import {input, templateCompositeFrom} from '#composite'; + +function getOutputName({list, property, prefix}) { + if (!property) return `#values`; + if (prefix) return `${prefix}.${property}`; + if (list) return `${list}.${property}`; + return `#list.${property}`; } + +export default templateCompositeFrom({ + annotation: `withPropertyFromList`, + + inputs: { + list: input({type: 'array'}), + property: input({type: 'string'}), + prefix: input.staticValue({type: 'string', defaultValue: null}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => + [getOutputName({list, property, prefix})], + + steps: () => [ + { + dependencies: [input('list'), input('property')], + compute: (continuation, { + [input('list')]: list, + [input('property')]: property, + }) => continuation({ + ['#values']: + list.map(item => item[property] ?? null), + }), + }, + + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('property'), + input.staticValue('prefix'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('property')]: property, + [input.staticValue('prefix')]: prefix, + }) => continuation({ + ['#outputName']: + getOutputName({list, property, prefix}), + }), + }, + + { + dependencies: ['#values', '#outputName'], + compute: (continuation, { + ['#values']: values, + ['#outputName']: outputName, + }) => + continuation.raiseOutput({[outputName]: values}), + }, + ], +}); -- cgit 1.3.0-6-gf8a5 From 5f740046562c85ab4e00063037ddf3af5a545279 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:05:39 -0300 Subject: data, test: withResultOfAvailabilityCheck: index mode --- src/data/composite/control-flow/inputAvailabilityCheckMode.js | 2 +- src/data/composite/control-flow/withResultOfAvailabilityCheck.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src/data') diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js index d74a1149..8008fdeb 100644 --- a/src/data/composite/control-flow/inputAvailabilityCheckMode.js +++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js @@ -3,7 +3,7 @@ import {is} from '#validators'; export default function inputAvailabilityCheckMode() { return input({ - validate: is('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy', 'index'), defaultValue: 'null', }); } diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js index bcbd0b37..a6942014 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -10,6 +10,7 @@ // * 'falsy': Check that the value isn't false when treated as a boolean // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! +// * 'index': Check that the value is a number, and is at least zero. // // See also: // - exitWithoutDependency @@ -57,6 +58,10 @@ export default templateCompositeFrom({ case 'falsy': availability = !!value && (!Array.isArray(value) || !empty(value)); break; + + case 'index': + availability = typeof value === 'number' && value >= 0; + break; } return continuation({'#availability': availability}); -- cgit 1.3.0-6-gf8a5 From bb646f28853399a43d52a056c86d04f6a4343932 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:09:22 -0300 Subject: data: Track.withAlbum: refactor for clarity Utilizes availability checks instead of manual null comparisons and empty() calls, extracts track lists using withPropertyFromList, operates on index instead of unique album object where possible (including found / not found check). --- src/data/composite/things/track/withAlbum.js | 98 ++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 27 deletions(-) (limited to 'src/data') diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 0e85cee9..9c974cd1 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -4,9 +4,12 @@ // exit instead. import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; import {is} from '#validators'; +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + export default templateCompositeFrom({ annotation: `withAlbum`, @@ -20,43 +23,84 @@ export default templateCompositeFrom({ outputs: ['#album'], steps: () => [ + // null albumData is always an early exit. + + exitWithoutDependency({ + dependency: 'albumData', + mode: input.value('null'), + }), + + // empty albumData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'albumData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#albumDataAvailability', + }), + { - dependencies: [input('notFoundMode'), 'albumData'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumDataAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['albumData']: albumData, - }) => - (albumData === null - ? continuation.exit(null) - : empty(albumData) - ? (notFoundMode === 'exit' - ? continuation.exit(null) - : continuation.raiseOutput({'#album': null})) - : continuation()), + ['#albumDataAvailability']: albumDataIsAvailable, + }) { + if (albumDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, }, + withPropertyFromList({ + list: 'albumData', + property: input.value('tracks'), + }), + { - dependencies: [input.myself(), 'albumData'], + dependencies: [input.myself(), '#albumData.tracks'], compute: (continuation, { [input.myself()]: track, - ['albumData']: albumData, - }) => - continuation({ - ['#album']: - albumData.find(album => album.tracks.includes(track)) - ?? null, - }), + ['#albumData.tracks']: trackLists, + }) => continuation({ + ['#albumIndex']: + trackLists.findIndex(tracks => tracks.includes(track)), + }), }, + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#albumIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#albumAvailability', + }), + { - dependencies: [input('notFoundMode'), '#album'], - compute: (continuation, { + dependencies: [input('notFoundMode'), '#albumAvailability'], + compute(continuation, { [input('notFoundMode')]: notFoundMode, - ['#album']: album, - }) => - ((album === null && notFoundMode === 'exit') - ? continuation.exit(null) - : continuation.raiseOutput({'#album': album})), + ['#albumAvailability']: albumIsAvailable, + }) { + if (albumIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#album': null}); + } + }, + }, + + { + dependencies: ['albumData', '#albumIndex'], + compute: (continuation, { + ['albumData']: albumData, + ['#albumIndex']: albumIndex, + }) => continuation.raiseOutput({ + ['#album']: + albumData[albumIndex], + }), }, ], }); -- cgit 1.3.0-6-gf8a5 From 855dcdbc17831809cfb3c800d378c62186702740 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:30:40 -0300 Subject: data: Flash.withFlashAct --- src/data/composite/things/flash/index.js | 1 + src/data/composite/things/flash/withFlashAct.js | 108 ++++++++++++++++++++++++ src/data/composite/things/track/withAlbum.js | 2 + 3 files changed, 111 insertions(+) create mode 100644 src/data/composite/things/flash/index.js create mode 100644 src/data/composite/things/flash/withFlashAct.js (limited to 'src/data') diff --git a/src/data/composite/things/flash/index.js b/src/data/composite/things/flash/index.js new file mode 100644 index 00000000..63ac13da --- /dev/null +++ b/src/data/composite/things/flash/index.js @@ -0,0 +1 @@ +export {default as withFlashAct} from './withFlashAct.js'; diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js new file mode 100644 index 00000000..ada2dcfe --- /dev/null +++ b/src/data/composite/things/flash/withFlashAct.js @@ -0,0 +1,108 @@ +// Gets the flash's act. This will early exit if flashActData is missing. +// By default, if there's no flash whose list of flashes includes this flash, +// the output dependency will be null; set {notFoundMode: 'exit'} to early +// exit instead. +// +// This step models with Flash.withAlbum. + +import {input, templateCompositeFrom} from '#composite'; +import {is} from '#validators'; + +import {exitWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withFlashAct`, + + inputs: { + notFoundMode: input({ + validate: is('exit', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#flashAct'], + + steps: () => [ + // null flashActData is always an early exit. + + exitWithoutDependency({ + dependency: 'flashActData', + mode: input.value('null'), + }), + + // empty flashActData conditionally exits early or outputs null. + + withResultOfAvailabilityCheck({ + from: 'flashActData', + mode: input.value('empty'), + }).outputs({ + '#availability': '#flashActDataAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActDataAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActDataAvailability']: flashActDataIsAvailable, + }) { + if (flashActDataIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + withPropertyFromList({ + list: 'flashActData', + property: input.value('flashes'), + }), + + { + dependencies: [input.myself(), '#flashActData.flashes'], + compute: (continuation, { + [input.myself()]: track, + ['#flashActData.flashes']: flashLists, + }) => continuation({ + ['#flashActIndex']: + flashLists.findIndex(flashes => flashes.includes(track)), + }), + }, + + // album not found conditionally exits or outputs null. + + withResultOfAvailabilityCheck({ + from: '#flashActIndex', + mode: input.value('index'), + }).outputs({ + '#availability': '#flashActAvailability', + }), + + { + dependencies: [input('notFoundMode'), '#flashActAvailability'], + compute(continuation, { + [input('notFoundMode')]: notFoundMode, + ['#flashActAvailability']: flashActIsAvailable, + }) { + if (flashActIsAvailable) return continuation(); + switch (notFoundMode) { + case 'exit': return continuation.exit(null); + case 'null': return continuation.raiseOutput({'#flashAct': null}); + } + }, + }, + + { + dependencies: ['flashActData', '#flashActIndex'], + compute: (continuation, { + ['flashActData']: flashActData, + ['#flashActIndex']: flashActIndex, + }) => continuation.raiseOutput({ + ['#flashAct']: + flashActData[flashActIndex], + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js index 9c974cd1..cbd16dcd 100644 --- a/src/data/composite/things/track/withAlbum.js +++ b/src/data/composite/things/track/withAlbum.js @@ -2,6 +2,8 @@ // By default, if there's no album whose list of tracks includes this track, // the output dependency will be null; set {notFoundMode: 'exit'} to early // exit instead. +// +// This step models with Flash.withFlashAct. import {input, templateCompositeFrom} from '#composite'; import {is} from '#validators'; -- cgit 1.3.0-6-gf8a5 From 991386372f418b165a192a3ad36fb9b6cecc4d76 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 26 Oct 2023 19:32:14 -0300 Subject: data: Flash.color: replace erroneous syntax w/ composite definition --- src/data/things/flash.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'src/data') diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 71174931..e2afcef4 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -9,9 +9,9 @@ import { oneOf, } from '#validators'; -import { - exposeUpdateValueOrContinue, -} from '#composite/control-flow'; +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; import { color, @@ -26,6 +26,8 @@ import { wikiData, } from '#composite/wiki-properties'; +import {withFlashAct} from '#composite/things/flash'; + import Thing from './thing.js'; export class Flash extends Thing { @@ -67,12 +69,14 @@ export class Flash extends Thing { validate: input.value(isColor), }), - { - flags: {expose: true}, - dependencies: ['this', 'flashActData'], - compute: ({this: flash, flashActData}) => - flashActData.find((act) => act.flashes.includes(flash))?.color ?? null, - }, + withFlashAct(), + + withPropertyFromObject({ + object: '#flashAct', + property: input.value('color'), + }), + + exposeDependency({dependency: '#flashAct.color'}), ], date: simpleDate(), -- cgit 1.3.0-6-gf8a5