diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2022-02-12 17:38:27 -0400 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2022-02-12 17:40:01 -0400 |
commit | 95bd943d62473e53de11cd6368e540cd48e4231a (patch) | |
tree | a6e1f0a21222eaeb01a270d8202a31616903649b /src/thing | |
parent | cfa02cb03a363c46408db7f0ec54bd3a7e4ad018 (diff) |
bam (Thing subclasses: several steps, one file)
Diffstat (limited to 'src/thing')
-rw-r--r-- | src/thing/album.js | 270 | ||||
-rw-r--r-- | src/thing/art-tag.js | 37 | ||||
-rw-r--r-- | src/thing/artist.js | 48 | ||||
-rw-r--r-- | src/thing/cacheable-object.js | 305 | ||||
-rw-r--r-- | src/thing/flash.js | 129 | ||||
-rw-r--r-- | src/thing/group.js | 73 | ||||
-rw-r--r-- | src/thing/homepage-layout.js | 99 | ||||
-rw-r--r-- | src/thing/news-entry.js | 49 | ||||
-rw-r--r-- | src/thing/static-page.js | 52 | ||||
-rw-r--r-- | src/thing/structures.js | 1 | ||||
-rw-r--r-- | src/thing/thing.js | 62 | ||||
-rw-r--r-- | src/thing/track.js | 173 | ||||
-rw-r--r-- | src/thing/validators.js | 327 | ||||
-rw-r--r-- | src/thing/wiki-info.js | 90 |
14 files changed, 0 insertions, 1715 deletions
diff --git a/src/thing/album.js b/src/thing/album.js deleted file mode 100644 index ba75352d..00000000 --- a/src/thing/album.js +++ /dev/null @@ -1,270 +0,0 @@ -import CacheableObject from './cacheable-object.js'; -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDimensions, - isDirectory, - isInstance, - isFileExtension, - isName, - isURL, - isString, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, -} from './validators.js'; - -import Artist from './artist.js'; -import ArtTag from './art-tag.js'; -import Track from './track.js'; - -import find from '../util/find.js'; - -export class TrackGroup extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Track Group', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - dateOriginallyReleased: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - tracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - isDefaultTrackGroup: { - flags: {update: true, expose: true}, - update: {validate: isBoolean} - }, - - // Update only - - trackData: { - flags: {update: true}, - update: {validate: validateArrayItems(item => isInstance(item, Track))} - }, - - // Expose only - - tracks: { - flags: {expose: true}, - - expose: { - dependencies: ['tracksByRef', 'trackData'], - compute: ({ tracksByRef, trackData }) => ( - (tracksByRef && trackData - ? (tracksByRef - .map(ref => find.track(ref, {wikiData: {trackData}})) - .filter(Boolean)) - : []) - ) - } - }, - }; -} - -export default class Album extends Thing { - static [Thing.referenceType] = 'album'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Album', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - urls: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(isURL) - } - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - trackArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - dateAddedToWiki: { - flags: {update: true, expose: true}, - - update: {validate: isDate} - }, - - artistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - coverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - trackCoverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - wallpaperArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - bannerArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - groupsByRef: { - flags: {update: true, expose: true}, - - update: { - validate: validateReferenceList('group') - } - }, - - artTagsByRef: { - flags: {update: true, expose: true}, - - update: { - validate: validateReferenceList('tag') - } - }, - - trackGroups: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(validateInstanceOf(TrackGroup)) - } - }, - - wallpaperStyle: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - wallpaperFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - bannerStyle: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - bannerFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - bannerDimensions: { - flags: {update: true, expose: true}, - update: {validate: isDimensions} - }, - - hasTrackArt: { - flags: {update: true, expose: true}, - - update: { - default: true, - validate: isBoolean - } - }, - - isMajorRelease: { - flags: {update: true, expose: true}, - - update: { - default: false, - validate: isBoolean - } - }, - - isListedOnHomepage: { - flags: {update: true, expose: true}, - - update: { - default: true, - validate: isBoolean - } - }, - - commentary: { - flags: {update: true, expose: true}, - update: {validate: isCommentary} - }, - - // Update only - - artistData: Thing.genWikiDataProperty(Artist), - trackData: Thing.genWikiDataProperty(Track), - - // Expose only - - // Previously known as: (album).artists - artistContribs: { - flags: {expose: true}, - expose: Thing.genContribsExpose('artistContribsByRef') - }, - - tracks: { - flags: {expose: true}, - - expose: { - dependencies: ['trackGroups', 'trackData'], - compute: ({ trackGroups, trackData }) => ( - (trackGroups && trackData - ? (trackGroups - .flatMap(group => group.tracksByRef ?? []) - .map(ref => find.track(ref, {wikiData: {trackData}})) - .filter(Boolean)) - : []) - ) - } - }, - }; -} diff --git a/src/thing/art-tag.js b/src/thing/art-tag.js deleted file mode 100644 index 4b09d885..00000000 --- a/src/thing/art-tag.js +++ /dev/null @@ -1,37 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isDirectory, - isName, -} from './validators.js'; - -export default class ArtTag extends Thing { - static [Thing.referenceType] = 'tag'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - isContentWarning: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - }; -} diff --git a/src/thing/artist.js b/src/thing/artist.js deleted file mode 100644 index bbb2a935..00000000 --- a/src/thing/artist.js +++ /dev/null @@ -1,48 +0,0 @@ -import Thing from './thing.js'; - -import { - isDirectory, - isName, - isString, - isURL, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export default class Artist extends Thing { - static [Thing.referenceType] = 'artist'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Artist', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - - aliasRefs: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('artist')} - }, - - contextNotes: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - }; -} diff --git a/src/thing/cacheable-object.js b/src/thing/cacheable-object.js deleted file mode 100644 index 9af41603..00000000 --- a/src/thing/cacheable-object.js +++ /dev/null @@ -1,305 +0,0 @@ -// Generally extendable class for caching properties and handling dependencies, -// with a few key properties: -// -// 1) The behavior of every property is defined by its descriptor, which is a -// static value stored on the subclass (all instances share the same property -// descriptors). -// -// 1a) Additional properties may not be added past the time of object -// construction, and attempts to do so (including externally setting a -// property name which has no corresponding descriptor) will throw a -// TypeError. (This is done via an Object.seal(this) call after a newly -// created instance defines its own properties according to the descriptor -// on its constructor class.) -// -// 2) Properties may have two flags set: update and expose. Properties which -// update are provided values from the external. Properties which expose -// provide values to the external, generally dependent on other update -// properties (within the same object). -// -// 2a) Properties may be flagged as both updating and exposing. This is so -// that the same name may be used for both "output" and "input". -// -// 3) Exposed properties have values which are computations dependent on other -// properties, as described by a `compute` function on the descriptor. -// Depended-upon properties are explicitly listed on the descriptor next to -// this function, and are only provided as arguments to the function once -// listed. -// -// 3a) An exposed property may depend only upon updating properties, not other -// exposed properties (within the same object). This is to force the -// general complexity of a single object to be fairly simple: inputs -// directly determine outputs, with the only in-between step being the -// `compute` function, no multiple-layer dependencies. Note that this is -// only true within a given object - externally, values provided to one -// object's `update` may be (and regularly are) the exposed values of -// another object. -// -// 3b) If a property both updates and exposes, it is automatically regarded as -// a dependancy. (That is, its exposed value will depend on the value it is -// updated with.) Rather than a required `compute` function, these have an -// optional `transform` function, which takes the update value as its first -// argument and then the usual key-value dependencies as its second. If no -// `transform` function is provided, the expose value is the same as the -// update value. -// -// 4) Exposed properties are cached; that is, if no depended-upon properties are -// updated, the value of an exposed property is not recomputed. -// -// 4a) The cache for an exposed property is invalidated as soon as any of its -// dependencies are updated, but the cache itself is lazy: the exposed -// value will not be recomputed until it is again accessed. (Likewise, an -// exposed value won't be computed for the first time until it is first -// accessed.) -// -// 5) Updating a property may optionally apply validation checks before passing, -// declared by a `validate` function on the `update` block. This function -// should either throw an error (e.g. TypeError) or return false if the value -// is invalid. -// -// 6) Objects do not expect all updating properties to be provided at once. -// Incomplete objects are deliberately supported and enabled. -// -// 6a) The default value for every updating property is null; undefined is not -// accepted as a property value under any circumstances (it always errors). -// However, this default may be overridden by specifying a `default` value -// on a property's `update` block. (This value will be checked against -// the property's validate function.) Note that a property may always be -// updated to null, even if the default is non-null. (Null always bypasses -// the validate check.) -// -// 6b) It's required by the external consumer of an object to determine whether -// or not the object is ready for use (within the larger program). This is -// convenienced by the static CacheableObject.listAccessibleProperties() -// function, which provides a mapping of exposed property names to whether -// or not their dependencies are yet met. - -import { color, ENABLE_COLOR } from '../util/cli.js'; - -import { inspect as nodeInspect } from 'util'; - -function inspect(value) { - return nodeInspect(value, {colors: ENABLE_COLOR}); -} - -export default class CacheableObject { - static instance = Symbol('CacheableObject `this` instance'); - - #propertyUpdateValues = Object.create(null); - #propertyUpdateCacheInvalidators = Object.create(null); - - /* - // Note the constructor doesn't take an initial data source. Due to a quirk - // of JavaScript, private members can't be accessed before the superclass's - // constructor is finished processing - so if we call the overridden - // update() function from inside this constructor, it will error when - // writing to private members. Pretty bad! - // - // That means initial data must be provided by following up with update() - // after constructing the new instance of the Thing (sub)class. - */ - - constructor() { - this.#defineProperties(); - this.#initializeUpdatingPropertyValues(); - - if (CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) { - return new Proxy(this, { - get: (obj, key) => { - if (!Object.hasOwn(obj, key)) { - if (key !== 'constructor') { - CacheableObject._invalidAccesses.add(`(${obj.constructor.name}).${key}`); - } - } - return obj[key]; - } - }); - } - } - - #initializeUpdatingPropertyValues() { - for (const [ property, descriptor ] of Object.entries(this.constructor.propertyDescriptors)) { - const { flags, update } = descriptor; - - if (!flags.update) { - continue; - } - - if (update?.default) { - this[property] = update?.default; - } else { - this[property] = null; - } - } - } - - #defineProperties() { - for (const [ property, descriptor ] of Object.entries(this.constructor.propertyDescriptors)) { - const { flags } = descriptor; - - const definition = { - configurable: false, - enumerable: true - }; - - if (flags.update) { - definition.set = this.#getUpdateObjectDefinitionSetterFunction(property); - } - - if (flags.expose) { - definition.get = this.#getExposeObjectDefinitionGetterFunction(property); - } - - Object.defineProperty(this, property, definition); - } - - Object.seal(this); - } - - #getUpdateObjectDefinitionSetterFunction(property) { - const { update } = this.#getPropertyDescriptor(property); - const validate = update?.validate; - const allowNull = update?.allowNull; - - return (newValue) => { - const oldValue = this.#propertyUpdateValues[property]; - - if (newValue === undefined) { - throw new ValueError(`Properties cannot be set to undefined`); - } - - if (newValue === oldValue) { - return; - } - - if (newValue !== null && validate) { - try { - const result = validate(newValue); - if (result === undefined) { - throw new TypeError(`Validate function returned undefined`); - } else if (result !== true) { - throw new TypeError(`Validation failed for value ${newValue}`); - } - } catch (error) { - error.message = `Property ${color.green(property)} (${inspect(this[property])} -> ${inspect(newValue)}): ${error.message}`; - throw error; - } - } - - this.#propertyUpdateValues[property] = newValue; - this.#invalidateCachesDependentUpon(property); - }; - } - - #getUpdatePropertyValidateFunction(property) { - const descriptor = this.#getPropertyDescriptor(property); - } - - #getPropertyDescriptor(property) { - return this.constructor.propertyDescriptors[property]; - } - - #invalidateCachesDependentUpon(property) { - for (const invalidate of this.#propertyUpdateCacheInvalidators[property] || []) { - invalidate(); - } - } - - #getExposeObjectDefinitionGetterFunction(property) { - const { flags } = this.#getPropertyDescriptor(property); - const compute = this.#getExposeComputeFunction(property); - - if (compute) { - let cachedValue; - const checkCacheValid = this.#getExposeCheckCacheValidFunction(property); - return () => { - if (checkCacheValid()) { - return cachedValue; - } else { - return (cachedValue = compute()); - } - }; - } else if (!flags.update && !compute) { - throw new Error(`Exposed property ${property} does not update and is missing compute function`); - } else { - return () => this.#propertyUpdateValues[property]; - } - } - - #getExposeComputeFunction(property) { - const { flags, expose } = this.#getPropertyDescriptor(property); - - const compute = expose?.compute; - const transform = expose?.transform; - - if (flags.update && !transform) { - return null; - } else if (flags.update && compute) { - throw new Error(`Updating property ${property} has compute function, should be formatted as transform`); - } else if (!flags.update && !compute) { - throw new Error(`Exposed property ${property} does not update and is missing compute function`); - } - - const dependencyKeys = expose.dependencies || []; - const dependencyGetters = dependencyKeys.map(key => () => [key, this.#propertyUpdateValues[key]]); - const getAllDependencies = () => Object.fromEntries(dependencyGetters.map(f => f()) - .concat([[this.constructor.instance, this]])); - - if (flags.update) { - return () => transform(this.#propertyUpdateValues[property], getAllDependencies()); - } else { - return () => compute(getAllDependencies()); - } - } - - #getExposeCheckCacheValidFunction(property) { - const { flags, expose } = this.#getPropertyDescriptor(property); - - let valid = false; - - const invalidate = () => { - valid = false; - }; - - const dependencyKeys = new Set(expose?.dependencies); - - if (flags.update) { - dependencyKeys.add(property); - } - - for (const key of dependencyKeys) { - if (this.#propertyUpdateCacheInvalidators[key]) { - this.#propertyUpdateCacheInvalidators[key].push(invalidate); - } else { - this.#propertyUpdateCacheInvalidators[key] = [invalidate]; - } - } - - return () => { - if (!valid) { - valid = true; - return false; - } else { - return true; - } - }; - } - - static DEBUG_SLOW_TRACK_INVALID_PROPERTIES = false; - static _invalidAccesses = new Set(); - - static showInvalidAccesses() { - if (!this.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) { - return; - } - - if (!this._invalidAccesses.size) { - return; - } - - console.log(`${this._invalidAccesses.size} unique invalid accesses:`); - for (const line of this._invalidAccesses) { - console.log(` - ${line}`); - } - } -} diff --git a/src/thing/flash.js b/src/thing/flash.js deleted file mode 100644 index 4eac65ad..00000000 --- a/src/thing/flash.js +++ /dev/null @@ -1,129 +0,0 @@ -import Thing from './thing.js'; - -import { - isColor, - isContributionList, - isDate, - isDirectory, - isFileExtension, - isName, - isNumber, - isString, - isURL, - oneOf, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export default class Flash extends Thing { - static [Thing.referenceType] = 'flash'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Flash', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - - // Flashes expose directory differently from other Things! Their - // default directory is dependent on the page number (or ID), not - // the name. - expose: { - dependencies: ['page'], - transform(directory, { page }) { - if (directory === null && page === null) - return null; - else if (directory === null) - return page; - else - return directory; - } - } - }, - - page: { - flags: {update: true, expose: true}, - update: {validate: oneOf(isString, isNumber)}, - - expose: { - transform: value => value.toString() - } - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - featuredTracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - contributorContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - }; -} - -export class FlashAct extends Thing { - static [Thing.referenceType] = 'flash-act'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Flash Act', - validate: isName - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - anchor: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - jump: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - jumpColor: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - flashesByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('flash')} - }, - }; -} diff --git a/src/thing/group.js b/src/thing/group.js deleted file mode 100644 index 3b92e957..00000000 --- a/src/thing/group.js +++ /dev/null @@ -1,73 +0,0 @@ -import CacheableObject from './cacheable-object.js'; -import Thing from './thing.js'; - -import { - isColor, - isDirectory, - isName, - isString, - isURL, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export class GroupCategory extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Group Category', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - groupsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('group')} - }, - }; -} - -export default class Group extends Thing { - static [Thing.referenceType] = 'group'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Group', validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - description: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - - // Expose only - - descriptionShort: { - flags: {expose: true}, - - expose: { - dependencies: ['description'], - compute: ({ description }) => description.split('<hr class="split">')[0] - } - } - }; -} diff --git a/src/thing/homepage-layout.js b/src/thing/homepage-layout.js deleted file mode 100644 index 47173917..00000000 --- a/src/thing/homepage-layout.js +++ /dev/null @@ -1,99 +0,0 @@ -import CacheableObject from './cacheable-object.js'; - -import { - isColor, - isCountingNumber, - isName, - isString, - oneOf, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, -} from './validators.js'; - -export class HomepageLayoutRow extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - type: { - flags: {update: true, expose: true}, - - update: { - validate(value) { - throw new Error(`'type' property validator must be overridden`); - } - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - }; -} - -export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { - static propertyDescriptors = { - ...HomepageLayoutRow.propertyDescriptors, - - // Update & expose - - type: { - flags: {update: true, expose: true}, - update: { - validate(value) { - if (value !== 'albums') { - throw new TypeError(`Expected 'albums'`); - } - - return true; - } - } - }, - - sourceGroupByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReference('group')} - }, - - sourceAlbumsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('album')} - }, - - countAlbumsFromGroup: { - flags: {update: true, expose: true}, - update: {validate: isCountingNumber} - }, - - actionLinks: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isString)} - }, - } -} - -export default class HomepageLayout extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - sidebarContent: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - rows: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)) - } - }, - }; -} diff --git a/src/thing/news-entry.js b/src/thing/news-entry.js deleted file mode 100644 index 2db2f37c..00000000 --- a/src/thing/news-entry.js +++ /dev/null @@ -1,49 +0,0 @@ -import Thing from './thing.js'; - -import { - isDate, - isDirectory, - isName, -} from './validators.js'; - -export default class NewsEntry extends Thing { - static [Thing.referenceType] = 'news-entry'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - content: { - flags: {update: true, expose: true}, - }, - - // Expose only - - contentShort: { - flags: {expose: true}, - - expose: { - dependencies: ['content'], - - compute({ content }) { - return body.split('<hr class="split">')[0]; - } - } - }, - }; -} diff --git a/src/thing/static-page.js b/src/thing/static-page.js deleted file mode 100644 index e2b51507..00000000 --- a/src/thing/static-page.js +++ /dev/null @@ -1,52 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isDirectory, - isName, - isString, -} from './validators.js'; - -export default class StaticPage extends Thing { - static [Thing.referenceType] = 'static'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName, default: 'Unnamed Static Page'} - }, - - nameShort: { - flags: {update: true, expose: true}, - update: {validate: isName}, - - expose: { - dependencies: ['name'], - transform: (value, { name }) => value ?? name - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - content: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - stylesheet: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - showInNavigationBar: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: true} - }, - }; -} diff --git a/src/thing/structures.js b/src/thing/structures.js deleted file mode 100644 index 364ba149..00000000 --- a/src/thing/structures.js +++ /dev/null @@ -1 +0,0 @@ -// Generic structure utilities common across various Thing types. diff --git a/src/thing/thing.js b/src/thing/thing.js deleted file mode 100644 index 2d6def62..00000000 --- a/src/thing/thing.js +++ /dev/null @@ -1,62 +0,0 @@ -// Base class for Things. No, we will not come up with a better name. -// Sorry not sorry! :) - -import CacheableObject from './cacheable-object.js'; - -import { - validateArrayItems, -} from './validators.js'; - -import { getKebabCase } from '../util/wiki-data.js'; -import find from '../util/find.js'; - -export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); - - static directoryExpose = { - dependencies: ['name'], - transform(directory, { name }) { - if (directory === null && name === null) - return null; - else if (directory === null) - return getKebabCase(name); - else - return directory; - } - }; - - static genContribsExpose(contribsByRefProperty) { - return { - dependencies: ['artistData', contribsByRefProperty], - compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) => ( - (contribsByRef && artistData - ? (contribsByRef - .map(({ who: ref, what }) => ({ - who: find.artist(ref, {wikiData: {artistData}}), - what - })) - .filter(({ who }) => who)) - : []) - ) - }; - } - - static genWikiDataProperty(thingClass) { - return { - flags: {update: true}, - update: { - validate: validateArrayItems(x => x instanceof thingClass) - } - }; - } - - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); - - if (!thing.directory) - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } -} diff --git a/src/thing/track.js b/src/thing/track.js deleted file mode 100644 index 3edabc92..00000000 --- a/src/thing/track.js +++ /dev/null @@ -1,173 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDirectory, - isDuration, - isName, - isURL, - isString, - validateArrayItems, - validateReference, - validateReferenceList, -} from './validators.js'; - -import Album from './album.js'; -import Artist from './artist.js'; -import ArtTag from './art-tag.js'; - -import find from '../util/find.js'; - -export default class Track extends Thing { - static [Thing.referenceType] = 'track'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Track', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - duration: { - flags: {update: true, expose: true}, - update: {validate: isDuration} - }, - - urls: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(isURL) - } - }, - - dateFirstReleased: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - hasCoverArt: { - flags: {update: true, expose: true}, - update: {default: true, validate: isBoolean} - }, - - hasURLs: { - flags: {update: true, expose: true}, - update: {default: true, validate: isBoolean} - }, - - referencedTracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - artistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - contributorContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - coverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - artTagsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('tag')} - }, - - // Previously known as: (track).aka - originalReleaseTrackByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReference('track')} - }, - - commentary: { - flags: {update: true, expose: true}, - update: {validate: isCommentary} - }, - - lyrics: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - // Update only - - albumData: Thing.genWikiDataProperty(Album), - artistData: Thing.genWikiDataProperty(Artist), - artTagData: Thing.genWikiDataProperty(ArtTag), - - // Expose only - - album: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData'], - compute: ({ [this.instance]: track, albumData }) => ( - albumData?.find(album => album.tracks.includes(track)) ?? null) - } - }, - - date: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData', 'dateFirstReleased'], - compute: ({ albumData, dateFirstReleased, [this.instance]: track }) => ( - dateFirstReleased ?? - albumData?.find(album => album.tracks.includes(track))?.date ?? - null - ) - } - }, - - // Previously known as: (track).artists - artistContribs: { - flags: {expose: true}, - expose: Thing.genContribsExpose('artistContribsByRef') - }, - - artTags: { - flags: {expose: true}, - - expose: { - dependencies: ['artTagsByRef', 'artTagData'], - - compute: ({ artTagsByRef, artTagData }) => ( - (artTagsByRef && artTagData - ? (artTagsByRef - .map(ref => find.tag(ref, {wikiData: {tagData: artTagData}})) - .filter(Boolean)) - : []) - ) - } - } - }; -} diff --git a/src/thing/validators.js b/src/thing/validators.js deleted file mode 100644 index 83922229..00000000 --- a/src/thing/validators.js +++ /dev/null @@ -1,327 +0,0 @@ -import { withAggregate } from '../util/sugar.js'; - -import { color, ENABLE_COLOR } from '../util/cli.js'; - -import { inspect as nodeInspect } from 'util'; - -function inspect(value) { - return nodeInspect(value, {colors: ENABLE_COLOR}); -} - -// Basic types (primitives) - -function a(noun) { - return (/[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`); -} - -function isType(value, type) { - if (typeof value !== type) - throw new TypeError(`Expected ${a(type)}, got ${typeof value}`); - - return true; -} - -export function isBoolean(value) { - return isType(value, 'boolean'); -} - -export function isNumber(value) { - return isType(value, 'number'); -} - -export function isPositive(number) { - isNumber(number); - - if (number <= 0) - throw new TypeError(`Expected positive number`); - - return true; -} - -export function isNegative(number) { - isNumber(number); - - if (number >= 0) - throw new TypeError(`Expected negative number`); - - return true; -} - -export function isPositiveOrZero(number) { - isNumber(number); - - if (number < 0) - throw new TypeError(`Expected positive number or zero`); - - return true; -} - -export function isNegativeOrZero(number) { - isNumber(number); - - if (number > 0) - throw new TypeError(`Expected negative number or zero`); - - return true; -} - -export function isInteger(number) { - isNumber(number); - - if (number % 1 !== 0) - throw new TypeError(`Expected integer`); - - return true; -} - -export function isCountingNumber(number) { - isInteger(number); - isPositive(number); - - return true; -} - -export function isString(value) { - return isType(value, 'string'); -} - -export function isStringNonEmpty(value) { - isString(value); - - if (value.trim().length === 0) - throw new TypeError(`Expected non-empty string`); - - return true; -} - -// Complex types (non-primitives) - -export function isInstance(value, constructor) { - isObject(value); - - if (!(value instanceof constructor)) - throw new TypeError(`Expected ${constructor.name}, got ${value.constructor.name}`); - - return true; -} - -export function isDate(value) { - return isInstance(value, Date); -} - -export function isObject(value) { - isType(value, 'object'); - - // Note: Please remember that null is always a valid value for properties - // held by a CacheableObject. This assertion is exclusively for use in other - // contexts. - if (value === null) - throw new TypeError(`Expected an object, got null`); - - return true; -} - -export function isArray(value) { - isObject(value); - - if (!Array.isArray(value)) - throw new TypeError(`Expected an array, got ${value}`); - - return true; -} - -function validateArrayItemsHelper(itemValidator) { - return (item, index) => { - try { - const value = itemValidator(item); - - if (value !== true) { - throw new Error(`Expected validator to return true`); - } - } catch (error) { - error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`; - throw error; - } - }; -} - -export function validateArrayItems(itemValidator) { - const fn = validateArrayItemsHelper(itemValidator); - - return array => { - isArray(array); - - withAggregate({message: 'Errors validating array items'}, ({ wrap }) => { - array.forEach(wrap(fn)); - }); - - return true; - }; -} - -export function validateInstanceOf(constructor) { - return object => isInstance(object, constructor); -} - -// Wiki data (primitives & non-primitives) - -export function isColor(color) { - isStringNonEmpty(color); - - if (color.startsWith('#')) { - if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length)) - throw new TypeError(`Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`); - - if (/[^0-9a-fA-F]/.test(color.slice(1))) - throw new TypeError(`Expected hexadecimal digits`); - - return true; - } - - throw new TypeError(`Unknown color format`); -} - -export function isCommentary(commentary) { - return isString(commentary); -} - -const isArtistRef = validateReference('artist'); - -export function isContribution(contrib) { - // TODO: Use better object validation for this (supporting aggregates etc) - - isObject(contrib); - - isArtistRef(contrib.who); - - if (contrib.what !== null) { - isStringNonEmpty(contrib.what); - } - - return true; -} - -export const isContributionList = validateArrayItems(isContribution); - -export function isDimensions(dimensions) { - isArray(dimensions); - - if (dimensions.length !== 2) - throw new TypeError(`Expected 2 item array`); - - isPositive(dimensions[0]); - isInteger(dimensions[0]); - isPositive(dimensions[1]); - isInteger(dimensions[1]); - - return true; -} - -export function isDirectory(directory) { - isStringNonEmpty(directory); - - if (directory.match(/[^a-zA-Z0-9_\-]/)) - throw new TypeError(`Expected only letters, numbers, dash, and underscore, got "${directory}"`); - - return true; -} - -export function isDuration(duration) { - isNumber(duration); - isPositiveOrZero(duration); - - return true; -} - -export function isFileExtension(string) { - isStringNonEmpty(string); - - if (string[0] === '.') - throw new TypeError(`Expected no dot (.) at the start of file extension`); - - if (string.match(/[^a-zA-Z0-9_]/)) - throw new TypeError(`Expected only alphanumeric and underscore`); - - return true; -} - -export function isLanguageCode(string) { - // TODO: This is a stub function because really we don't need a detailed - // is-language-code parser right now. - - isString(string); - - return true; -} - -export function isName(name) { - return isString(name); -} - -export function isURL(string) { - isStringNonEmpty(string); - - new URL(string); - - return true; -} - -export function validateReference(type = 'track') { - return ref => { - isStringNonEmpty(ref); - - const match = ref.trim().match(/^(?:(?<typePart>\S+):(?=\S))?(?<directoryPart>.+)(?<!:)$/); - - if (!match) - throw new TypeError(`Malformed reference`); - - const { groups: { typePart, directoryPart } } = match; - - if (typePart && typePart !== type) - throw new TypeError(`Expected ref to begin with "${type}:", got "${typePart}:"`); - - if (typePart) - isDirectory(directoryPart); - - isName(ref); - - return true; - }; -} - -export function validateReferenceList(type = '') { - return validateArrayItems(validateReference(type)); -} - -// Compositional utilities - -export function oneOf(...checks) { - return value => { - const errorMeta = []; - - for (let i = 0, check; check = checks[i]; i++) { - try { - const result = check(value); - - if (result !== true) { - throw new Error(`Check returned false`); - } - - return true; - } catch (error) { - errorMeta.push([check, i, error]); - } - } - - // Don't process error messages until every check has failed. - const errors = []; - for (const [ check, i, error ] of errorMeta) { - error.message = (check.name - ? `(#${i} "${check.name}") ${error.message}` - : `(#${i}) ${error.message}`); - error.check = check; - errors.push(error); - } - throw new AggregateError(errors, `Expected one of ${checks.length} possible checks, but none were true`); - }; -} diff --git a/src/thing/wiki-info.js b/src/thing/wiki-info.js deleted file mode 100644 index b805bf76..00000000 --- a/src/thing/wiki-info.js +++ /dev/null @@ -1,90 +0,0 @@ -import CacheableObject from './cacheable-object.js'; - -import { - isBoolean, - isColor, - isLanguageCode, - isName, - isString, - isURL, -} from './validators.js'; - -export default class WikiInfo extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName, default: 'Unnamed Wiki'} - }, - - // Displayed in nav bar. - shortName: { - flags: {update: true, expose: true}, - update: {validate: isName}, - - expose: { - dependencies: ['name'], - transform: (value, { name }) => value ?? name - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - // One-line description used for <meta rel="description"> tag. - description: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - footerContent: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - defaultLanguage: { - flags: {update: true, expose: true}, - update: {validate: isLanguageCode} - }, - - canonicalBase: { - flags: {update: true, expose: true}, - update: {validate: isURL} - }, - - // Feature toggles - - enableArtistAvatars: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableFlashesAndGames: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableListings: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableNews: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableArtTagUI: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableGroupUI: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - }; -} |