« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/things/thing.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/thing.js')
-rw-r--r--src/data/things/thing.js554
1 files changed, 293 insertions, 261 deletions
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>(?<who>.*?):<\/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>(?<who>.*?):<\/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: [],