« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/data/things/thing.js142
-rw-r--r--src/data/things/track.js290
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);