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(-) 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