« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/cacheable-object.js (renamed from src/data/things/cacheable-object.js)0
-rw-r--r--src/data/composite.js (renamed from src/data/things/composite.js)0
-rw-r--r--src/data/thing.js (renamed from src/data/things/thing.js)20
-rw-r--r--src/data/things/album.js158
-rw-r--r--src/data/things/art-tag.js17
-rw-r--r--src/data/things/artist.js23
-rw-r--r--src/data/things/flash.js62
-rw-r--r--src/data/things/group.js21
-rw-r--r--src/data/things/homepage-layout.js39
-rw-r--r--src/data/things/index.js4
-rw-r--r--src/data/things/language.js11
-rw-r--r--src/data/things/news-entry.js28
-rw-r--r--src/data/things/static-page.js27
-rw-r--r--src/data/things/track.js148
-rw-r--r--src/data/things/wiki-info.js40
-rw-r--r--src/data/validators.js (renamed from src/data/things/validators.js)0
-rw-r--r--src/data/yaml.js156
17 files changed, 375 insertions, 379 deletions
diff --git a/src/data/things/cacheable-object.js b/src/data/cacheable-object.js
index 1e7c7aa8..1e7c7aa8 100644
--- a/src/data/things/cacheable-object.js
+++ b/src/data/cacheable-object.js
diff --git a/src/data/things/composite.js b/src/data/composite.js
index 113f0a4f..113f0a4f 100644
--- a/src/data/things/composite.js
+++ b/src/data/composite.js
diff --git a/src/data/things/thing.js b/src/data/thing.js
index e1f488ee..ae8e71af 100644
--- a/src/data/things/thing.js
+++ b/src/data/thing.js
@@ -3,10 +3,9 @@
 
 import {inspect} from 'node:util';
 
+import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
 
-import CacheableObject from './cacheable-object.js';
-
 export default class Thing extends CacheableObject {
   static referenceType = Symbol.for('Thing.referenceType');
   static friendlyName = Symbol.for('Thing.friendlyName');
@@ -45,28 +44,21 @@ export default class Thing extends CacheableObject {
     const superspec = thingClass[Thing.yamlDocumentSpec];
 
     const {
-      fieldTransformations,
-      propertyFieldMapping,
+      fields,
       ignoredFields,
       invalidFieldCombinations,
       ...restOfSubspec
     } = subspec;
 
-    const newFields =
-      Object.values(subspec.propertyFieldMapping ?? {});
+    const newFields = Object.keys(fields ?? {});
 
     return {
       ...superspec,
       ...restOfSubspec,
 
-      fieldTransformations: {
-        ...superspec.fieldTransformations,
-        ...fieldTransformations,
-      },
-
-      propertyFieldMapping: {
-        ...superspec.propertyFieldMapping,
-        ...propertyFieldMapping,
+      fields: {
+        ...superspec.fields ?? {},
+        ...fields,
       },
 
       ignoredFields:
diff --git a/src/data/things/album.js b/src/data/things/album.js
index 02d34544..3a05ac83 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -1,6 +1,9 @@
 import {input} from '#composite';
 import find from '#find';
+import Thing from '#thing';
 import {isDate} from '#validators';
+import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions}
+  from '#yaml';
 
 import {exposeDependency, exposeUpdateValueOrContinue}
   from '#composite/control-flow';
@@ -27,14 +30,6 @@ import {
 
 import {withTracks, withTrackSections} from '#composite/things/album';
 
-import {
-  parseAdditionalFiles,
-  parseContributors,
-  parseDimensions,
-} from '#yaml';
-
-import Thing from './thing.js';
-
 export class Album extends Thing {
   static [Thing.referenceType] = 'album';
 
@@ -205,58 +200,85 @@ export class Album extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    fieldTransformations: {
-      'Artists': parseContributors,
-      'Cover Artists': parseContributors,
-      'Default Track Cover Artists': parseContributors,
-      'Wallpaper Artists': parseContributors,
-      'Banner Artists': parseContributors,
-
-      'Date': (value) => new Date(value),
-      'Date Added': (value) => new Date(value),
-      'Cover Art Date': (value) => new Date(value),
-      'Default Track Cover Art Date': (value) => new Date(value),
-
-      'Banner Dimensions': parseDimensions,
-
-      'Additional Files': parseAdditionalFiles,
-    },
-
-    propertyFieldMapping: {
-      name: 'Album',
-      directory: 'Directory',
-      date: 'Date',
-      color: 'Color',
-      urls: 'URLs',
-
-      hasTrackNumbers: 'Has Track Numbers',
-      isListedOnHomepage: 'Listed on Homepage',
-      isListedInGalleries: 'Listed in Galleries',
-
-      coverArtDate: 'Cover Art Date',
-      trackArtDate: 'Default Track Cover Art Date',
-      dateAddedToWiki: 'Date Added',
-
-      coverArtFileExtension: 'Cover Art File Extension',
-      trackCoverArtFileExtension: 'Track Art File Extension',
-
-      wallpaperArtistContribs: 'Wallpaper Artists',
-      wallpaperStyle: 'Wallpaper Style',
-      wallpaperFileExtension: 'Wallpaper File Extension',
-
-      bannerArtistContribs: 'Banner Artists',
-      bannerStyle: 'Banner Style',
-      bannerFileExtension: 'Banner File Extension',
-      bannerDimensions: 'Banner Dimensions',
-
-      commentary: 'Commentary',
-      additionalFiles: 'Additional Files',
-
-      artistContribs: 'Artists',
-      coverArtistContribs: 'Cover Artists',
-      trackCoverArtistContribs: 'Default Track Cover Artists',
-      groups: 'Groups',
-      artTags: 'Art Tags',
+    fields: {
+      'Album': {property: 'name'},
+      'Directory': {property: 'directory'},
+
+      'Date': {
+        property: 'date',
+        transform: parseDate,
+      },
+
+      'Color': {property: 'color'},
+      'URLs': {property: 'urls'},
+
+      'Has Track Numbers': {property: 'hasTrackNumbers'},
+      'Listed on Homepage': {property: 'isListedOnHomepage'},
+      'Listed in Galleries': {property: 'isListedInGalleries'},
+
+      'Cover Art Date': {
+        property: 'coverArtDate',
+        transform: parseDate,
+      },
+
+      'Default Track Cover Art Date': {
+        property: 'trackArtDate',
+        transform: parseDate,
+      },
+
+      'Date Added': {
+        property: 'dateAddedToWiki',
+        transform: parseDate,
+      },
+
+      'Cover Art File Extension': {property: 'coverArtFileExtension'},
+      'Track Art File Extension': {property: 'trackCoverArtFileExtension'},
+
+      'Wallpaper Artists': {
+        property: 'wallpaperArtistContribs',
+        transform: parseContributors,
+      },
+
+      'Wallpaper Style': {property: 'wallpaperStyle'},
+      'Wallpaper File Extension': {property: 'wallpaperFileExtension'},
+
+      'Banner Artists': {
+        property: 'bannerArtistContribs',
+        transform: parseContributors,
+      },
+
+      'Banner Style': {property: 'bannerStyle'},
+      'Banner File Extension': {property: 'bannerFileExtension'},
+
+      'Banner Dimensions': {
+        property: 'bannerDimensions',
+        transform: parseDimensions,
+      },
+
+      'Commentary': {property: 'commentary'},
+
+      'Additional Files': {
+        property: 'additionalFiles',
+        transform: parseAdditionalFiles,
+      },
+
+      'Artists': {
+        property: 'artistContribs',
+        transform: parseContributors,
+      },
+
+      'Cover Artists': {
+        property: 'coverArtistContribs',
+        transform: parseContributors,
+      },
+
+      'Default Track Cover Artists': {
+        property: 'trackCoverArtistContribs',
+        transform: parseContributors,
+      },
+
+      'Groups': {property: 'groups'},
+      'Art Tags': {property: 'artTags'},
     },
 
     ignoredFields: ['Review Points'],
@@ -274,14 +296,14 @@ export class TrackSectionHelper extends Thing {
   })
 
   static [Thing.yamlDocumentSpec] = {
-    fieldTransformations: {
-      'Date Originally Released': (value) => new Date(value),
-    },
-
-    propertyFieldMapping: {
-      name: 'Section',
-      color: 'Color',
-      dateOriginallyReleased: 'Date Originally Released',
+    fields: {
+      'Section': {property: 'name'},
+      'Color': {property: 'color'},
+
+      'Date Originally Released': {
+        property: 'dateOriginallyReleased',
+        transform: parseDate,
+      },
     },
   };
 }
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index c0b4a6d6..af6677f0 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -1,6 +1,7 @@
 import {input} from '#composite';
-import {sortAlbumsTracksChronologically} from '#wiki-data';
+import Thing from '#thing';
 import {isName} from '#validators';
+import {sortAlbumsTracksChronologically} from '#wiki-data';
 
 import {exposeUpdateValueOrContinue} from '#composite/control-flow';
 
@@ -12,8 +13,6 @@ import {
   wikiData,
 } from '#composite/wiki-properties';
 
-import Thing from './thing.js';
-
 export class ArtTag extends Thing {
   static [Thing.referenceType] = 'tag';
   static [Thing.friendlyName] = `Art Tag`;
@@ -65,13 +64,13 @@ export class ArtTag extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Tag',
-      nameShort: 'Short Name',
-      directory: 'Directory',
+    fields: {
+      'Tag': {property: 'name'},
+      'Short Name': {property: 'nameShort'},
+      'Directory': {property: 'directory'},
 
-      color: 'Color',
-      isContentWarning: 'Is CW',
+      'Color': {property: 'color'},
+      'Is CW': {property: 'isContentWarning'},
     },
   };
 }
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 42090557..502510a8 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -1,8 +1,11 @@
 import {input} from '#composite';
 import find from '#find';
 import {unique} from '#sugar';
+import Thing from '#thing';
 import {isName, validateArrayItems} from '#validators';
 
+import {withReverseContributionList} from '#composite/wiki-data';
+
 import {
   contentString,
   directory,
@@ -16,10 +19,6 @@ import {
   wikiData,
 } from '#composite/wiki-properties';
 
-import {withReverseContributionList} from '#composite/wiki-data';
-
-import Thing from './thing.js';
-
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
 
@@ -242,16 +241,16 @@ export class Artist extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Artist',
-      directory: 'Directory',
-      urls: 'URLs',
-      contextNotes: 'Context Notes',
+    fields: {
+      'Artist': {property: 'name'},
+      'Directory': {property: 'directory'},
+      'URLs': {property: 'urls'},
+      'Context Notes': {property: 'contextNotes'},
 
-      hasAvatar: 'Has Avatar',
-      avatarFileExtension: 'Avatar File Extension',
+      'Has Avatar': {property: 'hasAvatar'},
+      'Avatar File Extension': {property: 'avatarFileExtension'},
 
-      aliasNames: 'Aliases',
+      'Aliases': {property: 'aliasNames'},
     },
 
     ignoredFields: ['Dead URLs', 'Review Points'],
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index d7e8bb46..7e859bfa 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -1,13 +1,8 @@
 import {input} from '#composite';
 import find from '#find';
-
-import {
-  anyOf,
-  isColor,
-  isDirectory,
-  isNumber,
-  isString,
-} from '#validators';
+import Thing from '#thing';
+import {anyOf, isColor, isDirectory, isNumber, isString} from '#validators';
+import {parseDate, parseContributors} from '#yaml';
 
 import {exposeDependency, exposeUpdateValueOrContinue}
   from '#composite/control-flow';
@@ -28,10 +23,6 @@ import {
 
 import {withFlashAct} from '#composite/things/flash';
 
-import {parseContributors} from '#yaml';
-
-import Thing from './thing.js';
-
 export class Flash extends Thing {
   static [Thing.referenceType] = 'flash';
 
@@ -137,24 +128,25 @@ export class Flash extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    fieldTransformations: {
-      'Date': (value) => new Date(value),
-
-      'Contributors': parseContributors,
-    },
-
-    propertyFieldMapping: {
-      name: 'Flash',
-      directory: 'Directory',
-      page: 'Page',
-      color: 'Color',
-      urls: 'URLs',
+    fields: {
+      'Flash': {property: 'name'},
+      'Directory': {property: 'directory'},
+      'Page': {property: 'page'},
+      'Color': {property: 'color'},
+      'URLs': {property: 'urls'},
+
+      'Date': {
+        property: 'date',
+        transform: parseDate,
+      },
 
-      date: 'Date',
-      coverArtFileExtension: 'Cover Art File Extension',
+      'Cover Art File Extension': {property: 'coverArtFileExtension'},
 
-      featuredTracks: 'Featured Tracks',
-      contributorContribs: 'Contributors',
+      'Featured Tracks': {property: 'featuredTracks'},
+      'Contributors': {
+        property: 'contributorContribs',
+        transform: parseContributors,
+      },
     },
 
     ignoredFields: ['Review Points'],
@@ -199,15 +191,15 @@ export class FlashAct extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Act',
-      directory: 'Directory',
+    fields: {
+      'Act': {property: 'name'},
+      'Directory': {property: 'directory'},
 
-      color: 'Color',
-      listTerminology: 'List Terminology',
+      'Color': {property: 'color'},
+      'List Terminology': {property: 'listTerminology'},
 
-      jump: 'Jump',
-      jumpColor: 'Jump Color',
+      'Jump': {property: 'jump'},
+      'Jump Color': {property: 'jumpColor'},
     },
 
     ignoredFields: ['Review Points'],
diff --git a/src/data/things/group.js b/src/data/things/group.js
index a9708fb4..fe04dfaa 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -1,5 +1,6 @@
 import {input} from '#composite';
 import find from '#find';
+import Thing from '#thing';
 
 import {
   color,
@@ -11,8 +12,6 @@ import {
   wikiData,
 } from '#composite/wiki-properties';
 
-import Thing from './thing.js';
-
 export class Group extends Thing {
   static [Thing.referenceType] = 'group';
 
@@ -87,13 +86,13 @@ export class Group extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Group',
-      directory: 'Directory',
-      description: 'Description',
-      urls: 'URLs',
+    fields: {
+      'Group': {property: 'name'},
+      'Directory': {property: 'directory'},
+      'Description': {property: 'description'},
+      'URLs': {property: 'urls'},
 
-      featuredAlbums: 'Featured Albums',
+      'Featured Albums': {property: 'featuredAlbums'},
     },
 
     ignoredFields: ['Review Points'],
@@ -126,9 +125,9 @@ export class GroupCategory extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Category',
-      color: 'Color',
+    fields: {
+      'Category': {property: 'name'},
+      'Color': {property: 'color'},
     },
   };
 }
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index b4fb97db..bd0970fe 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,5 +1,6 @@
 import {input} from '#composite';
 import find from '#find';
+import Thing from '#thing';
 
 import {
   anyOf,
@@ -14,16 +15,8 @@ import {
 
 import {exposeDependency} from '#composite/control-flow';
 import {withResolvedReference} from '#composite/wiki-data';
-
-import {
-  color,
-  contentString,
-  name,
-  referenceList,
-  wikiData,
-} from '#composite/wiki-properties';
-
-import Thing from './thing.js';
+import {color, contentString, name, referenceList, wikiData}
+  from '#composite/wiki-properties';
 
 export class HomepageLayout extends Thing {
   static [Thing.friendlyName] = `Homepage Layout`;
@@ -48,9 +41,9 @@ export class HomepageLayout extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      sidebarContent: 'Sidebar Content',
-      navbarLinks: 'Navbar Links',
+    fields: {
+      'Sidebar Content': {property: 'sidebarContent'},
+      'Navbar Links': {property: 'navbarLinks'},
     },
 
     ignoredFields: ['Homepage'],
@@ -93,10 +86,10 @@ export class HomepageLayoutRow extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Row',
-      color: 'Color',
-      type: 'Type',
+    fields: {
+      'Row': {property: 'name'},
+      'Color': {property: 'color'},
+      'Type': {property: 'type'},
     },
   };
 }
@@ -181,12 +174,12 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
   });
 
   static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(HomepageLayoutRow, {
-    propertyFieldMapping: {
-      displayStyle: 'Display Style',
-      sourceGroup: 'Group',
-      countAlbumsFromGroup: 'Count',
-      sourceAlbums: 'Albums',
-      actionLinks: 'Actions',
+    fields: {
+      'Display Style': {property: 'displayStyle'},
+      'Group': {property: 'sourceGroup'},
+      'Count': {property: 'countAlbumsFromGroup'},
+      'Albums': {property: 'sourceAlbums'},
+      'Actions': {property: 'actionLinks'},
     },
   });
 }
diff --git a/src/data/things/index.js b/src/data/things/index.js
index d1143b0a..9a36eaae 100644
--- a/src/data/things/index.js
+++ b/src/data/things/index.js
@@ -6,7 +6,7 @@ import {compositeFrom} from '#composite';
 import * as serialize from '#serialize';
 import {openAggregate, showAggregate} from '#sugar';
 
-import Thing from './thing.js';
+import Thing from '#thing';
 
 import * as albumClasses from './album.js';
 import * as artTagClasses from './art-tag.js';
@@ -20,8 +20,6 @@ import * as staticPageClasses from './static-page.js';
 import * as trackClasses from './track.js';
 import * as wikiInfoClasses from './wiki-info.js';
 
-export {default as Thing} from './thing.js';
-
 const allClassLists = {
   'album.js': albumClasses,
   'art-tag.js': artTagClasses,
diff --git a/src/data/things/language.js b/src/data/things/language.js
index b7841ace..c576a316 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -1,8 +1,10 @@
 import { Temporal, toTemporalInstant } from '@js-temporal/polyfill';
 
+import CacheableObject from '#cacheable-object';
 import * as html from '#html';
 import {empty, withAggregate} from '#sugar';
 import {isLanguageCode} from '#validators';
+import Thing from '#thing';
 
 import {
   getExternalLinkStringOfStyleFromDescriptors,
@@ -12,14 +14,7 @@ import {
   isExternalLinkStyle,
 } from '#external-links';
 
-import {
-  externalFunction,
-  flag,
-  name,
-} from '#composite/wiki-properties';
-
-import CacheableObject from './cacheable-object.js';
-import Thing from './thing.js';
+import {externalFunction, flag, name} from '#composite/wiki-properties';
 
 export class Language extends Thing {
   static [Thing.getPropertyDescriptors] = () => ({
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index 06dad629..5a022449 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -1,11 +1,8 @@
-import {
-  contentString,
-  directory,
-  name,
-  simpleDate,
-} from '#composite/wiki-properties';
+import Thing from '#thing';
+import {parseDate} from '#yaml';
 
-import Thing from './thing.js';
+import {contentString, directory, name, simpleDate}
+  from '#composite/wiki-properties';
 
 export class NewsEntry extends Thing {
   static [Thing.referenceType] = 'news-entry';
@@ -34,15 +31,16 @@ export class NewsEntry extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    fieldTransformations: {
-      'Date': (value) => new Date(value),
-    },
+    fields: {
+      'Name': {property: 'name'},
+      'Directory': {property: 'directory'},
+
+      'Date': {
+        property: 'date',
+        transform: parseDate,
+      },
 
-    propertyFieldMapping: {
-      name: 'Name',
-      directory: 'Directory',
-      date: 'Date',
-      content: 'Content',
+      'Content': {property: 'content'},
     },
   };
 }
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 00c0b09c..7f8b7c91 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -1,13 +1,8 @@
+import Thing from '#thing';
 import {isName} from '#validators';
 
-import {
-  contentString,
-  directory,
-  name,
-  simpleString,
-} from '#composite/wiki-properties';
-
-import Thing from './thing.js';
+import {contentString, directory, name, simpleString}
+  from '#composite/wiki-properties';
 
 export class StaticPage extends Thing {
   static [Thing.referenceType] = 'static';
@@ -35,14 +30,14 @@ export class StaticPage extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Name',
-      nameShort: 'Short Name',
-      directory: 'Directory',
-
-      stylesheet: 'Style',
-      script: 'Script',
-      content: 'Content',
+    fields: {
+      'Name': {property: 'name'},
+      'Short Name': {property: 'nameShort'},
+      'Directory': {property: 'directory'},
+
+      'Style': {property: 'stylesheet'},
+      'Script': {property: 'script'},
+      'Content': {property: 'content'},
     },
 
     ignoredFields: ['Review Points'],
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 3621510b..9f44bd8d 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -1,15 +1,20 @@
 import {inspect} from 'node:util';
 
+import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
 import {input} from '#composite';
 import find from '#find';
+import Thing from '#thing';
+import {isColor, isContributionList, isDate, isFileExtension}
+  from '#validators';
 
 import {
-  isColor,
-  isContributionList,
-  isDate,
-  isFileExtension,
-} from '#validators';
+  parseAdditionalFiles,
+  parseAdditionalNames,
+  parseContributors,
+  parseDate,
+  parseDuration,
+} from '#yaml';
 
 import {withPropertyFromObject} from '#composite/data';
 import {withResolvedContribs} from '#composite/wiki-data';
@@ -55,16 +60,6 @@ import {
   withPropertyFromAlbum,
 } from '#composite/things/track';
 
-import {
-  parseAdditionalFiles,
-  parseAdditionalNames,
-  parseContributors,
-  parseDuration,
-} from '#yaml';
-
-import CacheableObject from './cacheable-object.js';
-import Thing from './thing.js';
-
 export class Track extends Thing {
   static [Thing.referenceType] = 'track';
 
@@ -340,54 +335,83 @@ export class Track extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    fieldTransformations: {
-      'Additional Names': parseAdditionalNames,
-      'Duration': parseDuration,
-
-      'Date First Released': (value) => new Date(value),
-      'Cover Art Date': (value) => new Date(value),
-      'Has Cover Art': (value) =>
-        (value === true ? false :
-         value === false ? true :
-         value),
-
-      'Artists': parseContributors,
-      'Contributors': parseContributors,
-      'Cover Artists': parseContributors,
-
-      'Additional Files': parseAdditionalFiles,
-      'Sheet Music Files': parseAdditionalFiles,
-      'MIDI Project Files': parseAdditionalFiles,
-    },
+    fields: {
+      'Track': {property: 'name'},
+      'Directory': {property: 'directory'},
+
+      'Additional Names': {
+        property: 'additionalNames',
+        transform: parseAdditionalNames,
+      },
+
+      'Duration': {
+        property: 'duration',
+        transform: parseDuration,
+      },
+
+      'Color': {property: 'color'},
+      'URLs': {property: 'urls'},
+
+      'Date First Released': {
+        property: 'dateFirstReleased',
+        transform: parseDate,
+      },
+
+      'Cover Art Date': {
+        property: 'coverArtDate',
+        transform: parseDate,
+      },
+
+      'Cover Art File Extension': {property: 'coverArtFileExtension'},
+
+      'Has Cover Art': {
+        property: 'disableUniqueCoverArt',
+        transform: value =>
+          (typeof value === 'boolean'
+            ? !value
+            : value),
+      },
+
+      'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'},
+
+      'Lyrics': {property: 'lyrics'},
+      'Commentary': {property: 'commentary'},
+
+      'Additional Files': {
+        property: 'additionalFiles',
+        transform: parseAdditionalFiles,
+      },
+
+      'Sheet Music Files': {
+        property: 'sheetMusicFiles',
+        transform: parseAdditionalFiles,
+      },
+
+      'MIDI Project Files': {
+        property: 'midiProjectFiles',
+        transform: parseAdditionalFiles,
+      },
+
+      'Originally Released As': {property: 'originalReleaseTrack'},
+      'Referenced Tracks': {property: 'referencedTracks'},
+      'Sampled Tracks': {property: 'sampledTracks'},
+
+      'Artists': {
+        property: 'artistContribs',
+        transform: parseContributors,
+      },
+
+      'Contributors': {
+        property: 'contributorContribs',
+        transform: parseContributors,
+      },
+
+      'Cover Artists': {
+        property: 'coverArtistContribs',
+        transform: parseContributors,
+      },
 
-    propertyFieldMapping: {
-      name: 'Track',
-      directory: 'Directory',
-      additionalNames: 'Additional Names',
-      duration: 'Duration',
-      color: 'Color',
-      urls: 'URLs',
-
-      dateFirstReleased: 'Date First Released',
-      coverArtDate: 'Cover Art Date',
-      coverArtFileExtension: 'Cover Art File Extension',
-      disableUniqueCoverArt: 'Has Cover Art', // This gets transformed to flip true/false.
-
-      alwaysReferenceByDirectory: 'Always Reference By Directory',
-
-      lyrics: 'Lyrics',
-      commentary: 'Commentary',
-      additionalFiles: 'Additional Files',
-      sheetMusicFiles: 'Sheet Music Files',
-      midiProjectFiles: 'MIDI Project Files',
-
-      originalReleaseTrack: 'Originally Released As',
-      referencedTracks: 'Referenced Tracks',
-      sampledTracks: 'Sampled Tracks',
-      artistContribs: 'Artists',
-      contributorContribs: 'Contributors',
-      coverArtistContribs: 'Cover Artists',
-      artTags: 'Art Tags',
+      'Art Tags': {property: 'artTags'},
     },
 
     ignoredFields: ['Review Points'],
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index 80793550..fd6c239c 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -1,16 +1,10 @@
 import {input} from '#composite';
 import find from '#find';
+import Thing from '#thing';
 import {isColor, isLanguageCode, isName, isURL} from '#validators';
 
-import {
-  contentString,
-  flag,
-  name,
-  referenceList,
-  wikiData,
-} from '#composite/wiki-properties';
-
-import Thing from './thing.js';
+import {contentString, flag, name, referenceList, wikiData}
+  from '#composite/wiki-properties';
 
 export class WikiInfo extends Thing {
   static [Thing.friendlyName] = `Wiki Info`;
@@ -76,20 +70,20 @@ export class WikiInfo extends Thing {
   });
 
   static [Thing.yamlDocumentSpec] = {
-    propertyFieldMapping: {
-      name: 'Name',
-      nameShort: 'Short Name',
-      color: 'Color',
-      description: 'Description',
-      footerContent: 'Footer Content',
-      defaultLanguage: 'Default Language',
-      canonicalBase: 'Canonical Base',
-      divideTrackListsByGroups: 'Divide Track Lists By Groups',
-      enableFlashesAndGames: 'Enable Flashes & Games',
-      enableListings: 'Enable Listings',
-      enableNews: 'Enable News',
-      enableArtTagUI: 'Enable Art Tag UI',
-      enableGroupUI: 'Enable Group UI',
+    fields: {
+      'Name': {property: 'name'},
+      'Short Name': {property: 'nameShort'},
+      'Color': {property: 'color'},
+      'Description': {property: 'description'},
+      'Footer Content': {property: 'footerContent'},
+      'Default Language': {property: 'defaultLanguage'},
+      'Canonical Base': {property: 'canonicalBase'},
+      'Divide Track Lists By Groups': {property: 'divideTrackListsByGroups'},
+      'Enable Flashes & Games': {property: 'enableFlashesAndGames'},
+      'Enable Listings': {property: 'enableListings'},
+      'Enable News': {property: 'enableNews'},
+      'Enable Art Tag UI': {property: 'enableArtTagUI'},
+      'Enable Group UI': {property: 'enableGroupUI'},
     },
   };
 }
diff --git a/src/data/things/validators.js b/src/data/validators.js
index efe76fe0..efe76fe0 100644
--- a/src/data/things/validators.js
+++ b/src/data/validators.js
diff --git a/src/data/yaml.js b/src/data/yaml.js
index a232970b..19f56292 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -12,8 +12,8 @@ import CacheableObject, {CacheableObjectPropertyValueError}
 import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli';
 import find, {bindFind} from '#find';
 import {traverse} from '#node-utils';
-
-import T, {Thing} from '#things';
+import Thing from '#thing';
+import T from '#things';
 
 import {
   annotateErrorWithFile,
@@ -28,6 +28,7 @@ import {
   showAggregate,
   typeAppearance,
   withAggregate,
+  withEntries,
 } from '#sugar';
 
 import {
@@ -65,76 +66,68 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page';
 // makeProcessDocument is a factory function: the returned function will take a
 // document and apply the configuration passed to makeProcessDocument in order
 // to construct a Thing subclass.
-function makeProcessDocument(
-  thingConstructor,
-  {
-    // Optional early step for transforming field values before providing them
-    // to the Thing's update() method. This is useful when the input format
-    // (i.e. values in the document) differ from the format the actual Thing
-    // expects.
-    //
-    // Each key and value are a field name (not an update() property) and a
-    // function which takes the value for that field and returns the value which
-    // will be passed on to update().
-    //
-    fieldTransformations = {},
-
-    // Mapping of Thing.update() source properties to field names.
-    //
-    // Note this is property -> field, not field -> property. This is a
-    // shorthand convenience because properties are generally typical
-    // camel-cased JS properties, while fields may contain whitespace and be
-    // more easily represented as quoted strings.
-    //
-    propertyFieldMapping,
-
-    // Completely ignored fields. These won't throw an unknown field error if
-    // they're present in a document, but they won't be used for Thing property
-    // generation, either. Useful for stuff that's present in data files but not
-    // yet implemented as part of a Thing's data model!
-    //
-    ignoredFields = [],
-
-    // List of fields which are invalid when coexisting in a document.
-    // Data objects are generally allowing with regards to what properties go
-    // together, allowing for properties to be set separately from each other
-    // instead of complaining about invalid or unused-data cases. But it's
-    // useful to see these kinds of errors when actually validating YAML files!
-    //
-    // Each item of this array should itself be an object with a descriptive
-    // message and a list of fields. Of those fields, none should ever coexist
-    // with any other. For example:
-    //
-    //   [
-    //     {message: '...', fields: ['A', 'B', 'C']},
-    //     {message: '...', fields: ['C', 'D']},
-    //   ]
-    //
-    // ...means A can't coexist with B or C, B can't coexist with A or C, and
-    // C can't coexist iwth A, B, or D - but it's okay for D to coexist with
-    // A or B.
-    //
-    invalidFieldCombinations = [],
-  }
-) {
+//
+function makeProcessDocument(thingConstructor, {
+  // The bulk of configuration happens here in the spec's `fields` property.
+  // Each key is a field that's expected on the source document; fields that
+  // don't match one of these keys will cause an error. Values are object
+  // entries describing what to do with the field.
+  //
+  // A field entry's `property` tells what property the value for this field
+  // will be put into, on the respective Thing (subclass) instance.
+  //
+  // A field entry's `transform` optionally allows converting the raw value in
+  // YAML into some other format before providing setting it on the Thing
+  // instance.
+  //
+  fields: fieldSpecs = {},
+
+  // Completely ignored fields. These won't throw an unknown field error if
+  // they're present in a document, but they won't be used for Thing property
+  // generation, either. Useful for stuff that's present in data files but not
+  // yet implemented as part of a Thing's data model!
+  //
+  ignoredFields = [],
+
+  // List of fields which are invalid when coexisting in a document.
+  // Data objects are generally allowing with regards to what properties go
+  // together, allowing for properties to be set separately from each other
+  // instead of complaining about invalid or unused-data cases. But it's
+  // useful to see these kinds of errors when actually validating YAML files!
+  //
+  // Each item of this array should itself be an object with a descriptive
+  // message and a list of fields. Of those fields, none should ever coexist
+  // with any other. For example:
+  //
+  //   [
+  //     {message: '...', fields: ['A', 'B', 'C']},
+  //     {message: '...', fields: ['C', 'D']},
+  //   ]
+  //
+  // ...means A can't coexist with B or C, B can't coexist with A or C, and
+  // C can't coexist iwth A, B, or D - but it's okay for D to coexist with
+  // A or B.
+  //
+  invalidFieldCombinations = [],
+}) {
   if (!thingConstructor) {
     throw new Error(`Missing Thing class`);
   }
 
-  if (!propertyFieldMapping) {
-    throw new Error(`Expected propertyFieldMapping to be provided`);
+  if (!fieldSpecs) {
+    throw new Error(`Expected fields to be provided`);
   }
 
-  const knownFields = Object.values(propertyFieldMapping);
+  const knownFields = Object.keys(fieldSpecs);
 
-  // Invert the property-field mapping, since it'll come in handy for
-  // assigning update() source values later.
-  const fieldPropertyMapping = Object.fromEntries(
-    Object.entries(propertyFieldMapping)
-      .map(([property, field]) => [field, property]));
+  const propertyToField =
+    withEntries(fieldSpecs, entries => entries
+      .map(([field, {property}]) => [property, field]));
 
+  // TODO: Is this function even necessary??
+  // Aren't we doing basically the same work in the function it's decorating???
   const decorateErrorWithName = (fn) => {
-    const nameField = propertyFieldMapping['name'];
+    const nameField = propertyToField.name;
     if (!nameField) return fn;
 
     return (document) => {
@@ -151,7 +144,7 @@ function makeProcessDocument(
   };
 
   return decorateErrorWithName((document) => {
-    const nameField = propertyFieldMapping['name'];
+    const nameField = propertyToField.name;
     const namePart =
       (nameField
         ? (document[nameField]
@@ -192,7 +185,8 @@ function makeProcessDocument(
     const fieldCombinationErrors = [];
 
     for (const {message, fields} of invalidFieldCombinations) {
-      const fieldsPresent = presentFields.filter(field => fields.includes(field));
+      const fieldsPresent =
+        presentFields.filter(field => fields.includes(field));
 
       if (fieldsPresent.length >= 2) {
         const filteredDocument =
@@ -201,7 +195,8 @@ function makeProcessDocument(
             fieldsPresent,
             {preserveOriginalOrder: true});
 
-        fieldCombinationErrors.push(new FieldCombinationError(filteredDocument, message));
+        fieldCombinationErrors.push(
+          new FieldCombinationError(filteredDocument, message));
 
         for (const field of Object.keys(filteredDocument)) {
           skippedFields.add(field);
@@ -220,8 +215,8 @@ function makeProcessDocument(
 
       // This variable would like to certify itself as "not into capitalism".
       let propertyValue =
-        (Object.hasOwn(fieldTransformations, field)
-          ? fieldTransformations[field](documentValue)
+        (fieldSpecs[field].transform
+          ? fieldSpecs[field].transform(documentValue)
           : documentValue);
 
       // Completely blank items in a YAML list are read as null.
@@ -247,19 +242,13 @@ function makeProcessDocument(
       fieldValues[field] = propertyValue;
     }
 
-    const sourceProperties = {};
-
-    for (const [field, value] of Object.entries(fieldValues)) {
-      const property = fieldPropertyMapping[field];
-      sourceProperties[property] = value;
-    }
-
     const thing = Reflect.construct(thingConstructor, []);
 
     const fieldValueErrors = [];
 
-    for (const [property, value] of Object.entries(sourceProperties)) {
-      const field = propertyFieldMapping[property];
+    for (const [field, value] of Object.entries(fieldValues)) {
+      const {property} = fieldSpecs[field];
+
       try {
         thing[property] = value;
       } catch (caughtError) {
@@ -382,6 +371,10 @@ export class SkippedFieldsSummaryError extends Error {
 
 // --> Utilities shared across document parsing functions
 
+export function parseDate(date) {
+  return new Date(date);
+}
+
 export function parseDuration(string) {
   if (typeof string !== 'string') {
     return string;
@@ -779,7 +772,7 @@ export const getDataSteps = () => [
         case 'albums':
           return T.HomepageLayoutAlbumsRow;
         default:
-          throw new TypeError(`No processDocument function for row type ${type}!`);
+          throw new TypeError(`No processDocument function for row type ${document['Type']}!`);
       }
     },
 
@@ -1574,9 +1567,12 @@ export function filterReferenceErrors(wikiData) {
               return false;
             }, fn);
 
+            const {fields} = thing.constructor[Thing.yamlDocumentSpec];
+
             const field =
-              thing.constructor[Thing.yamlDocumentSpec]
-                .propertyFieldMapping[property];
+              Object.entries(fields ?? {})
+                .find(([field, fieldSpec]) => fieldSpec.property === property)
+                ?.[0];
 
             const fieldPropertyMessage =
               (field