« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/data/things/album.js66
-rw-r--r--src/data/things/composite.js127
-rw-r--r--src/data/things/thing.js11
-rw-r--r--src/data/things/track.js16
4 files changed, 159 insertions, 61 deletions
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}),
   ]);
 }