« get me outta code hell

find, data: move find specs into Thing subclasses - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-01-30 14:04:41 -0400
committer(quasar) nebula <qznebula@protonmail.com>2024-01-30 14:30:06 -0400
commitad146774480bb29cff04b50d887e132f3bd3f64f (patch)
tree9c789f74cf21793d9e3672f92628aa237f61eec3
parent7fbc5b87ed05bce433ed959ca18119b72835ee41 (diff)
find, data: move find specs into Thing subclasses
-rw-r--r--src/data/thing.js1
-rw-r--r--src/data/things/album.js7
-rw-r--r--src/data/things/art-tag.js12
-rw-r--r--src/data/things/artist.js61
-rw-r--r--src/data/things/flash.js14
-rw-r--r--src/data/things/group.js7
-rw-r--r--src/data/things/news-entry.js7
-rw-r--r--src/data/things/static-page.js7
-rw-r--r--src/data/things/track.js29
-rw-r--r--src/find.js215
10 files changed, 223 insertions, 137 deletions
diff --git a/src/data/thing.js b/src/data/thing.js
index 028c0dc..706e893 100644
--- a/src/data/thing.js
+++ b/src/data/thing.js
@@ -13,6 +13,7 @@ export default class Thing extends CacheableObject {
   static getPropertyDescriptors = Symbol.for('Thing.getPropertyDescriptors');
   static getSerializeDescriptors = Symbol.for('Thing.getSerializeDescriptors');
 
+  static findSpecs = Symbol.for('Thing.findSpecs');
   static yamlDocumentSpec = Symbol.for('Thing.yamlDocumentSpec');
   static getYamlLoadingSpec = Symbol.for('Thing.getYamlLoadingSpec');
 
diff --git a/src/data/things/album.js b/src/data/things/album.js
index 318979c..c5ef444 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -207,6 +207,13 @@ export class Album extends Thing {
     commentatorArtists: S.toRefs,
   });
 
+  static [Thing.findSpecs] = {
+    album: {
+      referenceTypes: ['album', 'album-commentary', 'album-gallery'],
+      bindTo: 'albumData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Album': {property: 'name'},
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index 4d423db..69fbb52 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -65,6 +65,18 @@ export class ArtTag extends Thing {
     },
   });
 
+  static [Thing.findSpecs] = {
+    artTag: {
+      referenceTypes: ['tag'],
+      bindTo: 'artTagData',
+
+      getMatchableNames: tag =>
+        (tag.isContentWarning
+          ? [`cw: ${tag.name}`]
+          : [tag.name]),
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Tag': {property: 'name'},
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index a12dc96..589eca9 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -9,7 +9,7 @@ import find from '#find';
 import {stitchArrays, unique} from '#sugar';
 import Thing from '#thing';
 import {isName, validateArrayItems} from '#validators';
-import {sortAlphabetically} from '#wiki-data';
+import {getKebabCase, sortAlphabetically} from '#wiki-data';
 
 import {withReverseContributionList} from '#composite/wiki-data';
 
@@ -28,6 +28,7 @@ import {
 
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
+  static [Thing.wikiDataArray] = 'artistData';
 
   static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({
     // Update & expose
@@ -247,6 +248,64 @@ export class Artist extends Thing {
     flashesAsContributor: S.toRefs,
   });
 
+  static [Thing.findSpecs] = {
+    artist: {
+      referenceTypes: ['artist', 'artist-gallery'],
+      bindTo: 'artistData',
+
+      include: artist => !artist.isAlias,
+    },
+
+    artistIncludingAliases: {
+      referenceTypes: ['artist', 'artist-gallery'],
+      bindTo: 'artistData',
+
+      getMatchableDirectories(artist) {
+        // Regular artists are always matchable by their directory.
+        if (!artist.isAlias) {
+          return [artist.directory];
+        }
+
+        const originalArtist = artist.aliasedArtist;
+
+        // Aliases never match by the same directory as the original.
+        if (artist.directory === originalArtist.directory) {
+          return [];
+        }
+
+        // Aliases never match by the same directory as some *previous* alias
+        // in the original's alias list. This is honestly a bit awkward, but it
+        // avoids artist aliases conflicting with each other when checking for
+        // duplicate directories.
+        for (const aliasName of originalArtist.aliasNames) {
+          // These are trouble. We should be accessing aliases' directories
+          // directly, but artists currently don't expose a reverse reference
+          // list for aliases. (This is pending a cleanup of "reverse reference"
+          // behavior in general.) It doesn't actually cause problems *here*
+          // because alias directories are computed from their names 100% of the
+          // time, but that *is* an assumption this code makes.
+          if (aliasName === artist.name) continue;
+          if (artist.directory === getKebabCase(aliasName)) {
+            return [];
+          }
+        }
+
+        // And, aliases never return just a blank string. This part is pretty
+        // spooky because it doesn't handle two differently named aliases, on
+        // different artists, who have names that are similar *apart* from a
+        // character that's shortened. But that's also so fundamentally scary
+        // that we can't support it properly with existing code, anyway - we
+        // would need to be able to specifically set a directory *on an alias,*
+        // which currently can't be done in YAML data files.
+        if (artist.directory === '') {
+          return [];
+        }
+
+        return [artist.directory];
+      },
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Artist': {property: 'name'},
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 9365354..4823f72 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -130,6 +130,13 @@ export class Flash extends Thing {
     color: S.id,
   });
 
+  static [Thing.findSpecs] = {
+    flash: {
+      referenceTypes: ['flash'],
+      bindTo: 'flashData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Flash': {property: 'name'},
@@ -193,6 +200,13 @@ export class FlashAct extends Thing {
     }),
   });
 
+  static [Thing.findSpecs] = {
+    flashAct: {
+      referenceTypes: ['flash-act'],
+      bindTo: 'flashActData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Act': {property: 'name'},
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 462c928..b6fba79 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -87,6 +87,13 @@ export class Group extends Thing {
     },
   });
 
+  static [Thing.findSpecs] = {
+    group: {
+      referenceTypes: ['group', 'group-gallery'],
+      bindTo: 'groupData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Group': {property: 'name'},
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index e0ec7f7..cb8e364 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -33,6 +33,13 @@ export class NewsEntry extends Thing {
     },
   });
 
+  static [Thing.findSpecs] = {
+    newsEntry: {
+      referenceTypes: ['news-entry'],
+      bindTo: 'newsData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Name': {property: 'name'},
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 9c7fec6..69cbfa1 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -35,6 +35,13 @@ export class StaticPage extends Thing {
     script: simpleString(),
   });
 
+  static [Thing.findSpecs] = {
+    staticPage: {
+      referenceTypes: ['static'],
+      bindTo: 'staticPageData',
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Name': {property: 'name'},
diff --git a/src/data/things/track.js b/src/data/things/track.js
index d1a12aa..5a8bec3 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -451,6 +451,35 @@ export class Track extends Thing {
     ],
   };
 
+  static [Thing.findSpecs] = {
+    track: {
+      referenceTypes: ['track'],
+      bindTo: 'trackData',
+
+      getMatchableNames: track =>
+        (track.alwaysReferenceByDirectory
+          ? []
+          : [track.name]),
+    },
+
+    trackOriginalReleasesOnly: {
+      referenceTypes: ['track'],
+      bindTo: 'trackData',
+
+      include: track =>
+        !CacheableObject.getUpdateValue(track, 'originalReleaseTrack'),
+
+      // It's still necessary to check alwaysReferenceByDirectory here, since
+      // it may be set manually (with `Always Reference By Directory: true`),
+      // and these shouldn't be matched by name (as per usual).
+      // See the definition for that property for more information.
+      getMatchableNames: track =>
+        (track.alwaysReferenceByDirectory
+          ? []
+          : [track.name]),
+    },
+  };
+
   // Track YAML loading is handled in album.js.
   static [Thing.getYamlLoadingSpec] = null;
 
diff --git a/src/find.js b/src/find.js
index fecf1ab..81f910d 100644
--- a/src/find.js
+++ b/src/find.js
@@ -2,8 +2,8 @@ import {inspect} from 'node:util';
 
 import CacheableObject from '#cacheable-object';
 import {colors, logWarn} from '#cli';
+import thingConstructors from '#things';
 import {typeAppearance} from '#sugar';
-import {getKebabCase} from '#wiki-data';
 
 function warnOrThrow(mode, message) {
   if (mode === 'error') {
@@ -149,125 +149,75 @@ function findHelper({
   };
 }
 
-const find = {
-  album: findHelper({
-    referenceTypes: ['album', 'album-commentary', 'album-gallery'],
-  }),
-
-  artist: findHelper({
-    referenceTypes: ['artist', 'artist-gallery'],
-
-    include: artist => !artist.isAlias,
-  }),
+const hardcodedFindSpecs = {
+  // Listings aren't Thing objects, so this find spec isn't provided by any
+  // Thing constructor.
+  listing: {
+    referenceTypes: ['listing'],
+    bindTo: 'listingSpec',
+  },
+};
 
-  artistIncludingAliases: findHelper({
-    referenceTypes: ['artist', 'artist-gallery'],
+export function getAllFindSpecs(key) {
+  try {
+    thingConstructors;
+  } catch (error) {
+    throw new Error(`Thing constructors aren't ready yet, can't get all find specs`);
+  }
 
-    getMatchableDirectories(artist) {
-      // Regular artists are always matchable by their directory.
-      if (!artist.isAlias) {
-        return [artist.directory];
-      }
+  const findSpecs = {...hardcodedFindSpecs};
 
-      const originalArtist = artist.aliasedArtist;
+  for (const thingConstructor of Object.values(thingConstructors)) {
+    const thingFindSpecs = thingConstructor[Symbol.for('Thing.findSpecs')];
+    if (!thingFindSpecs) continue;
 
-      // Aliases never match by the same directory as the original.
-      if (artist.directory === originalArtist.directory) {
-        return [];
-      }
+    Object.assign(findSpecs, thingFindSpecs);
+  }
 
-      // Aliases never match by the same directory as some *previous* alias
-      // in the original's alias list. This is honestly a bit awkward, but it
-      // avoids artist aliases conflicting with each other when checking for
-      // duplicate directories.
-      for (const aliasName of originalArtist.aliasNames) {
-        // These are trouble. We should be accessing aliases' directories
-        // directly, but artists currently don't expose a reverse reference
-        // list for aliases. (This is pending a cleanup of "reverse reference"
-        // behavior in general.) It doesn't actually cause problems *here*
-        // because alias directories are computed from their names 100% of the
-        // time, but that *is* an assumption this code makes.
-        if (aliasName === artist.name) continue;
-        if (artist.directory === getKebabCase(aliasName)) {
-          return [];
-        }
-      }
+  return findSpecs;
+}
 
-      // And, aliases never return just a blank string. This part is pretty
-      // spooky because it doesn't handle two differently named aliases, on
-      // different artists, who have names that are similar *apart* from a
-      // character that's shortened. But that's also so fundamentally scary
-      // that we can't support it properly with existing code, anyway - we
-      // would need to be able to specifically set a directory *on an alias,*
-      // which currently can't be done in YAML data files.
-      if (artist.directory === '') {
-        return [];
-      }
+export function findFindSpec(key) {
+  if (Object.hasOwn(hardcodedFindSpecs, key)) {
+    return hardcodedFindSpecs[key];
+  }
 
-      return [artist.directory];
-    },
-  }),
+  try {
+    thingConstructors;
+  } catch (error) {
+    throw new Error(`Thing constructors aren't ready yet, can't check if "find.${key}" available`);
+  }
 
-  artTag: findHelper({
-    referenceTypes: ['tag'],
+  for (const thingConstructor of Object.values(thingConstructors)) {
+    const thingFindSpecs = thingConstructor[Symbol.for('Thing.findSpecs')];
+    if (!thingFindSpecs) continue;
 
-    getMatchableNames: tag =>
-      (tag.isContentWarning
-        ? [`cw: ${tag.name}`]
-        : [tag.name]),
-  }),
+    if (Object.hasOwn(thingFindSpecs, key)) {
+      return thingFindSpecs[key];
+    }
+  }
 
-  flash: findHelper({
-    referenceTypes: ['flash'],
-  }),
+  throw new Error(`"find.${key}" isn't available`);
+}
 
-  flashAct: findHelper({
-    referenceTypes: ['flash-act'],
-  }),
+export default new Proxy({}, {
+  get: (store, key) => {
+    if (!Object.hasOwn(store, key)) {
+      let behavior = (...args) => {
+        // This will error if the find spec isn't available...
+        const findSpec = findFindSpec(key);
 
-  group: findHelper({
-    referenceTypes: ['group', 'group-gallery'],
-  }),
+        // ...or, if it is available, replace this function with the
+        // ready-for-use find function made out of that find spec.
+        return (behavior = findHelper(findSpec))(...args);
+      };
 
-  listing: findHelper({
-    referenceTypes: ['listing'],
-  }),
-
-  newsEntry: findHelper({
-    referenceTypes: ['news-entry'],
-  }),
-
-  staticPage: findHelper({
-    referenceTypes: ['static'],
-  }),
-
-  track: findHelper({
-    referenceTypes: ['track'],
-
-    getMatchableNames: track =>
-      (track.alwaysReferenceByDirectory
-        ? []
-        : [track.name]),
-  }),
-
-  trackOriginalReleasesOnly: findHelper({
-    referenceTypes: ['track'],
-
-    include: track =>
-      !CacheableObject.getUpdateValue(track, 'originalReleaseTrack'),
-
-    // It's still necessary to check alwaysReferenceByDirectory here, since it
-    // may be set manually (with the `Always Reference By Directory` field), and
-    // these shouldn't be matched by name (as per usual). See the definition for
-    // that property for more information.
-    getMatchableNames: track =>
-      (track.alwaysReferenceByDirectory
-        ? []
-        : [track.name]),
-  }),
-};
+      store[key] = (...args) => behavior(...args);
+    }
 
-export default find;
+    return store[key];
+  },
+});
 
 // Handy utility function for binding the find.thing() functions to a complete
 // wikiData object, optionally taking default options to provide to the find
@@ -275,34 +225,27 @@ export default find;
 // called, so if their values change, you'll have to continue with a fresh call
 // to bindFind.
 export function bindFind(wikiData, opts1) {
-  return Object.fromEntries(
-    Object.entries({
-      album: 'albumData',
-      artist: 'artistData',
-      artTag: 'artTagData',
-      flash: 'flashData',
-      flashAct: 'flashActData',
-      group: 'groupData',
-      listing: 'listingSpec',
-      newsEntry: 'newsData',
-      staticPage: 'staticPageData',
-      track: 'trackData',
-      trackOriginalReleasesOnly: 'trackData',
-    }).map(([key, value]) => {
-      const findFn = find[key];
-      const thingData = wikiData[value];
-      return [
-        key,
-        opts1
-          ? (ref, opts2) =>
-              opts2
-                ? findFn(ref, thingData, {...opts1, ...opts2})
-                : findFn(ref, thingData, opts1)
-          : (ref, opts2) =>
-              opts2
-                ? findFn(ref, thingData, opts2)
-                : findFn(ref, thingData),
-      ];
-    })
-  );
+  const findSpecs = getAllFindSpecs();
+
+  const boundFindFns = {};
+
+  for (const [key, spec] of Object.entries(findSpecs)) {
+    if (!spec.bindTo) continue;
+
+    const findFn = findHelper(spec);
+    const thingData = wikiData[spec.bindTo];
+
+    boundFindFns[key] =
+      (opts1
+        ? (ref, opts2) =>
+            (opts2
+              ? findFn(ref, thingData, {...opts1, ...opts2})
+              : findFn(ref, thingData, opts1))
+        : (ref, opts2) =>
+            (opts2
+              ? findFn(ref, thingData, opts2)
+              : findFn(ref, thingData)));
+  }
+
+  return boundFindFns;
 }