From 55e4afead38bc541cba4ae1cef183527c254f99a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 17:28:15 -0300 Subject: data: track: experimental Thing.compose.from() processing style --- src/data/things/thing.js | 142 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..143c1515 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; +import {empty, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -418,4 +418,144 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } + + static findArtistsFromContribs(contribsByRef, artistData) { + return ( + contribsByRef + .map(({who, what}) => ({ + who: find.artist(who, artistData), + what, + })) + .filter(({who}) => who)); + } + + static composite = { + from(composition) { + const base = composition.at(-1); + const steps = composition.slice(0, -1); + + const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + + if (base.flags.compose) { + aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + } + + const exposeFunctionOrder = []; + const exposeDependencies = new Set(base.expose?.dependencies); + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const message = + (step.annotation + ? `Errors in step #${i + 1} (${step.annotation})` + : `Errors in step #${i + 1}`); + + aggregate.nest({message}, ({push}) => { + if (!step.flags.compose) { + push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); + } + + if (step.flags.update) { + push(new Error(`Steps which update aren't supported yet`)); + } + + if (step.flags.expose) expose: { + if (!step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); + break expose; + } + + if (step.expose.dependencies) { + for (const dependency of step.expose.dependencies) { + exposeDependencies.add(dependency); + } + } + + if (base.flags.update) { + if (step.expose.transform) { + exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + } else { + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } else { + if (step.expose.transform && !step.expose.compute) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + + exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + } + } + }); + } + + aggregate.close(); + + const constructedDescriptor = {}; + + constructedDescriptor.flags = { + update: !!base.flags.update, + expose: !!base.flags.expose, + compose: false, + }; + + if (base.flags.update) { + constructedDescriptor.update = base.flags.update; + } + + if (base.flags.expose) { + const expose = constructedDescriptor.expose = {}; + expose.dependencies = Array.from(exposeDependencies); + + const continuationSymbol = Symbol(); + + if (base.flags.update) { + expose.transform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + let valueSoFar = value; + + for (const {type, fn} of exposeFunctionOrder) { + const result = + (type === 'transform' + ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + valueSoFar = updatedValue; + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }) + : fn(dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + })); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.transform(valueSoFar, dependencies); + }; + } else { + expose.compute = (initialDependencies) => { + const dependencies = {...initialDependencies}; + + for (const {fn} of exposeFunctionOrder) { + const result = + fn(valueSoFar, dependencies, providedDependencies => { + Object.assign(dependencies, providedDependencies ?? {}); + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + } + + return base.expose.compute(dependencies); + }; + } + } + + return constructedDescriptor; + }, + }; } -- cgit 1.3.0-6-gf8a5 From 128c47001a639d1569bdfadf783ccede22116350 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:17:02 -0300 Subject: data: fix compute() bugs in Thing.composite.from() --- src/data/things/thing.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 143c1515..111de550 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -420,6 +420,8 @@ export default class Thing extends CacheableObject { } static findArtistsFromContribs(contribsByRef, artistData) { + if (empty(contribsByRef)) return null; + return ( contribsByRef .map(({who, what}) => ({ @@ -518,7 +520,7 @@ export default class Thing extends CacheableObject { const result = (type === 'transform' ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { - valueSoFar = updatedValue; + valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) @@ -532,7 +534,11 @@ export default class Thing extends CacheableObject { } } - return base.expose.transform(valueSoFar, dependencies); + if (base.expose.transform) { + return base.expose.transform(valueSoFar, dependencies); + } else { + return base.expose.compute(dependencies); + } }; } else { expose.compute = (initialDependencies) => { @@ -540,7 +546,7 @@ export default class Thing extends CacheableObject { for (const {fn} of exposeFunctionOrder) { const result = - fn(valueSoFar, dependencies, providedDependencies => { + fn(dependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }); -- cgit 1.3.0-6-gf8a5 From 0fd10f2997db8ddec95e3caff94343eafdd9dda1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 21 Aug 2023 21:18:23 -0300 Subject: data: track: more composite shenanigans --- src/data/things/thing.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 111de550..5d14b296 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -250,14 +250,7 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefProperty], compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - contribsByRef && artistData - ? contribsByRef - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who) - : [], + Thing.findArtistsFromContribs(contribsByRef, artistData), }, }), @@ -563,5 +556,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + + withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + flags: {expose: true, compose: true}, + + expose: { + dependencies: ['artistData', contribsByRefProperty], + compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + callback({ + [dependencyName]: + Thing.findArtistsFromContribs(contribsByRef, artistData), + }), + }, + }), }; } -- cgit 1.3.0-6-gf8a5 From 75691866ed68b9261dd920b79d4ab214df3f049b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 13:02:19 -0300 Subject: data: filter only requested deps, require requesting 'this' * Thing.composite.from() only provides the dependencies specified in each step and the base, and prevents '#'-prefixed keys from being specified on the main (composite) dependency list. * CacheableObject no longer provides a "reflection" dependency to every compute/transform function, and now requires the property 'this' to be specified instead of the constructor.instance symbol. (The static CacheableObject.instance, inherited by all subclasses, was also removed.) * Also minor improvements to sugar.js data processing utility functions. --- src/data/things/thing.js | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d14b296..bc10e06b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, openAggregate} from '#sugar'; +import {empty, filterProperties, openAggregate} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { @@ -278,6 +278,7 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { dependencies: [ + 'this', contribsByRefProperty, thingDataProperty, nullerProperty, @@ -285,7 +286,7 @@ export default class Thing extends CacheableObject { ].filter(Boolean), compute({ - [Thing.instance]: thing, + this: thing, [nullerProperty]: nuller, [contribsByRefProperty]: contribsByRef, [thingDataProperty]: thingData, @@ -330,9 +331,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], }, }), @@ -344,9 +345,9 @@ export default class Thing extends CacheableObject { flags: {expose: true}, expose: { - dependencies: [thingDataProperty], + dependencies: ['this', thingDataProperty], - compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) => + compute: ({this: thing, [thingDataProperty]: thingData}) => thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], }, }), @@ -462,15 +463,19 @@ export default class Thing extends CacheableObject { if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; exposeDependencies.add(dependency); } } + let fn, type; if (base.flags.update) { if (step.expose.transform) { - exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform}); + type = 'transform'; + fn = step.expose.transform; } else { - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } } else { if (step.expose.transform && !step.expose.compute) { @@ -478,8 +483,15 @@ export default class Thing extends CacheableObject { break expose; } - exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute}); + type = 'compute'; + fn = step.expose.compute; } + + exposeFunctionOrder.push({ + type, + fn, + ownDependencies: step.expose.dependencies, + }); } }); } @@ -509,15 +521,20 @@ export default class Thing extends CacheableObject { const dependencies = {...initialDependencies}; let valueSoFar = value; - for (const {type, fn} of exposeFunctionOrder) { + for (const {type, fn, ownDependencies} of exposeFunctionOrder) { + const filteredDependencies = + (ownDependencies + ? filterProperties(dependencies, ownDependencies) + : {}) + const result = (type === 'transform' - ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => { + ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; }) - : fn(dependencies, providedDependencies => { + : fn(filteredDependencies, providedDependencies => { Object.assign(dependencies, providedDependencies ?? {}); return continuationSymbol; })); @@ -527,10 +544,13 @@ export default class Thing extends CacheableObject { } } + const filteredDependencies = + filterProperties(dependencies, base.expose.dependencies); + if (base.expose.transform) { - return base.expose.transform(valueSoFar, dependencies); + return base.expose.transform(valueSoFar, filteredDependencies); } else { - return base.expose.compute(dependencies); + return base.expose.compute(filteredDependencies); } }; } else { -- cgit 1.3.0-6-gf8a5 From 1481db921e645ab09aad3a57b4ce308e2c57d738 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 13:52:43 -0300 Subject: data: signature changes to misc compositional functions --- src/data/things/thing.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index bc10e06b..f1ae6c71 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -577,14 +577,18 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, - withDynamicContribs: (contribsByRefProperty, dependencyName) => ({ + // Resolves the contribsByRef contained in the provided dependency, + // providing (named by the second argument) the result. "Resolving" + // means mapping the "who" reference of each contribution to an artist + // object, and filtering out those whose "who" doesn't match any artist. + withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}, callback) => + dependencies: ['artistData', contribsByRefDependency], + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => callback({ - [dependencyName]: + [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), }, -- cgit 1.3.0-6-gf8a5 From 6483809c6d9c67f1311a64f2572b4fe5881d3a0d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 22 Aug 2023 22:36:20 -0300 Subject: data: composition docs, annotations, nesting --- src/data/things/thing.js | 316 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 308 insertions(+), 8 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1ae6c71..1186c389 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -426,14 +426,222 @@ export default class Thing extends CacheableObject { } static composite = { - from(composition) { + // Composes multiple compositional "steps" and a "base" to form a property + // descriptor out of modular building blocks. This is an extension to the + // more general-purpose CacheableObject property descriptor syntax, and + // aims to make modular data processing - which lends to declarativity - + // much easier, without fundamentally altering much of the typical syntax + // or terminology, nor building on it to an excessive degree. + // + // Think of a composition as being a chain of steps which lead into a final + // base property, which is usually responsible for returning the value that + // will actually get exposed when the property being described is accessed. + // + // == The compositional base: == + // + // The final item in a compositional list is its base, and it identifies + // the essential qualities of the property descriptor. The compositional + // steps preceding it may exit early, in which case the expose function + // defined on the base won't be called; or they will provide dependencies + // that the base may use to compute the final value that gets exposed for + // this property. + // + // The base indicates the capabilities of the composition as a whole. + // It should be {expose: true}, since that's the only area that preceding + // compositional steps (currently) can actually influence. If it's also + // {update: true}, then the composition as a whole accepts an update value + // just like normal update-flag property descriptors - meaning it can be + // set with `thing.someProperty = value` and that value will be paseed + // into each (implementing) step's transform() function, as well as the + // base. Bases usually aren't {compose: true}, but can be - check out the + // section on "nesting compositions" for details about that. + // + // Every composition always has exactly one compositional base, and it's + // always the last item in the composition list. All items preceding it + // are compositional steps, described below. + // + // == Compositional steps: == + // + // Compositional steps are, in essence, typical property descriptors with + // the extra flag {compose: true}. They operate on existing dependencies, + // and are typically dynamically constructed by "utility" functions (but + // can also be manually declared within the step list of a composition). + // Compositional steps serve two purposes: + // + // 1. exit early, if some condition is matched, returning and exposing + // some value directly from that step instead of continuing further + // down the step list; + // + // 2. and/or provide new, dynamically created "private" dependencies which + // can be accessed by further steps down the list, or at the base at + // the bottom, modularly supplying information that will contribute to + // the final value exposed for this property. + // + // Usually it's just one of those two, but it's fine for a step to perform + // both jobs if the situation benefits. + // + // Compositional steps are the real "modular" or "compositional" part of + // this data processing style - they're designed to be combined together + // in dynamic, versatile ways, as each property demands it. You usually + // define a compositional step to be returned by some ordinary static + // property-descriptor-returning function (customarily namespaced under + // the relevant Thing class's static `composite` field) - that lets you + // reuse it in multiple compositions later on. + // + // Compositional steps are implemented with "continuation passing style", + // meaning the connection to the next link on the chain is passed right to + // each step's compute (or transform) function, and the implementation gets + // to decide whether to continue on that chain or exit early by returning + // some other value. + // + // Every step along the chain, apart from the base at the bottom, has to + // have the {compose: true} step. That means its compute() or transform() + // function will be passed an extra argument at the end, `continuation`. + // To provide new dependencies to items further down the chain, just pass + // them directly to this continuation() function, customarily with a hash + // ('#') prefixing each name - for example: + // + // compute({..some dependencies..}, continuation) { + // return continuation({ + // '#excitingProperty': (..a value made from dependencies..), + // }); + // } + // + // Performing an early exit is as simple as returning some other value, + // instead of the continuation. + // + // It may be fine to simply provide new dependencies under a hard-coded + // name, such as '#excitingProperty' above, but if you're writing a utility + // that dynamically returns the compositional step and you suspect you + // might want to use this step multiple times in a single composition, + // it's customary to accept a name for the result. + // + // Here's a detailed example showing off early exit, dynamically operating + // on a provided dependency name, and then providing a result in another + // also-provided dependency name: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: [contribsByRefDependency, 'artistData'], + // compute({ + // [contribsByRefDependency]: contribsByRef, + // artistData, + // }, continuation) { + // if (!artistData) return null; /* early exit! */ + // return continuation({ + // [outputDependency]: /* this is the important part */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // And how you might work that into a composition: + // + // static Track[Thing.getPropertyDescriptors].coverArtists = + // Thing.composite.from([ + // Track.composite.doSomethingWhichMightEarlyExit(), + // Thing.composite.withResolvedContribs({ + // from: 'coverArtistContribsByRef', + // to: '#coverArtistContribs', + // }), + // + // { + // flags: {expose: true}, + // expose: { + // dependencies: ['#coverArtistContribs'], + // compute({'#coverArtistContribs': coverArtistContribs}) { + // return coverArtistContribs.map(({who}) => who); + // }, + // }, + // }, + // ]); + // + // == To compute or to transform: == + // + // A compositional step can work directly on a property's stored update + // value, transforming it in place and either early exiting with it or + // passing it on (via continuation) to the next item(s) in the + // compositional step list. (If needed, these can provide dependencies + // the same way as compute functions too - just pass that object after + // the updated (or same) transform value in your call to continuation().) + // + // But in order to make them more versatile, compositional steps have an + // extra trick up their sleeve. If a compositional step implements compute + // and *not* transform, it can still be used in a composition targeting a + // property which updates! These retain their full dependency-providing and + // early exit functionality - they just won't be provided the update value. + // If a compute-implementing step returns its continuation, then whichever + // later step (or the base) next implements transform() will receive the + // update value that had so far been running - as well as any dependencies + // the compute() step returned, of course! + // + // Please note that a compositional step which transforms *should not* + // specify, in its flags, {update: true}. Just provide the transform() + // function in its expose descriptor; it will be automatically detected + // and used when appropriate. + // + // It's actually possible for a step to specify both transform and compute, + // in which case the transform() implementation will only be selected if + // the composition's base is {update: true}. It's not exactly known why you + // would want to specify unique-but-related transform and compute behavior, + // but the basic possibility was too cool to skip out on. + // + // == Nesting compositions: == + // + // Compositional steps are so convenient that you just might want to bundle + // them together, and form a whole new step-shaped unit of its own! + // + // In order to allow for this while helping to ensure internal dependencies + // remain neatly isolated from the composition which nests your bundle, + // the Thing.composite.from() function will accept and adapt to a base that + // specifies the {compose: true} flag, just like the steps preceding it. + // + // The continuation function that gets provided to the base will be mildly + // special - after all, nothing follows the base within the composition's + // own list! Instead of appending dependencies alongside any previously + // provided ones to be available to the next step, the base's continuation + // function should be used to define "exports" of the composition as a + // whole. It's similar to the usual behavior of the continuation, just + // expanded to the scope of the composition instead of following steps. + // + // For example, suppose your composition (which you expect to include in + // other compositions) brings about several internal, hash-prefixed + // dependencies to contribute to its own results. Those dependencies won't + // end up "bleeding" into the dependency list of whichever composition is + // nesting this one - they will totally disappear once all the steps in + // the nested composition have finished up. + // + // To "export" the results of processing all those dependencies (provided + // that's something you want to do and this composition isn't used purely + // for a conditional early-exit), you'll want to define them in the + // continuation passed to the base. (Customarily, those should start with + // a hash just like the exports from any other compositional step; they're + // still dynamically provided dependencies!) + // + from(firstArg, secondArg) { + let annotation, composition; + if (typeof firstArg === 'string') { + [annotation, composition] = [firstArg, secondArg]; + } else { + [annotation, composition] = [null, firstArg]; + } + const base = composition.at(-1); const steps = composition.slice(0, -1); - const aggregate = openAggregate({message: `Errors preparing Thing.composite.from() composition`}); + const aggregate = openAggregate({ + message: + `Errors preparing Thing.composite.from() composition` + + (annotation ? ` (${annotation})` : ''), + }); - if (base.flags.compose) { - aggregate.push(new TypeError(`Base (bottom item) must not be {compose: true}`)); + if (base.flags.compose && base.flags.compute) { + push(new TypeError(`Base which composes can't also update yet`)); } const exposeFunctionOrder = []; @@ -500,14 +708,18 @@ export default class Thing extends CacheableObject { const constructedDescriptor = {}; + if (annotation) { + constructedDescriptor.annotation = annotation; + } + constructedDescriptor.flags = { update: !!base.flags.update, expose: !!base.flags.expose, - compose: false, + compose: !!base.flags.compose, }; if (base.flags.update) { - constructedDescriptor.update = base.flags.update; + constructedDescriptor.update = base.update; } if (base.flags.expose) { @@ -547,6 +759,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = filterProperties(dependencies, base.expose.dependencies); + // Note: base.flags.compose is not compatible with base.flags.update, + // so the base.flags.compose case is not handled here. + if (base.expose.transform) { return base.expose.transform(valueSoFar, filteredDependencies); } else { @@ -554,7 +769,7 @@ export default class Thing extends CacheableObject { } }; } else { - expose.compute = (initialDependencies) => { + expose.compute = (initialDependencies, continuationIfApplicable) => { const dependencies = {...initialDependencies}; for (const {fn} of exposeFunctionOrder) { @@ -569,7 +784,23 @@ export default class Thing extends CacheableObject { } } - return base.expose.compute(dependencies); + if (base.flags.compose) { + let exportDependencies; + + const result = + base.expose.compute(dependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); + + if (result !== continuationSymbol) { + return result; + } + + return exportDependencies; + } else { + return base.expose.compute(dependencies); + } }; } } @@ -577,11 +808,48 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Provides dependencies exactly as they are (or null if not defined) to the + // continuation. Although this can *technically* be used to alias existing + // dependencies to some other name within the middle of a composition, it's + // intended to be used only as a composition's base - doing so makes the + // composition as a whole suitable as a step in some other composition, + // providing the listed (internal) dependencies to later steps just like + // other compositional steps. + export(mapping) { + const mappingEntries = Object.entries(mapping); + + return { + annotation: `Thing.composite.export`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: Object.values(mapping), + + compute(dependencies, continuation) { + const exports = {}; + + // Note: This is slightly different behavior from filterProperties, + // as defined in sugar.js, which doesn't fall back to null for + // properties which don't exist on the original object. + for (const [exportKey, dependencyKey] of mappingEntries) { + exports[exportKey] = + (Object.hasOwn(dependencies, dependencyKey) + ? dependencies[dependencyKey] + : null); + } + + return continuation(exports); + } + }, + }; + }, + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { @@ -593,5 +861,37 @@ export default class Thing extends CacheableObject { }), }, }), + + // Resolves a reference by using the provided find function to match it + // within the provided thingData dependency. This will early exit if the + // data dependency is null, or, if earlyExitIfNotFound is set to true, + // if the find function doesn't match anything for the reference. + // Otherwise, the data object (or null, if not found) is provided on + // the output dependency. + withResolvedReference({ + ref: refDependency, + data: dataDependency, + to: outputDependency, + find: findFunction, + earlyExitIfNotFound = false, + }) { + return { + annotation: `Thing.composite.withResolvedReference`, + flags: {expose: true, compose: true}, + + expose: { + dependencies: [refDependency, dataDependency], + + compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (data === null) return null; + + const match = findFunction(ref, data, {mode: 'quiet'}); + if (match === null && earlyExitIfNotFound) return null; + + return continuation({[outputDependency]: match}); + }, + }, + }; + } }; } -- cgit 1.3.0-6-gf8a5 From 13914b9f07f60d6d8aaaddc7df675d41950320c3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 12:22:34 -0300 Subject: test: Track.{color,date,hasUniqueCoverArt} (unit) --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1186c389..decde6f4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -419,7 +419,7 @@ export default class Thing extends CacheableObject { return ( contribsByRef .map(({who, what}) => ({ - who: find.artist(who, artistData), + who: find.artist(who, artistData, {mode: 'quiet'}), what, })) .filter(({who}) => who)); -- cgit 1.3.0-6-gf8a5 From 0f4e27426384536c179583a8ffaf3dd9f121766b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:00:35 -0300 Subject: data: Thing.composite.from: fix not calling export continuation --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index decde6f4..c1f969b2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -797,7 +797,7 @@ export default class Thing extends CacheableObject { return result; } - return exportDependencies; + return continuationIfApplicable(exportDependencies); } else { return base.expose.compute(dependencies); } -- cgit 1.3.0-6-gf8a5 From 8dd100d04fdd13b4ab8348d61378de5fd74f72d4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:01:05 -0300 Subject: data: Thing.composite.withResolvedReference: fix null refs The `earlyExitIfNotFound` flag is only supposed to exit if the reference really existed and failed to match anything. If it was null in the first place, withResolvedReferences should always just pass null ahead. --- src/data/things/thing.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c1f969b2..798a057a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -866,8 +866,9 @@ export default class Thing extends CacheableObject { // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if earlyExitIfNotFound is set to true, // if the find function doesn't match anything for the reference. - // Otherwise, the data object (or null, if not found) is provided on - // the output dependency. + // Otherwise, the data object is provided on the output dependency; + // or null, if the reference doesn't match anything or itself was null + // to begin with. withResolvedReference({ ref: refDependency, data: dataDependency, @@ -883,6 +884,8 @@ export default class Thing extends CacheableObject { dependencies: [refDependency, dataDependency], compute({[refDependency]: ref, [dataDependency]: data}, continuation) { + if (!ref) return continuation({[outputDependency]: null}); + if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); -- cgit 1.3.0-6-gf8a5 From fab4b46d13795bcc82c8b4dd6b5a39ef23c42430 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 23 Aug 2023 19:02:44 -0300 Subject: data: fix more bad function signatures --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 798a057a..eaf4655d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -854,8 +854,8 @@ export default class Thing extends CacheableObject { expose: { dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, callback) => - callback({ + compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + continuation({ [outputDependency]: Thing.findArtistsFromContribs(contribsByRef, artistData), }), -- cgit 1.3.0-6-gf8a5 From 2b2bbe9083d6f205e6b04b08c8bc4339a6a9ed87 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 18:47:09 -0300 Subject: data: Thing.composite.from: mapDependencies/mapContinuation --- src/data/things/thing.js | 162 ++++++++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 71 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index eaf4655d..a9fd220f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -644,7 +644,7 @@ export default class Thing extends CacheableObject { push(new TypeError(`Base which composes can't also update yet`)); } - const exposeFunctionOrder = []; + const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); for (let i = 0; i < steps.length; i++) { @@ -695,11 +695,7 @@ export default class Thing extends CacheableObject { fn = step.expose.compute; } - exposeFunctionOrder.push({ - type, - fn, - ownDependencies: step.expose.dependencies, - }); + exposeSteps.push(step.expose); } }); } @@ -727,81 +723,104 @@ export default class Thing extends CacheableObject { expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol(); + const noTransformSymbol = Symbol(); - if (base.flags.update) { - expose.transform = (value, initialDependencies) => { - const dependencies = {...initialDependencies}; - let valueSoFar = value; - - for (const {type, fn, ownDependencies} of exposeFunctionOrder) { - const filteredDependencies = - (ownDependencies - ? filterProperties(dependencies, ownDependencies) - : {}) - - const result = - (type === 'transform' - ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => { + const _filterDependencies = (dependencies, step) => { + const filteredDependencies = + (step.dependencies + ? filterProperties(dependencies, step.dependencies) + : {}); + + if (step.mapDependencies) { + for (const [to, from] of Object.entries(step.mapDependencies)) { + filteredDependencies[to] = dependencies[from] ?? null; + } + } + + return filteredDependencies; + }; + + const _assignDependencies = (continuationAssignment, step) => { + if (!step.mapContinuation) { + return continuationAssignment; + } + + const assignDependencies = {}; + + for (const [from, to] of Object.entries(step.mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } + + return assignDependencies; + }; + + const _computeOrTransform = (value, initialDependencies) => { + const dependencies = {...initialDependencies}; + + let valueSoFar = value; + + for (const step of exposeSteps) { + const filteredDependencies = _filterDependencies(dependencies, step); + + let assignDependencies = null; + + const result = + (valueSoFar !== noTransformSymbol && step.transform + ? step.transform( + valueSoFar, filteredDependencies, + (updatedValue, providedDependencies) => { valueSoFar = updatedValue ?? null; - Object.assign(dependencies, providedDependencies ?? {}); + assignDependencies = providedDependencies; return continuationSymbol; }) - : fn(filteredDependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); + : step.compute( + filteredDependencies, + (providedDependencies) => { + assignDependencies = providedDependencies; return continuationSymbol; })); - if (result !== continuationSymbol) { - return result; - } + if (result !== continuationSymbol) { + return result; } - const filteredDependencies = - filterProperties(dependencies, base.expose.dependencies); + Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + } - // Note: base.flags.compose is not compatible with base.flags.update, - // so the base.flags.compose case is not handled here. + const filteredDependencies = _filterDependencies(dependencies, base.expose); - if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); - } else { - return base.expose.compute(filteredDependencies); - } - }; - } else { - expose.compute = (initialDependencies, continuationIfApplicable) => { - const dependencies = {...initialDependencies}; - - for (const {fn} of exposeFunctionOrder) { - const result = - fn(dependencies, providedDependencies => { - Object.assign(dependencies, providedDependencies ?? {}); - return continuationSymbol; - }); - - if (result !== continuationSymbol) { - return result; - } - } + // Note: base.flags.compose is not compatible with base.flags.update. + if (base.expose.transform) { + return base.expose.transform(valueSoFar, filteredDependencies); + } else if (base.flags.compose) { + const continuation = continuationIfApplicable; - if (base.flags.compose) { - let exportDependencies; + let exportDependencies; - const result = - base.expose.compute(dependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = + base.expose.compute(filteredDependencies, providedDependencies => { + exportDependencies = providedDependencies; + return continuationSymbol; + }); - if (result !== continuationSymbol) { - return result; - } - - return continuationIfApplicable(exportDependencies); - } else { - return base.expose.compute(dependencies); + if (result !== continuationSymbol) { + return result; } - }; + + return continuation(_assignDependencies(exportDependencies, base.expose)); + } else { + return base.expose.compute(filteredDependencies); + } + }; + + if (base.flags.update) { + expose.transform = + (value, initialDependencies) => + _computeOrTransform(value, initialDependencies); + } else { + expose.compute = + (initialDependencies) => + _computeOrTransform(undefined, initialDependencies); } } @@ -848,16 +867,17 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs: ({from: contribsByRefDependency, to: outputDependency}) => ({ + withResolvedContribs: ({from, to}) => ({ annotation: `Thing.composite.withResolvedContribs`, flags: {expose: true, compose: true}, expose: { - dependencies: ['artistData', contribsByRefDependency], - compute: ({artistData, [contribsByRefDependency]: contribsByRef}, continuation) => + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => continuation({ - [outputDependency]: - Thing.findArtistsFromContribs(contribsByRef, artistData), + to: Thing.findArtistsFromContribs(from, artistData), }), }, }), -- cgit 1.3.0-6-gf8a5 From 57edc116016f45f1bc9e7e3e6560450b6c480602 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 18:49:11 -0300 Subject: data: fix not passing noTransformSymbol --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a9fd220f..d553a3ec 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -820,7 +820,7 @@ export default class Thing extends CacheableObject { } else { expose.compute = (initialDependencies) => - _computeOrTransform(undefined, initialDependencies); + _computeOrTransform(noTransformSymbol, initialDependencies); } } -- cgit 1.3.0-6-gf8a5 From eb869dd1b786a4180647e5b8b1b6f20aefb6c004 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 19:43:28 -0300 Subject: data: Track.compposite.from: 'options', cache-safe documentation --- src/data/things/thing.js | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d553a3ec..1bca6c38 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -561,6 +561,97 @@ export default class Thing extends CacheableObject { // }, // ]); // + // == Cache-safe dependency names: == + // + // [Disclosure: The caching engine hasn't actually been implemented yet. + // As such, this section is subject to change, and simply provides sound + // forward-facing advice and interfaces.] + // + // It's a good idea to write individual compositional steps in such a way + // that they're "cache-safe" - meaning the same input (dependency) values + // will always result in the same output (continuation or early exit). + // + // In order to facilitate this, compositional step descriptors may specify + // unique `mapDependencies`, `mapContinuation`, and `options` values. + // + // Consider the `withResolvedContribs` example adjusted to make use of + // two of these options below: + // + // static Thing.composite.withResolvedContribs = ({ + // from: contribsByRefDependency, + // to: outputDependency, + // }) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {contribsByRef: contribsByRefDependency}, + // mapContinuation: {outputDependency}, + // compute({ + // contribsByRef, /* no longer in square brackets */ + // artistData, + // }, continuation) { + // if (!artistData) return null; + // return continuation({ + // outputDependency: /* no longer in square brackets */ + // (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // With a little destructuring and restructuring JavaScript sugar, the + // above can be simplified some more: + // + // static Thing.composite.withResolvedContribs = ({from, to}) => ({ + // flags: {expose: true, compose: true}, + // expose: { + // dependencies: ['artistData'], + // mapDependencies: {from}, + // mapContinuation: {to}, + // compute({artistData, from: contribsByRef}, continuation) { + // if (!artistData) return null; + // return continuation({ + // to: (..resolve contributions one way or another..), + // }); + // }, + // }, + // }); + // + // These two properties let you separate the name-mapping behavior (for + // dependencies and the continuation) from the main body of the compute + // function. That means the compute function will *always* get inputs in + // the same form (dependencies 'artistData' and 'from' above), and will + // *always* provide its output in the same form (early return or 'to'). + // + // Thanks to that, this `compute` function is cache-safe! Its outputs can + // be cached corresponding to each set of mapped inputs. So it won't matter + // whether the `from` dependency is named `coverArtistContribsByRef` or + // `contributorContribsByRef` or something else - the compute function + // doesn't care, and only expects that value to be provided via its `from` + // argument. Likewise, it doesn't matter if the output should be sent to + // '#coverArtistContribs` or `#contributorContribs` or some other name; + // the mapping is handled automatically outside, and compute will always + // output its value to the continuation's `to`. + // + // Note that `mapDependencies` and `mapContinuation` should be objects of + // the same "shape" each run - that is, the values will change depending on + // outside context, but the keys are always the same. You shouldn't use + // `mapDependencies` to dynamically select more or fewer dependencies. + // If you need to dynamically select a range of dependencies, just specify + // them in the `dependencies` array like usual. The caching engine will + // understand that differently named `dependencies` indicate separate + // input-output caches should be used. + // + // The 'options' property makes it possible to specify external arguments + // that fundamentally change the behavior of the `compute` function, while + // still remaining cache-safe. It indicates that the caching engine should + // use a completely different input-to-output cache for each permutation + // of the 'options' values. This way, those functions are still cacheable + // at all; they'll just be cached separately for each set of option values. + // Values on the 'options' property will always be provided in compute's + // dependencies under '#options' (to avoid name conflicts with other + // dependencies). + // // == To compute or to transform: == // // A compositional step can work directly on a property's stored update @@ -725,7 +816,7 @@ export default class Thing extends CacheableObject { const continuationSymbol = Symbol(); const noTransformSymbol = Symbol(); - const _filterDependencies = (dependencies, step) => { + function _filterDependencies(dependencies, step) { const filteredDependencies = (step.dependencies ? filterProperties(dependencies, step.dependencies) @@ -737,10 +828,14 @@ export default class Thing extends CacheableObject { } } + if (step.options) { + filteredDependencies['#options'] = step.options; + } + return filteredDependencies; - }; + } - const _assignDependencies = (continuationAssignment, step) => { + function _assignDependencies(continuationAssignment, step) { if (!step.mapContinuation) { return continuationAssignment; } @@ -752,9 +847,9 @@ export default class Thing extends CacheableObject { } return assignDependencies; - }; + } - const _computeOrTransform = (value, initialDependencies) => { + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; let valueSoFar = value; @@ -811,7 +906,7 @@ export default class Thing extends CacheableObject { } else { return base.expose.compute(filteredDependencies); } - }; + } if (base.flags.update) { expose.transform = @@ -842,9 +937,10 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { + options: {mappingEntries}, dependencies: Object.values(mapping), - compute(dependencies, continuation) { + compute({'#options': {mappingEntries}, ...dependencies}, continuation) { const exports = {}; // Note: This is slightly different behavior from filterProperties, @@ -890,9 +986,9 @@ export default class Thing extends CacheableObject { // or null, if the reference doesn't match anything or itself was null // to begin with. withResolvedReference({ - ref: refDependency, - data: dataDependency, - to: outputDependency, + ref, + data, + to, find: findFunction, earlyExitIfNotFound = false, }) { @@ -901,17 +997,19 @@ export default class Thing extends CacheableObject { flags: {expose: true, compose: true}, expose: { - dependencies: [refDependency, dataDependency], + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {to}, - compute({[refDependency]: ref, [dataDependency]: data}, continuation) { - if (!ref) return continuation({[outputDependency]: null}); + compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { + if (!ref) return continuation({to: null}); if (data === null) return null; const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && earlyExitIfNotFound) return null; - return continuation({[outputDependency]: match}); + return continuation({to: match}); }, }, }; -- cgit 1.3.0-6-gf8a5 From ba1cf3fe611661c85ef4a7150c924f99e1e94ba3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 21:10:46 -0300 Subject: data: bug fixes & Thing.composite.from.debug mode --- src/data/things/thing.js | 272 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 227 insertions(+), 45 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1bca6c38..555e443d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -508,7 +508,9 @@ export default class Thing extends CacheableObject { // } // // Performing an early exit is as simple as returning some other value, - // instead of the continuation. + // instead of the continuation. You may also use `continuation.exit(value)` + // to perform the exact same kind of early exit - it's just a different + // syntax that might fit in better in certain longer compositions. // // It may be fine to simply provide new dependencies under a hard-coded // name, such as '#excitingProperty' above, but if you're writing a utility @@ -715,6 +717,17 @@ export default class Thing extends CacheableObject { // still dynamically provided dependencies!) // from(firstArg, secondArg) { + const debug = fn => { + if (Thing.composite.from.debug === true) { + const result = fn(); + if (Array.isArray(result)) { + console.log(`[composite]`, ...result); + } else { + console.log(`[composite]`, result); + } + } + }; + let annotation, composition; if (typeof firstArg === 'string') { [annotation, composition] = [firstArg, secondArg]; @@ -738,6 +751,13 @@ export default class Thing extends CacheableObject { const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); + if (base.expose?.mapDependencies) { + for (const dependency of Object.values(base.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const message = @@ -767,6 +787,13 @@ export default class Thing extends CacheableObject { } } + if (step.expose.mapDependencies) { + for (const dependency of Object.values(step.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + let fn, type; if (base.flags.update) { if (step.expose.transform) { @@ -813,8 +840,8 @@ export default class Thing extends CacheableObject { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); - const continuationSymbol = Symbol(); - const noTransformSymbol = Symbol(); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); function _filterDependencies(dependencies, step) { const filteredDependencies = @@ -849,48 +876,153 @@ export default class Thing extends CacheableObject { return assignDependencies; } + function _prepareContinuation(transform, step) { + const continuationStorage = { + returnedWith: null, + providedDependencies: null, + providedValue: null, + }; + + const continuation = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (base.flags.compose) { + continuation.raise = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + } + + return {continuation, continuationStorage}; + } + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; - let valueSoFar = value; + let valueSoFar = value; // Set only for {update: true} compositions + let exportDependencies = null; // Set only for {compose: true} compositions + + debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + + for (let i = 0; i < exposeSteps.length; i++) { + const step = exposeSteps[i]; + debug(() => [`step #${i+1}:`, step]); + + const transform = + valueSoFar !== noTransformSymbol && + step.transform; - for (const step of exposeSteps) { const filteredDependencies = _filterDependencies(dependencies, step); + const {continuation, continuationStorage} = _prepareContinuation(transform, step); - let assignDependencies = null; + if (transform) { + debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } else { + debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } const result = - (valueSoFar !== noTransformSymbol && step.transform - ? step.transform( - valueSoFar, filteredDependencies, - (updatedValue, providedDependencies) => { - valueSoFar = updatedValue ?? null; - assignDependencies = providedDependencies; - return continuationSymbol; - }) - : step.compute( - filteredDependencies, - (providedDependencies) => { - assignDependencies = providedDependencies; - return continuationSymbol; - })); + (transform + ? step.transform(valueSoFar, filteredDependencies, continuation) + : step.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { + if (base.flags.compose) { + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + } + + debug(() => `step #${i+1} - early-exit (inferred)`); + debug(() => `early-exit: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return result; } - Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + if (continuationStorage.returnedWith === 'exit') { + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationSymbol.providedValue; + } + + if (continuationStorage.returnedWith === 'raise') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + + debug(() => `step #${i+1} - result: raise`); + + break; + } + + if (continuationStorage.returnedWith === 'continuation') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + debug(() => `step #${i+1} - result: continuation`); + + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + + debug(() => [`assign dependencies:`, assignDependencies]); + } + } } + if (exportDependencies) { + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); + } + + debug(() => `completed all steps, reached base`); + const filteredDependencies = _filterDependencies(dependencies, base.expose); // Note: base.flags.compose is not compatible with base.flags.update. if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); + debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.transform(valueSoFar, filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + + return result; } else if (base.flags.compose) { - const continuation = continuationIfApplicable; + const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); - let exportDependencies; + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = base.expose.compute(filteredDependencies, providedDependencies => { @@ -899,12 +1031,39 @@ export default class Thing extends CacheableObject { }); if (result !== continuationSymbol) { - return result; + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'continuation') { + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'exit') { + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationStorage.providedValue; } - return continuation(_assignDependencies(exportDependencies, base.expose)); + if (continuationStorage.returnedWith === 'raise') { + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); + + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationIfApplicable(exportDependencies); + } } else { - return base.expose.compute(filteredDependencies); + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.compute(filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return result; } } @@ -953,7 +1112,7 @@ export default class Thing extends CacheableObject { : null); } - return continuation(exports); + return continuation.raise(exports); } }, }; @@ -985,34 +1144,57 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference({ + withResolvedReference: ({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) { - return { - annotation: `Thing.composite.withResolvedReference`, - flags: {expose: true, compose: true}, + }) => + Thing.composite.from(`Thing.composite.withResolvedReference`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {ref}, + mapContinuation: {to}, + + compute: ({ref}, continuation) => + (ref + ? continuation() + : continuation.raise({to: null})), + }, + }, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {to}, + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {data}, + + compute: ({data}, continuation) => + (data === null + ? continuation.exit(null) + : continuation()), + }, + }, - compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { - if (!ref) return continuation({to: null}); + { + flags: {expose: true, compose: true}, + expose: { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - if (data === null) return null; + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) return null; + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation({to: match}); + return continuation({match}); + }, }, }, - }; - } + ]), }; } -- cgit 1.3.0-6-gf8a5 From 2a87cbaf06e364585bae2b48919c46b6d1f7aa1f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 13:28:29 -0300 Subject: data: Thing.composite.from bugfixes --- src/data/things/thing.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 555e443d..16dd786d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -922,7 +922,7 @@ export default class Thing extends CacheableObject { return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies) { + function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { const dependencies = {...initialDependencies}; let valueSoFar = value; // Set only for {update: true} compositions @@ -1020,15 +1020,11 @@ export default class Thing extends CacheableObject { return result; } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); + const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - const result = - base.expose.compute(filteredDependencies, providedDependencies => { - exportDependencies = providedDependencies; - return continuationSymbol; - }); + const result = base.expose.compute(filteredDependencies, continuation); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); @@ -1069,12 +1065,12 @@ export default class Thing extends CacheableObject { if (base.flags.update) { expose.transform = - (value, initialDependencies) => - _computeOrTransform(value, initialDependencies); + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); } else { expose.compute = - (initialDependencies) => - _computeOrTransform(noTransformSymbol, initialDependencies); + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); } } -- cgit 1.3.0-6-gf8a5 From fe9bd87d1e6b71c3019b38ca2f99e0c21d916186 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 25 Aug 2023 13:28:56 -0300 Subject: data: use continuation.exit and continuation.raise where needed --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16dd786d..578a5a4e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1187,7 +1187,7 @@ export default class Thing extends CacheableObject { return continuation.exit(null); } - return continuation({match}); + return continuation.raise({match}); }, }, }, -- cgit 1.3.0-6-gf8a5 From e2f1cd30f8d5804f97043faedc5aea9fe06cea32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 18:46:14 -0300 Subject: data: Thing.composite.from: fix undefined return for explicit exit --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 578a5a4e..c870b89c 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -969,7 +969,7 @@ export default class Thing extends CacheableObject { debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); - return continuationSymbol.providedValue; + return continuationStorage.providedValue; } if (continuationStorage.returnedWith === 'raise') { -- cgit 1.3.0-6-gf8a5 From 25beb8731d756bfa4fe6babb9e4b0a707c7823e0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 19:22:38 -0300 Subject: data, test: misc. additions * Thing.composite.expose * Thing.composite.exposeUpdateValueOrContinue * Track.composite.withAlbumProperty * refactor: Track.color, Track.album, Track.date * refactor: Track.coverArtistContribs * test: Track.album (unit) --- src/data/things/thing.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c870b89c..2af06904 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1114,6 +1114,57 @@ export default class Thing extends CacheableObject { }; }, + // Exposes a dependency exactly as it is; this is typically the base of a + // composition which was created to serve as one property's descriptor. + // Since this serves as a base, specify {update: true} to indicate that + // the property as a whole updates (and some previous compositional step + // works with that update value). + // + // Please note that this *doesn't* verify that the dependency exists, so + // if you provide the wrong name or it hasn't been set by a previous + // compositional step, the property will be exposed as undefined instead + // of null. + // + expose: (dependency, {update = false} = {}) => ({ + annotation: `Thing.composite.expose`, + flags: {expose: true, update}, + + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, + }), + + // Exposes the update value of an {update: true} property, or continues if + // it's unavailable. By default, "unavailable" means value === null, but + // set {mode: 'empty'} to + exposeUpdateValueOrContinue({mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeUpdateValueOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + + transform(value, {'#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(value) + : value === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(value); + } + } + }, + }; + }, + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist -- cgit 1.3.0-6-gf8a5 From 12b8040b05e81a523ef59ba583dde751206f2e1d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 20:38:27 -0300 Subject: data, test: retain validator for Track.color --- src/data/things/thing.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2af06904..2adba5c4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1116,9 +1116,11 @@ export default class Thing extends CacheableObject { // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify {update: true} to indicate that - // the property as a whole updates (and some previous compositional step - // works with that update value). + // Since this serves as a base, specify a value for {update} to indicate + // that the property as a whole updates (and some previous compositional + // step works with that update value). Set {update: true} to only enable + // the update flag, or set update to an object to specify a descriptor + // (e.g. for custom value validation). // // Please note that this *doesn't* verify that the dependency exists, so // if you provide the wrong name or it hasn't been set by a previous @@ -1127,17 +1129,23 @@ export default class Thing extends CacheableObject { // expose: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, - flags: {expose: true, update}, + flags: {expose: true, update: !!update}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, + + update: + (typeof update === 'object' + ? update + : null), }), // Exposes the update value of an {update: true} property, or continues if // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to + // set {mode: 'empty'} to check with empty() instead, continuing for empty + // arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1156,7 +1164,7 @@ export default class Thing extends CacheableObject { : value === null); if (shouldContinue) { - return continuation(); + return continuation(value); } else { return continuation.exit(value); } -- cgit 1.3.0-6-gf8a5 From e6038d8c07971447f444cf597328ca8d9863f8fd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 21:18:43 -0300 Subject: data, test: Track.color inherits from track section --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 2adba5c4..f5dc786e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1127,7 +1127,7 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - expose: (dependency, {update = false} = {}) => ({ + exposeDependency: (dependency, {update = false} = {}) => ({ annotation: `Thing.composite.expose`, flags: {expose: true, update: !!update}, -- cgit 1.3.0-6-gf8a5 From 618f49e0ddcea245a4e0972efe5450419b27c639 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 26 Aug 2023 21:31:07 -0300 Subject: data: Thing.composite.exposeDependencyOrContinue --- src/data/things/thing.js | 48 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f5dc786e..f88e8726 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1077,6 +1077,8 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // -- Compositional steps for compositions to nest -- + // Provides dependencies exactly as they are (or null if not defined) to the // continuation. Although this can *technically* be used to alias existing // dependencies to some other name within the middle of a composition, it's @@ -1114,6 +1116,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for top-level property descriptors -- + // Exposes a dependency exactly as it is; this is typically the base of a // composition which was created to serve as one property's descriptor. // Since this serves as a base, specify a value for {update} to indicate @@ -1128,7 +1132,7 @@ export default class Thing extends CacheableObject { // of null. // exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.expose`, + annotation: `Thing.composite.exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -1142,10 +1146,42 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes the update value of an {update: true} property, or continues if - // it's unavailable. By default, "unavailable" means value === null, but - // set {mode: 'empty'} to check with empty() instead, continuing for empty - // arrays also. + // Exposes a dependency as it is, or continues if it's unavailable. + // By default, "unavailable" means dependency === null; provide + // {mode: 'empty'} to check with empty() instead, continuing for + // empty arrays also. + exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { + if (mode !== 'null' && mode !== 'empty') { + throw new TypeError(`Expected mode to be null or empty`); + } + + return { + annotation: `Thing.composite.exposeDependencyOrContinue`, + flags: {expose: true, compose: true}, + expose: { + options: {mode}, + mapDependencies: {dependency}, + + compute({dependency, '#options': {mode}}, continuation) { + const shouldContinue = + (mode === 'empty' + ? empty(dependency) + : dependency === null); + + if (shouldContinue) { + return continuation(); + } else { + return continuation.exit(dependency); + } + }, + }, + }; + }, + + // Exposes the update value of an {update: true} property as it is, + // or continues if it's unavailable. By default, "unavailable" means + // value === null; provide {mode: 'empty'} to check with empty() instead, + // continuing for empty arrays also. exposeUpdateValueOrContinue({mode = 'null'} = {}) { if (mode !== 'null' && mode !== 'empty') { throw new TypeError(`Expected mode to be null or empty`); @@ -1173,6 +1209,8 @@ export default class Thing extends CacheableObject { }; }, + // -- Compositional steps for processing data -- + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist -- cgit 1.3.0-6-gf8a5 From 083a4b8c3a0e545a2d8195255d57c5b7e0c49028 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 27 Aug 2023 16:15:34 -0300 Subject: data: misc. additions, fixes & refactoring Thing.composite.from: * Transparently support expose.transform steps inside nested compositions, w/ various Thing.composite.from clean-up * Support continuation.raise() without provided dependencies * add Thing.composite.exposeConstant * add Thing.composite.withResultOfAvailabilityCheck * supports {mode: 'null' | 'empty' | 'falsy'} * works with dependency or update value * add Thing.composite.earlyExitWithoutDependency * refactor Thing.composite.exposeDependencyOrContinue * refactor Thing.composite.exposeUpdateValueOrContinue * add Track.withHasUniqueCoverArt * refactor Track.coverArtFileExtension * refactor Track.hasUniqueCoverArt --- src/data/things/thing.js | 433 +++++++++++++++++++++++++++++------------------ 1 file changed, 266 insertions(+), 167 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f88e8726..892a3a4b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -780,6 +780,16 @@ export default class Thing extends CacheableObject { break expose; } + if ( + step.expose.transform && + !step.expose.compute && + !base.flags.update && + !base.flags.compose + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + break expose; + } + if (step.expose.dependencies) { for (const dependency of step.expose.dependencies) { if (typeof dependency === 'string' && dependency.startsWith('#')) continue; @@ -794,26 +804,7 @@ export default class Thing extends CacheableObject { } } - let fn, type; - if (base.flags.update) { - if (step.expose.transform) { - type = 'transform'; - fn = step.expose.transform; - } else { - type = 'compute'; - fn = step.expose.compute; - } - } else { - if (step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; - } - - type = 'compute'; - fn = step.expose.compute; - } - - exposeSteps.push(step.expose); + exposeSteps.push(step); } }); } @@ -845,38 +836,38 @@ export default class Thing extends CacheableObject { function _filterDependencies(dependencies, step) { const filteredDependencies = - (step.dependencies - ? filterProperties(dependencies, step.dependencies) + (step.expose.dependencies + ? filterProperties(dependencies, step.expose.dependencies) : {}); - if (step.mapDependencies) { - for (const [to, from] of Object.entries(step.mapDependencies)) { + if (step.expose.mapDependencies) { + for (const [to, from] of Object.entries(step.expose.mapDependencies)) { filteredDependencies[to] = dependencies[from] ?? null; } } - if (step.options) { - filteredDependencies['#options'] = step.options; + if (step.expose.options) { + filteredDependencies['#options'] = step.expose.options; } return filteredDependencies; } function _assignDependencies(continuationAssignment, step) { - if (!step.mapContinuation) { + if (!step.expose.mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.mapContinuation)) { + for (const [from, to] of Object.entries(step.expose.mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform, step) { + function _prepareContinuation(transform) { const continuationStorage = { returnedWith: null, providedDependencies: null, @@ -930,27 +921,25 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition (annotation: ${annotation})`)); - for (let i = 0; i < exposeSteps.length; i++) { + stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; debug(() => [`step #${i+1}:`, step]); const transform = valueSoFar !== noTransformSymbol && - step.transform; + step.expose.transform; const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform, step); + const {continuation, continuationStorage} = _prepareContinuation(transform); - if (transform) { - debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } else { - debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - } + debug(() => + `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = (transform - ? step.transform(valueSoFar, filteredDependencies, continuation) - : step.compute(filteredDependencies, continuation)); + ? step.expose.transform(valueSoFar, filteredDependencies, continuation) + : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { if (base.flags.compose) { @@ -964,39 +953,34 @@ export default class Thing extends CacheableObject { return result; } - if (continuationStorage.returnedWith === 'exit') { - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - - debug(() => `step #${i+1} - result: raise`); - - break; - } - - if (continuationStorage.returnedWith === 'continuation') { - if (transform) { - valueSoFar = continuationStorage.providedValue; - } - - debug(() => `step #${i+1} - result: continuation`); + switch (continuationStorage.returnedWith) { + case 'exit': + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + debug(() => `step #${i+1} - result: raise`); + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; + if (transform) valueSoFar = continuationStorage.providedValue; + break stepLoop; + + case 'continuation': + if (transform) { + valueSoFar = continuationStorage.providedValue; + } - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + debug(() => `step #${i+1} - result: continuation`); + debug(() => [`assign dependencies:`, assignDependencies]); + } else { + debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); + } - debug(() => [`assign dependencies:`, assignDependencies]); - } + break; } } @@ -1008,53 +992,50 @@ export default class Thing extends CacheableObject { debug(() => `completed all steps, reached base`); - const filteredDependencies = _filterDependencies(dependencies, base.expose); + const filteredDependencies = _filterDependencies(dependencies, base); - // Note: base.flags.compose is not compatible with base.flags.update. - if (base.expose.transform) { - debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + const transform = + valueSoFar !== noTransformSymbol && + base.expose.transform; - const result = base.expose.transform(valueSoFar, filteredDependencies); + debug(() => + `base - ${transform ? 'transform' : 'compute'} ` + + `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - - return result; - } else if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(false, base.expose); - - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + if (base.flags.compose) { + const {continuation, continuationStorage} = _prepareContinuation(transform); - const result = base.expose.compute(filteredDependencies, continuation); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies, continuation) + : base.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); } - if (continuationStorage.returnedWith === 'continuation') { - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); - } - - if (continuationStorage.returnedWith === 'exit') { - debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationStorage.providedValue; - } - - if (continuationStorage.returnedWith === 'raise') { - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); - - debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - - return continuationIfApplicable(exportDependencies); + switch (continuationStorage.returnedWith) { + case 'continuation': + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + + case 'exit': + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationStorage.providedValue; + + case 'raise': + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); } } else { - debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); - - const result = base.expose.compute(filteredDependencies); + const result = + (transform + ? base.expose.transform(valueSoFar, filteredDependencies) + : base.expose.compute(filteredDependencies)); debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); debug(() => color.bright(`end composition (annotation: ${annotation})`)); @@ -1063,14 +1044,23 @@ export default class Thing extends CacheableObject { } } - if (base.flags.update) { - expose.transform = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); + const transformFn = + (value, initialDependencies, continuationIfApplicable) => + _computeOrTransform(value, initialDependencies, continuationIfApplicable); + + const computeFn = + (initialDependencies, continuationIfApplicable) => + _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + + if (base.flags.compose) { + if (exposeSteps.some(step => step.expose.transform)) { + expose.transform = transformFn; + } + expose.compute = computeFn; + } else if (base.flags.update) { + expose.transform = transformFn; } else { - expose.compute = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); + expose.compute = computeFn; } } @@ -1146,68 +1136,177 @@ export default class Thing extends CacheableObject { : null), }), - // Exposes a dependency as it is, or continues if it's unavailable. - // By default, "unavailable" means dependency === null; provide - // {mode: 'empty'} to check with empty() instead, continuing for - // empty arrays also. - exposeDependencyOrContinue(dependency, {mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); + // Exposes a constant value exactly as it is; like exposeDependency, this + // is typically the base of a composition serving as a particular property + // descriptor. It generally follows steps which will conditionally early + // exit with some other value, with the exposeConstant base serving as the + // fallback default value. Like exposeDependency, set {update} to true or + // an object to indicate that the property as a whole updates. + exposeConstant: (value, {update = false} = {}) => ({ + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, + + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, + + update: + (typeof update === 'object' + ? update + : null), + }), + + // Checks the availability of a dependency or the update value and provides + // the result to later steps under '#availability' (by default). This is + // mainly intended for use by the more specific utilities, which you should + // consider using instead. Customize {mode} to select one of these modes, + // or leave unset and default to 'null': + // + // * 'null': Check that the value isn't null. + // * 'empty': Check that the value is neither null nor an empty array. + // * 'falsy': Check that the value isn't false when treated as a boolean + // (nor an empty array). Keep in mind this will also be false + // for values like zero and the empty string! + // + withResultOfAvailabilityCheck({ + fromUpdateValue, + fromDependency, + mode = 'null', + to = '#availability', + }) { + if (!['null', 'empty', 'falsy'].includes(mode)) { + throw new TypeError(`Expected mode to be null, empty, or falsy`); } - return { - annotation: `Thing.composite.exposeDependencyOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - mapDependencies: {dependency}, - - compute({dependency, '#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(dependency) - : dependency === null); - - if (shouldContinue) { - return continuation(); - } else { - return continuation.exit(dependency); - } - }, - }, + if (fromUpdateValue && fromDependency) { + throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); + } + + if (!fromUpdateValue && !fromDependency) { + throw new TypeError(`Missing dependency name (or fromUpdateValue)`); + } + + const checkAvailability = (value, mode) => { + switch (mode) { + case 'null': return value !== null; + case 'empty': return !empty(value); + case 'falsy': return !empty(value) && !!value; + default: return false; + } }; + + if (fromDependency) { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {from: fromDependency}, + mapContinuation: {to}, + options: {mode}, + compute: ({from, '#options': {mode}}, continuation) => + continuation({to: checkAvailability(from, mode)}), + }, + }; + } else { + return { + annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + flags: {expose: true, compose: true}, + expose: { + mapContinuation: {to}, + options: {mode}, + transform: (value, {'#options': {mode}}, continuation) => + continuation(value, {to: checkAvailability(value, mode)}), + }, + }; + } }, + // Exposes a dependency as it is, or continues if it's unavailable. + // See withResultOfAvailabilityCheck for {mode} options! + exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), + }, + }, + ]), + // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. By default, "unavailable" means - // value === null; provide {mode: 'empty'} to check with empty() instead, - // continuing for empty arrays also. - exposeUpdateValueOrContinue({mode = 'null'} = {}) { - if (mode !== 'null' && mode !== 'empty') { - throw new TypeError(`Expected mode to be null or empty`); - } + // or continues if it's unavailable. See withResultOfAvailabilityCheck + // for {mode} options! + exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => + Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromUpdateValue: true, + mode, + }), - return { - annotation: `Thing.composite.exposeUpdateValueOrContinue`, - flags: {expose: true, compose: true}, - expose: { - options: {mode}, - - transform(value, {'#options': {mode}}, continuation) { - const shouldContinue = - (mode === 'empty' - ? empty(value) - : value === null); - - if (shouldContinue) { - return continuation(value); - } else { - return continuation.exit(value); - } - } + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), + }, }, - }; - }, + + { + flags: {expose: true, compose: true}, + expose: { + transform: (value, {}, continuation) => + continuation.exit(value), + }, + }, + ]), + + // Early exits if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => + Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({ + fromDependency: dependency, + mode, + }), + + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + options: {value}, + + compute: ({ + '#availability': availability, + '#options': {value}, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + }, + ]), // -- Compositional steps for processing data -- -- cgit 1.3.0-6-gf8a5 From 29580733b79872333f3f9e45d50d987218d334ea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 28 Aug 2023 13:19:57 -0300 Subject: data: fix annotation typo --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 892a3a4b..78ff4c81 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1198,7 +1198,7 @@ export default class Thing extends CacheableObject { if (fromDependency) { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromDependency`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -1210,7 +1210,7 @@ export default class Thing extends CacheableObject { }; } else { return { - annotation: `Thing.composite.withResultOfCommonComparison.fromUpdateValue`, + annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, -- cgit 1.3.0-6-gf8a5 From 895712f5a0381c41557c6d306d6697019368bb7b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:33:46 -0300 Subject: data: clean up Thing.composite.from debug messaging * print annotation next to every log message, instead of just the begin/end messages * add Thing.composite.debug() to conveniently wrap one property access * don't output (and don't access) track album in inspect.custom when depth < 0 --- src/data/things/thing.js | 69 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 23 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 78ff4c81..4fd6a26a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -719,11 +719,18 @@ export default class Thing extends CacheableObject { from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { + const label = + (annotation + ? color.dim(`[composite: ${annotation}]`) + : color.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { - console.log(`[composite]`, ...result); + console.log(label, ...result.map(value => + (typeof value === 'object' + ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + : value))); } else { - console.log(`[composite]`, result); + console.log(label, result); } } }; @@ -919,7 +926,7 @@ export default class Thing extends CacheableObject { let valueSoFar = value; // Set only for {update: true} compositions let exportDependencies = null; // Set only for {compose: true} compositions - debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + debug(() => color.bright(`begin composition`)); stepLoop: for (let i = 0; i < exposeSteps.length; i++) { const step = exposeSteps[i]; @@ -932,9 +939,9 @@ export default class Thing extends CacheableObject { const filteredDependencies = _filterDependencies(dependencies, step); const {continuation, continuationStorage} = _prepareContinuation(transform); - debug(() => - `step #${i+1} - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); const result = (transform @@ -946,18 +953,15 @@ export default class Thing extends CacheableObject { throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); } - debug(() => `step #${i+1} - early-exit (inferred)`); - debug(() => `early-exit: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); - + debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); + debug(() => color.bright(`end composition`)); return result; } switch (continuationStorage.returnedWith) { case 'exit': - debug(() => `step #${i+1} - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': @@ -986,7 +990,7 @@ export default class Thing extends CacheableObject { if (exportDependencies) { debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } @@ -998,9 +1002,9 @@ export default class Thing extends CacheableObject { valueSoFar !== noTransformSymbol && base.expose.transform; - debug(() => - `base - ${transform ? 'transform' : 'compute'} ` + - `with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + debug(() => [ + `base - ${transform ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); if (base.flags.compose) { const {continuation, continuationStorage} = _prepareContinuation(transform); @@ -1020,15 +1024,15 @@ export default class Thing extends CacheableObject { case 'exit': debug(() => `base - result: early-exit (explicit)`); - debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`early-exit:`, continuationStorage.providedValue]); + debug(() => color.bright(`end composition`)); return continuationStorage.providedValue; case 'raise': exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); debug(() => `base - result: raise`); - debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition`)); return continuationIfApplicable(exportDependencies); } } else { @@ -1037,8 +1041,8 @@ export default class Thing extends CacheableObject { ? base.expose.transform(valueSoFar, filteredDependencies) : base.expose.compute(filteredDependencies)); - debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); - debug(() => color.bright(`end composition (annotation: ${annotation})`)); + debug(() => [`base - non-compose (final) result:`, result]); + debug(() => color.bright(`end composition`)); return result; } @@ -1067,6 +1071,25 @@ export default class Thing extends CacheableObject { return constructedDescriptor; }, + // Evaluates a function with composite debugging enabled, turns debugging + // off again, and returns the result of the function. This is mostly syntax + // sugar, but also helps avoid unit tests avoid accidentally printing debug + // info for a bunch of unrelated composites (due to property enumeration + // when displaying an unexpected result). Use as so: + // + // Without debugging: + // t.same(thing.someProp, value) + // + // With debugging: + // t.same(Thing.composite.debug(() => thing.someProp), value) + // + debug(fn) { + Thing.composite.from.debug = true; + const value = fn(); + Thing.composite.from.debug = false; + return value; + }, + // -- Compositional steps for compositions to nest -- // Provides dependencies exactly as they are (or null if not defined) to the -- cgit 1.3.0-6-gf8a5 From 3336d5f15e29350656273a37c0a1c7a69d24663b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:56:04 -0300 Subject: data: Thing.composite.from: fix including '#' deps from base ...in the final composition's dependencies. --- src/data/things/thing.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 4fd6a26a..25d8c8a3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -756,7 +756,14 @@ export default class Thing extends CacheableObject { } const exposeSteps = []; - const exposeDependencies = new Set(base.expose?.dependencies); + const exposeDependencies = new Set(); + + if (base.expose?.dependencies) { + for (const dependency of base.expose.dependencies) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } if (base.expose?.mapDependencies) { for (const dependency of Object.values(base.expose.mapDependencies)) { -- cgit 1.3.0-6-gf8a5 From 7d6d8a2839ece38c4a70bd9e3fda73b2e0aa39b8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 15:57:22 -0300 Subject: data: Thing.composite.earlyExitWithoutDependency: latest syntax --- src/data/things/thing.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 25d8c8a3..6bdc897f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1321,19 +1321,25 @@ export default class Thing extends CacheableObject { mode, }), + { + flags: {expose: true, compose: true}, + expose: { + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + { flags: {expose: true, compose: true}, expose: { dependencies: ['#availability'], options: {value}, - compute: ({ - '#availability': availability, - '#options': {value}, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, }, ]), -- cgit 1.3.0-6-gf8a5 From 011c197aeedab56d501b03b800433dd0cd9bc4f7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 30 Aug 2023 16:28:47 -0300 Subject: data: always define composite utilities with `key() {}` syntax Sublime Text doesn't index the key in `key: () => {}` as a symbol for function definitions if the parameter list takes up more than one line, but always works for `key() {}`. This also just makes it a little easier to add "preamble" before the main return value, when relevant. Consistent syntax is usually a plus for recurring behavioral forms! --- src/data/things/thing.js | 121 +++++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 50 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 6bdc897f..cd62288e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1151,20 +1151,24 @@ export default class Thing extends CacheableObject { // compositional step, the property will be exposed as undefined instead // of null. // - exposeDependency: (dependency, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, + exposeDependency(dependency, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeDependency`, + flags: {expose: true, update: !!update}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, + expose: { + mapDependencies: {dependency}, + compute: ({dependency}) => dependency, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1172,20 +1176,24 @@ export default class Thing extends CacheableObject { // exit with some other value, with the exposeConstant base serving as the // fallback default value. Like exposeDependency, set {update} to true or // an object to indicate that the property as a whole updates. - exposeConstant: (value, {update = false} = {}) => ({ - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, + exposeConstant(value, { + update = false, + } = {}) { + return { + annotation: `Thing.composite.exposeConstant`, + flags: {expose: true, update: !!update}, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, + expose: { + options: {value}, + compute: ({'#options': {value}}) => value, + }, - update: - (typeof update === 'object' - ? update - : null), - }), + update: + (typeof update === 'object' + ? update + : null), + }; + }, // Checks the availability of a dependency or the update value and provides // the result to later steps under '#availability' (by default). This is @@ -1254,8 +1262,10 @@ export default class Thing extends CacheableObject { // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue: (dependency, {mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ + exposeDependencyOrContinue(dependency, { + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1280,13 +1290,16 @@ export default class Thing extends CacheableObject { continuation.exit(dependency), }, }, - ]), + ]); + }, // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck // for {mode} options! - exposeUpdateValueOrContinue: ({mode = 'null'} = {}) => - Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ + exposeUpdateValueOrContinue({ + mode = 'null', + } = {}) { + return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ Thing.composite.withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1310,12 +1323,16 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency: (dependency, {mode = 'null', value = null} = {}) => - Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ Thing.composite.withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -1342,7 +1359,8 @@ export default class Thing extends CacheableObject { continuation.exit(value), }, }, - ]), + ]); + }, // -- Compositional steps for processing data -- @@ -1350,20 +1368,22 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs: ({from, to}) => ({ - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + withResolvedContribs({from, to}) { + return { + annotation: `Thing.composite.withResolvedContribs`, + flags: {expose: true, compose: true}, - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }), + expose: { + dependencies: ['artistData'], + mapDependencies: {from}, + mapContinuation: {to}, + compute: ({artistData, from}, continuation) => + continuation({ + to: Thing.findArtistsFromContribs(from, artistData), + }), + }, + }; + }, // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -1372,14 +1392,14 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference: ({ + withResolvedReference({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) => - Thing.composite.from(`Thing.composite.withResolvedReference`, [ + }) { + return Thing.composite.from(`Thing.composite.withResolvedReference`, [ { flags: {expose: true, compose: true}, expose: { @@ -1423,6 +1443,7 @@ export default class Thing extends CacheableObject { }, }, }, - ]), + ]); + }, }; } -- cgit 1.3.0-6-gf8a5 From 001bcb69db4f4050fca222568ae2895f58a2f0df Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:51:01 -0300 Subject: data: simplify Thing.composite.from (needs docs update) --- src/data/things/thing.js | 402 +++++++++++++++++++++++++++-------------------- 1 file changed, 230 insertions(+), 172 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cd62288e..782946ce 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -743,7 +743,7 @@ export default class Thing extends CacheableObject { } const base = composition.at(-1); - const steps = composition.slice(0, -1); + const steps = composition.slice(); const aggregate = openAggregate({ message: @@ -751,78 +751,118 @@ export default class Thing extends CacheableObject { (annotation ? ` (${annotation})` : ''), }); - if (base.flags.compose && base.flags.compute) { - push(new TypeError(`Base which composes can't also update yet`)); - } + const baseExposes = + (base.flags + ? base.flags.expose + : true); - const exposeSteps = []; - const exposeDependencies = new Set(); + const baseUpdates = + (base.flags + ? base.flags.update + : false); - if (base.expose?.dependencies) { - for (const dependency of base.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } - } + const baseComposes = + (base.flags + ? base.flags.compose + : true); - if (base.expose?.mapDependencies) { - for (const dependency of Object.values(base.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); } + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + for (let i = 0; i < steps.length; i++) { const step = steps[i]; + const isBase = i === steps.length - 1; const message = - (step.annotation - ? `Errors in step #${i + 1} (${step.annotation})` - : `Errors in step #${i + 1}`); + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (!step.flags.compose) { - push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); - } + if (step.flags) { + let flagsErrored = false; - if (step.flags.update) { - push(new Error(`Steps which update aren't supported yet`)); - } - - if (step.flags.expose) expose: { - if (!step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); - break expose; + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; } - if ( - step.expose.transform && - !step.expose.compute && - !base.flags.update && - !base.flags.compose - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; } - if (step.expose.dependencies) { - for (const dependency of step.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (flagsErrored) { + return; } + } - if (step.expose.mapDependencies) { - for (const dependency of Object.values(step.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; } - exposeSteps.push(step); + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); } }); } + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + aggregate.close(); const constructedDescriptor = {}; @@ -832,64 +872,68 @@ export default class Thing extends CacheableObject { } constructedDescriptor.flags = { - update: !!base.flags.update, - expose: !!base.flags.expose, - compose: !!base.flags.compose, + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, }; - if (base.flags.update) { + if (baseUpdates) { constructedDescriptor.update = base.update; } - if (base.flags.expose) { + if (baseExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol('continuation symbol'); const noTransformSymbol = Symbol('no-transform symbol'); - function _filterDependencies(dependencies, step) { + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { const filteredDependencies = - (step.expose.dependencies - ? filterProperties(dependencies, step.expose.dependencies) + (dependencies + ? filterProperties(availableDependencies, dependencies) : {}); - if (step.expose.mapDependencies) { - for (const [to, from] of Object.entries(step.expose.mapDependencies)) { - filteredDependencies[to] = dependencies[from] ?? null; + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; } } - if (step.expose.options) { - filteredDependencies['#options'] = step.expose.options; + if (options) { + filteredDependencies['#options'] = options; } return filteredDependencies; } - function _assignDependencies(continuationAssignment, step) { - if (!step.expose.mapContinuation) { + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.expose.mapContinuation)) { + for (const [from, to] of Object.entries(mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform) { + function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, - providedDependencies: null, - providedValue: null, + providedDependencies: undefined, + providedValue: undefined, }; const continuation = - (transform + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { continuationStorage.returnedWith = 'continuation'; continuationStorage.providedDependencies = providedDependencies; @@ -908,150 +952,166 @@ export default class Thing extends CacheableObject { return continuationSymbol; }; - if (base.flags.compose) { - continuation.raise = - (transform + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { - const dependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - let valueSoFar = value; // Set only for {update: true} compositions - let exportDependencies = null; // Set only for {compose: true} compositions + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - debug(() => color.bright(`begin composition`)); + const availableDependencies = {...initialDependencies}; - stepLoop: for (let i = 0; i < exposeSteps.length; i++) { - const step = exposeSteps[i]; - debug(() => [`step #${i+1}:`, step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const transform = - valueSoFar !== noTransformSymbol && - step.expose.transform; + stepLoop: for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform); + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ - `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); const result = - (transform + (callingTransformForThisStep ? step.expose.transform(valueSoFar, filteredDependencies, continuation) : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { - if (base.flags.compose) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); - debug(() => color.bright(`end composition`)); + debug(() => color.bright(`end composition - exit (inferred)`)); + return result; } - switch (continuationStorage.returnedWith) { - case 'exit': - debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; - - case 'raise': - debug(() => `step #${i+1} - result: raise`); - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; - if (transform) valueSoFar = continuationStorage.providedValue; - break stepLoop; + const {returnedWith} = continuationStorage; - case 'continuation': - if (transform) { - valueSoFar = continuationStorage.providedValue; - } + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); - debug(() => `step #${i+1} - result: continuation`); - debug(() => [`assign dependencies:`, assignDependencies]); - } else { - debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); - break; + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } } - } - if (exportDependencies) { - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - - debug(() => `completed all steps, reached base`); + const {providedValue, providedDependencies} = continuationStorage; - const filteredDependencies = _filterDependencies(dependencies, base); + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); - const transform = - valueSoFar !== noTransformSymbol && - base.expose.transform; + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - debug(() => [ - `base - ${transform ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform); - - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies, continuation) - : base.expose.compute(filteredDependencies, continuation)); + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; - if (result !== continuationSymbol) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); - } + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } - switch (continuationStorage.returnedWith) { - case 'continuation': - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'exit': - debug(() => `base - result: early-exit (explicit)`); - debug(() => [`early-exit:`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + switch (returnedWith) { case 'raise': - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); - debug(() => `base - result: raise`); - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - } else { - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies) - : base.expose.compute(filteredDependencies)); + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); - debug(() => [`base - non-compose (final) result:`, result]); - debug(() => color.bright(`end composition`)); + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); - return result; + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } } } @@ -1063,12 +1123,10 @@ export default class Thing extends CacheableObject { (initialDependencies, continuationIfApplicable) => _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - if (base.flags.compose) { - if (exposeSteps.some(step => step.expose.transform)) { - expose.transform = transformFn; - } - expose.compute = computeFn; - } else if (base.flags.update) { + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { expose.transform = transformFn; } else { expose.compute = computeFn; @@ -1229,7 +1287,7 @@ export default class Thing extends CacheableObject { switch (mode) { case 'null': return value !== null; case 'empty': return !empty(value); - case 'falsy': return !empty(value) && !!value; + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; } }; -- cgit 1.3.0-6-gf8a5 From c0bbd7e8fa6c76df4fa492e3a9d3b5e9ef42ec5c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:53:02 -0300 Subject: data: misc. utility additions * add earlyExitWithoutUpdateValue * add raiseWithoutDependency * add raiseWithoutUpdateValue * add earlyExitIfAvailabilityCheckFailed (internal) * refactor earlyExitWithoutDependency The "raise" utilities make use of the new `raiseAbove` continuation feature. --- src/data/things/thing.js | 100 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 8 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 782946ce..501286d7 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1384,6 +1384,36 @@ export default class Thing extends CacheableObject { ]); }, + // Early exits if an availability check fails. + // This is for internal use only - use `earlyExitWithoutDependency` or + // `earlyExitWIthoutUpdateValue` instead. + earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, + }) { + return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), + }, + }, + ]); + }, + // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! earlyExitWithoutDependency(dependency, { @@ -1391,10 +1421,32 @@ export default class Thing extends CacheableObject { value = null, } = {}) { return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Early exits if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, + } = {}) { + return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), + ]); + }, + + // Raises if a dependency isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ + Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { flags: {expose: true, compose: true}, @@ -1410,11 +1462,43 @@ export default class Thing extends CacheableObject { { flags: {expose: true, compose: true}, expose: { - dependencies: ['#availability'], - options: {value}, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), + }, + }, + ]); + }, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), + // Raises if this property's update value isn't available. + // See withResultOfAvailabilityCheck for {mode} options! + raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, + } = {}) { + return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ + Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), + }, + }, + + { + flags: {expose: true, compose: true}, + expose: { + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, }, ]); -- cgit 1.3.0-6-gf8a5 From 918fb043a640cf937de604fc74cb95566fa66459 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:56:34 -0300 Subject: data: refactor Thing.composite.withResolvedReference --- src/data/things/thing.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 501286d7..389b3845 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1542,30 +1542,8 @@ export default class Thing extends CacheableObject { earlyExitIfNotFound = false, }) { return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {ref}, - mapContinuation: {to}, - - compute: ({ref}, continuation) => - (ref - ? continuation() - : continuation.raise({to: null})), - }, - }, - - { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {data}, - - compute: ({data}, continuation) => - (data === null - ? continuation.exit(null) - : continuation()), - }, - }, + Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + Thing.composite.earlyExitWithoutDependency(data), { flags: {expose: true, compose: true}, -- cgit 1.3.0-6-gf8a5 From 5a63b96cfd3d26e4b74ff4c6dfc793aef057f81b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 15:57:15 -0300 Subject: data: update Thing.common.dynamicThingsFromReferenceList Only the internal implementation. This should really be updated to take key/value-style parameters, and probably be renamed, but this helps to confirm a swathe of expected behavior continues to work with an existing `common` utility reimplemented compositionally. --- src/data/things/thing.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 389b3845..751e168f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -192,26 +192,29 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList: ( - referenceListProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, + dynamicThingsFromReferenceList( + refs, + data, + findFunction + ) { + return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(refs, {value: []}), + Thing.composite.earlyExitWithoutDependency(data, {value: []}), - expose: { - dependencies: [referenceListProperty, thingDataProperty], - compute: ({ - [referenceListProperty]: refs, - [thingDataProperty]: thingData, - }) => - refs && thingData - ? refs - .map((ref) => findFn(ref, thingData, {mode: 'quiet'})) - .filter(Boolean) - : [], - }, - }), + { + flags: {expose: true}, + expose: { + mapDependencies: {refs, data}, + options: {findFunction}, + + compute: ({refs, data, '#options': {findFunction}}) => + refs + .map(ref => findFunction(ref, data, {mode: 'quiet'})) + .filter(Boolean), + }, + }, + ]); + }, // Corresponding function for a single reference. dynamicThingFromSingleReference: ( -- cgit 1.3.0-6-gf8a5 From f3162203ef1f758d500e065804f9dbe478d0481d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 16:05:53 -0300 Subject: data: Thing.composite.from: fix missed step.expose assumptions --- src/data/things/thing.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 751e168f..d4d7c850 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -993,7 +993,7 @@ export default class Thing extends CacheableObject { debug(() => color.bright(`begin composition - not transforming`)); } - stepLoop: for (let i = 0; i < steps.length; i++) { + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const isBase = i === steps.length - 1; @@ -1021,8 +1021,8 @@ export default class Thing extends CacheableObject { const result = (callingTransformForThisStep - ? step.expose.transform(valueSoFar, filteredDependencies, continuation) - : step.expose.compute(filteredDependencies, continuation)); + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); -- cgit 1.3.0-6-gf8a5 From 56a8e47f0e5ad276baef9d27c16960e3ea2c583b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 16:06:58 -0300 Subject: data: remove lots of boilerplate {expose: true, compose: true} --- src/data/things/thing.js | 133 ++++++++++++++++++----------------------------- 1 file changed, 50 insertions(+), 83 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d4d7c850..15ec62c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1333,23 +1333,17 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, + mapDependencies: {dependency}, + compute: ({dependency}, continuation) => + continuation.exit(dependency), }, ]); }, @@ -1367,22 +1361,16 @@ export default class Thing extends CacheableObject { }), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation() + : continuation.raise()), }, { - flags: {expose: true, compose: true}, - expose: { - transform: (value, {}, continuation) => - continuation.exit(value), - }, + transform: (value, {}, continuation) => + continuation.exit(value), }, ]); }, @@ -1396,23 +1384,17 @@ export default class Thing extends CacheableObject { }) { return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, + options: {value}, + compute: ({'#options': {value}}, continuation) => + continuation.exit(value), }, ]); }, @@ -1452,24 +1434,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { - flags: {expose: true, compose: true}, - expose: { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1485,24 +1461,18 @@ export default class Thing extends CacheableObject { Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + mapDependencies: {availability}, + compute: ({availability}, continuation) => + (availability + ? continuation.raise() + : continuation()), }, { - flags: {expose: true, compose: true}, - expose: { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, + options: {raise}, + mapContinuation: map, + compute: ({'#options': {raise}}, continuation) => + continuation.raiseAbove(raise), }, ]); }, @@ -1549,21 +1519,18 @@ export default class Thing extends CacheableObject { Thing.composite.earlyExitWithoutDependency(data), { - flags: {expose: true, compose: true}, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation.raise({match}); - }, + return continuation.raise({match}); }, }, ]); -- cgit 1.3.0-6-gf8a5 From 6325a70991396412eb8e93cee5f17bdb2859ae9d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 19:52:42 -0300 Subject: data, test: update & test misc. Track reverse reference lists * update & test Track.referencedByTracks * update & test Track.sampledByTracks * update & test Track.featuredInFlashes * update Thing.common.reverseReferenceList * add Thing.composite.withReverseReferenceList * add Track.composite.trackReverseReferenceList --- src/data/things/thing.js | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 15ec62c3..1c99a323 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -330,16 +330,15 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [], - }, - }), + reverseReferenceList({ + data, + refList, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.exposeDependency('#reverseReferenceList'), + ]); + }, // Corresponding function for single references. Note that the return value // is still a list - this is for matching all the objects whose single @@ -1535,5 +1534,29 @@ export default class Thing extends CacheableObject { }, ]); }, + + // Check out the info on Thing.common.reverseReferenceList! + // This is its composable form. + withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, + }) { + return Thing.composite.from(`Thing.common.reverseReferenceList`, [ + Thing.composite.earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); + }, }; } -- cgit 1.3.0-6-gf8a5 From a3b80f08fc54cda6a6787bcd078059823026add6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 31 Aug 2023 20:19:22 -0300 Subject: data: update Thing.composition.from documentation --- src/data/things/thing.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1c99a323..19f5fb53 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -565,6 +565,14 @@ export default class Thing extends CacheableObject { // }, // ]); // + // One last note! A super common code pattern when creating more complex + // compositions is to have several steps which *only* expose and compose. + // As a syntax shortcut, you can skip the outer section. It's basically + // like writing out just the {expose: {...}} part. Remember that this + // indicates that the step you're defining is compositional, so you have + // to specify the flags manually for the base, even if this property isn't + // going to get an {update: true} flag. + // // == Cache-safe dependency names: == // // [Disclosure: The caching engine hasn't actually been implemented yet. @@ -705,7 +713,7 @@ export default class Thing extends CacheableObject { // expanded to the scope of the composition instead of following steps. // // For example, suppose your composition (which you expect to include in - // other compositions) brings about several internal, hash-prefixed + // other compositions) brings about several private, hash-prefixed // dependencies to contribute to its own results. Those dependencies won't // end up "bleeding" into the dependency list of whichever composition is // nesting this one - they will totally disappear once all the steps in @@ -718,6 +726,40 @@ export default class Thing extends CacheableObject { // a hash just like the exports from any other compositional step; they're // still dynamically provided dependencies!) // + // Another way to "export" dependencies is by using calling *any* step's + // `continuation.raise()` function. This is sort of like early exiting, + // but instead of quitting out the whole entire property, it will just + // break out of the current, nested composition's list of steps, acting + // as though the composition had finished naturally. The dependencies + // passed to `raise` will be the ones which get exported. + // + // Since `raise` is another way to export dependencies, if you're using + // dynamic export names, you should specify `mapContinuation` on the step + // calling `continuation.raise` as well. + // + // An important note on `mapDependencies` here: A nested composition gets + // free access to all the ordinary properties defined on the thing it's + // working on, but if you want it to depend on *private* dependencies - + // ones prefixed with '#' - which were provided by some other compositional + // step preceding wherever this one gets nested, then you *have* to use + // `mapDependencies` to gain access. Check out the section on "cache-safe + // dependency names" for information on this syntax! + // + // Also - on rare occasion - you might want to make a reusable composition + // that itself causes the composition *it's* nested in to raise. If that's + // the case, give `composition.raiseAbove()` a go! This effectively means + // kicking out of *two* layers of nested composition - the one including + // the step with the `raiseAbove` call, and the composition which that one + // is nested within. You don't need to use `raiseAbove` if the reusable + // utility function just returns a single compositional step, but if you + // want to make use of other compositional steps, it gives you access to + // the same conditional-raise capabilities. + // + // Have some syntax sugar! Since nested compositions are defined by having + // the base be {compose: true}, the composition will infer as much if you + // don't specifying the base's flags at all. Simply use the same shorthand + // syntax as for other compositional steps, and it'll work out cleanly! + // from(firstArg, secondArg) { const debug = fn => { if (Thing.composite.from.debug === true) { -- cgit 1.3.0-6-gf8a5 From 9d8616ced8f505b499780e859d96f288d67f2154 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 12:13:25 -0300 Subject: data: remove unused Thing.common utilities dynamicInheritContribs is replaced by more specialized behavior on tracks (which are the only thing that inherit contribs this way), and reverseSingleReference, introduced with reverseReferenceList, was never used anywhere. --- src/data/things/thing.js | 68 ------------------------------------------------ 1 file changed, 68 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f5fb53..ad27ca55 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -257,60 +257,6 @@ export default class Thing extends CacheableObject { }, }), - // Dynamically inherit a contribution list from some other object, if it - // hasn't been overridden on this object. This is handy for solo albums - // where all tracks have the same artist, for example. - dynamicInheritContribs: ( - // If this property is explicitly false, the contribution list returned - // will always be empty. - nullerProperty, - - // Property holding contributions on the current object. - contribsByRefProperty, - - // Property holding corresponding "default" contributions on the parent - // object, which will fallen back to if the object doesn't have its own - // contribs. - parentContribsByRefProperty, - - // Data array to search in and "find" function to locate parent object - // (which will be passed the child object and the wiki data array). - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - expose: { - dependencies: [ - 'this', - contribsByRefProperty, - thingDataProperty, - nullerProperty, - 'artistData', - ].filter(Boolean), - - compute({ - this: thing, - [nullerProperty]: nuller, - [contribsByRefProperty]: contribsByRef, - [thingDataProperty]: thingData, - artistData, - }) { - if (!artistData) return []; - if (nuller === false) return []; - const refs = - contribsByRef ?? - findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty]; - if (!refs) return []; - return refs - .map(({who: ref, what}) => ({ - who: find.artist(ref, artistData), - what, - })) - .filter(({who}) => who); - }, - }, - }), - // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. contribsPresent: (contribsByRefProperty) => ({ @@ -340,20 +286,6 @@ export default class Thing extends CacheableObject { ]); }, - // Corresponding function for single references. Note that the return value - // is still a list - this is for matching all the objects whose single - // reference (in the given property) matches this Thing. - reverseSingleReference: (thingDataProperty, referencerRefListProperty) => ({ - flags: {expose: true}, - - expose: { - dependencies: ['this', thingDataProperty], - - compute: ({this: thing, [thingDataProperty]: thingData}) => - thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [], - }, - }), - // General purpose wiki data constructor, for properties like artistData, // trackData, etc. wikiData: (thingClass) => ({ -- cgit 1.3.0-6-gf8a5 From 703f065560e71ec7f750ea8a9dfdff2c71e0fde8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 12:27:50 -0300 Subject: data: move Thing.composite definition into dedicated file --- src/data/things/thing.js | 1176 +--------------------------------------------- 1 file changed, 2 insertions(+), 1174 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ad27ca55..01aa8b1b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; +import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -359,1178 +360,5 @@ export default class Thing extends CacheableObject { .filter(({who}) => who)); } - static composite = { - // Composes multiple compositional "steps" and a "base" to form a property - // descriptor out of modular building blocks. This is an extension to the - // more general-purpose CacheableObject property descriptor syntax, and - // aims to make modular data processing - which lends to declarativity - - // much easier, without fundamentally altering much of the typical syntax - // or terminology, nor building on it to an excessive degree. - // - // Think of a composition as being a chain of steps which lead into a final - // base property, which is usually responsible for returning the value that - // will actually get exposed when the property being described is accessed. - // - // == The compositional base: == - // - // The final item in a compositional list is its base, and it identifies - // the essential qualities of the property descriptor. The compositional - // steps preceding it may exit early, in which case the expose function - // defined on the base won't be called; or they will provide dependencies - // that the base may use to compute the final value that gets exposed for - // this property. - // - // The base indicates the capabilities of the composition as a whole. - // It should be {expose: true}, since that's the only area that preceding - // compositional steps (currently) can actually influence. If it's also - // {update: true}, then the composition as a whole accepts an update value - // just like normal update-flag property descriptors - meaning it can be - // set with `thing.someProperty = value` and that value will be paseed - // into each (implementing) step's transform() function, as well as the - // base. Bases usually aren't {compose: true}, but can be - check out the - // section on "nesting compositions" for details about that. - // - // Every composition always has exactly one compositional base, and it's - // always the last item in the composition list. All items preceding it - // are compositional steps, described below. - // - // == Compositional steps: == - // - // Compositional steps are, in essence, typical property descriptors with - // the extra flag {compose: true}. They operate on existing dependencies, - // and are typically dynamically constructed by "utility" functions (but - // can also be manually declared within the step list of a composition). - // Compositional steps serve two purposes: - // - // 1. exit early, if some condition is matched, returning and exposing - // some value directly from that step instead of continuing further - // down the step list; - // - // 2. and/or provide new, dynamically created "private" dependencies which - // can be accessed by further steps down the list, or at the base at - // the bottom, modularly supplying information that will contribute to - // the final value exposed for this property. - // - // Usually it's just one of those two, but it's fine for a step to perform - // both jobs if the situation benefits. - // - // Compositional steps are the real "modular" or "compositional" part of - // this data processing style - they're designed to be combined together - // in dynamic, versatile ways, as each property demands it. You usually - // define a compositional step to be returned by some ordinary static - // property-descriptor-returning function (customarily namespaced under - // the relevant Thing class's static `composite` field) - that lets you - // reuse it in multiple compositions later on. - // - // Compositional steps are implemented with "continuation passing style", - // meaning the connection to the next link on the chain is passed right to - // each step's compute (or transform) function, and the implementation gets - // to decide whether to continue on that chain or exit early by returning - // some other value. - // - // Every step along the chain, apart from the base at the bottom, has to - // have the {compose: true} step. That means its compute() or transform() - // function will be passed an extra argument at the end, `continuation`. - // To provide new dependencies to items further down the chain, just pass - // them directly to this continuation() function, customarily with a hash - // ('#') prefixing each name - for example: - // - // compute({..some dependencies..}, continuation) { - // return continuation({ - // '#excitingProperty': (..a value made from dependencies..), - // }); - // } - // - // Performing an early exit is as simple as returning some other value, - // instead of the continuation. You may also use `continuation.exit(value)` - // to perform the exact same kind of early exit - it's just a different - // syntax that might fit in better in certain longer compositions. - // - // It may be fine to simply provide new dependencies under a hard-coded - // name, such as '#excitingProperty' above, but if you're writing a utility - // that dynamically returns the compositional step and you suspect you - // might want to use this step multiple times in a single composition, - // it's customary to accept a name for the result. - // - // Here's a detailed example showing off early exit, dynamically operating - // on a provided dependency name, and then providing a result in another - // also-provided dependency name: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: [contribsByRefDependency, 'artistData'], - // compute({ - // [contribsByRefDependency]: contribsByRef, - // artistData, - // }, continuation) { - // if (!artistData) return null; /* early exit! */ - // return continuation({ - // [outputDependency]: /* this is the important part */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // And how you might work that into a composition: - // - // static Track[Thing.getPropertyDescriptors].coverArtists = - // Thing.composite.from([ - // Track.composite.doSomethingWhichMightEarlyExit(), - // Thing.composite.withResolvedContribs({ - // from: 'coverArtistContribsByRef', - // to: '#coverArtistContribs', - // }), - // - // { - // flags: {expose: true}, - // expose: { - // dependencies: ['#coverArtistContribs'], - // compute({'#coverArtistContribs': coverArtistContribs}) { - // return coverArtistContribs.map(({who}) => who); - // }, - // }, - // }, - // ]); - // - // One last note! A super common code pattern when creating more complex - // compositions is to have several steps which *only* expose and compose. - // As a syntax shortcut, you can skip the outer section. It's basically - // like writing out just the {expose: {...}} part. Remember that this - // indicates that the step you're defining is compositional, so you have - // to specify the flags manually for the base, even if this property isn't - // going to get an {update: true} flag. - // - // == Cache-safe dependency names: == - // - // [Disclosure: The caching engine hasn't actually been implemented yet. - // As such, this section is subject to change, and simply provides sound - // forward-facing advice and interfaces.] - // - // It's a good idea to write individual compositional steps in such a way - // that they're "cache-safe" - meaning the same input (dependency) values - // will always result in the same output (continuation or early exit). - // - // In order to facilitate this, compositional step descriptors may specify - // unique `mapDependencies`, `mapContinuation`, and `options` values. - // - // Consider the `withResolvedContribs` example adjusted to make use of - // two of these options below: - // - // static Thing.composite.withResolvedContribs = ({ - // from: contribsByRefDependency, - // to: outputDependency, - // }) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {contribsByRef: contribsByRefDependency}, - // mapContinuation: {outputDependency}, - // compute({ - // contribsByRef, /* no longer in square brackets */ - // artistData, - // }, continuation) { - // if (!artistData) return null; - // return continuation({ - // outputDependency: /* no longer in square brackets */ - // (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // With a little destructuring and restructuring JavaScript sugar, the - // above can be simplified some more: - // - // static Thing.composite.withResolvedContribs = ({from, to}) => ({ - // flags: {expose: true, compose: true}, - // expose: { - // dependencies: ['artistData'], - // mapDependencies: {from}, - // mapContinuation: {to}, - // compute({artistData, from: contribsByRef}, continuation) { - // if (!artistData) return null; - // return continuation({ - // to: (..resolve contributions one way or another..), - // }); - // }, - // }, - // }); - // - // These two properties let you separate the name-mapping behavior (for - // dependencies and the continuation) from the main body of the compute - // function. That means the compute function will *always* get inputs in - // the same form (dependencies 'artistData' and 'from' above), and will - // *always* provide its output in the same form (early return or 'to'). - // - // Thanks to that, this `compute` function is cache-safe! Its outputs can - // be cached corresponding to each set of mapped inputs. So it won't matter - // whether the `from` dependency is named `coverArtistContribsByRef` or - // `contributorContribsByRef` or something else - the compute function - // doesn't care, and only expects that value to be provided via its `from` - // argument. Likewise, it doesn't matter if the output should be sent to - // '#coverArtistContribs` or `#contributorContribs` or some other name; - // the mapping is handled automatically outside, and compute will always - // output its value to the continuation's `to`. - // - // Note that `mapDependencies` and `mapContinuation` should be objects of - // the same "shape" each run - that is, the values will change depending on - // outside context, but the keys are always the same. You shouldn't use - // `mapDependencies` to dynamically select more or fewer dependencies. - // If you need to dynamically select a range of dependencies, just specify - // them in the `dependencies` array like usual. The caching engine will - // understand that differently named `dependencies` indicate separate - // input-output caches should be used. - // - // The 'options' property makes it possible to specify external arguments - // that fundamentally change the behavior of the `compute` function, while - // still remaining cache-safe. It indicates that the caching engine should - // use a completely different input-to-output cache for each permutation - // of the 'options' values. This way, those functions are still cacheable - // at all; they'll just be cached separately for each set of option values. - // Values on the 'options' property will always be provided in compute's - // dependencies under '#options' (to avoid name conflicts with other - // dependencies). - // - // == To compute or to transform: == - // - // A compositional step can work directly on a property's stored update - // value, transforming it in place and either early exiting with it or - // passing it on (via continuation) to the next item(s) in the - // compositional step list. (If needed, these can provide dependencies - // the same way as compute functions too - just pass that object after - // the updated (or same) transform value in your call to continuation().) - // - // But in order to make them more versatile, compositional steps have an - // extra trick up their sleeve. If a compositional step implements compute - // and *not* transform, it can still be used in a composition targeting a - // property which updates! These retain their full dependency-providing and - // early exit functionality - they just won't be provided the update value. - // If a compute-implementing step returns its continuation, then whichever - // later step (or the base) next implements transform() will receive the - // update value that had so far been running - as well as any dependencies - // the compute() step returned, of course! - // - // Please note that a compositional step which transforms *should not* - // specify, in its flags, {update: true}. Just provide the transform() - // function in its expose descriptor; it will be automatically detected - // and used when appropriate. - // - // It's actually possible for a step to specify both transform and compute, - // in which case the transform() implementation will only be selected if - // the composition's base is {update: true}. It's not exactly known why you - // would want to specify unique-but-related transform and compute behavior, - // but the basic possibility was too cool to skip out on. - // - // == Nesting compositions: == - // - // Compositional steps are so convenient that you just might want to bundle - // them together, and form a whole new step-shaped unit of its own! - // - // In order to allow for this while helping to ensure internal dependencies - // remain neatly isolated from the composition which nests your bundle, - // the Thing.composite.from() function will accept and adapt to a base that - // specifies the {compose: true} flag, just like the steps preceding it. - // - // The continuation function that gets provided to the base will be mildly - // special - after all, nothing follows the base within the composition's - // own list! Instead of appending dependencies alongside any previously - // provided ones to be available to the next step, the base's continuation - // function should be used to define "exports" of the composition as a - // whole. It's similar to the usual behavior of the continuation, just - // expanded to the scope of the composition instead of following steps. - // - // For example, suppose your composition (which you expect to include in - // other compositions) brings about several private, hash-prefixed - // dependencies to contribute to its own results. Those dependencies won't - // end up "bleeding" into the dependency list of whichever composition is - // nesting this one - they will totally disappear once all the steps in - // the nested composition have finished up. - // - // To "export" the results of processing all those dependencies (provided - // that's something you want to do and this composition isn't used purely - // for a conditional early-exit), you'll want to define them in the - // continuation passed to the base. (Customarily, those should start with - // a hash just like the exports from any other compositional step; they're - // still dynamically provided dependencies!) - // - // Another way to "export" dependencies is by using calling *any* step's - // `continuation.raise()` function. This is sort of like early exiting, - // but instead of quitting out the whole entire property, it will just - // break out of the current, nested composition's list of steps, acting - // as though the composition had finished naturally. The dependencies - // passed to `raise` will be the ones which get exported. - // - // Since `raise` is another way to export dependencies, if you're using - // dynamic export names, you should specify `mapContinuation` on the step - // calling `continuation.raise` as well. - // - // An important note on `mapDependencies` here: A nested composition gets - // free access to all the ordinary properties defined on the thing it's - // working on, but if you want it to depend on *private* dependencies - - // ones prefixed with '#' - which were provided by some other compositional - // step preceding wherever this one gets nested, then you *have* to use - // `mapDependencies` to gain access. Check out the section on "cache-safe - // dependency names" for information on this syntax! - // - // Also - on rare occasion - you might want to make a reusable composition - // that itself causes the composition *it's* nested in to raise. If that's - // the case, give `composition.raiseAbove()` a go! This effectively means - // kicking out of *two* layers of nested composition - the one including - // the step with the `raiseAbove` call, and the composition which that one - // is nested within. You don't need to use `raiseAbove` if the reusable - // utility function just returns a single compositional step, but if you - // want to make use of other compositional steps, it gives you access to - // the same conditional-raise capabilities. - // - // Have some syntax sugar! Since nested compositions are defined by having - // the base be {compose: true}, the composition will infer as much if you - // don't specifying the base's flags at all. Simply use the same shorthand - // syntax as for other compositional steps, and it'll work out cleanly! - // - from(firstArg, secondArg) { - const debug = fn => { - if (Thing.composite.from.debug === true) { - const label = - (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); - const result = fn(); - if (Array.isArray(result)) { - console.log(label, ...result.map(value => - (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) - : value))); - } else { - console.log(label, result); - } - } - }; - - let annotation, composition; - if (typeof firstArg === 'string') { - [annotation, composition] = [firstArg, secondArg]; - } else { - [annotation, composition] = [null, firstArg]; - } - - const base = composition.at(-1); - const steps = composition.slice(); - - const aggregate = openAggregate({ - message: - `Errors preparing Thing.composite.from() composition` + - (annotation ? ` (${annotation})` : ''), - }); - - const baseExposes = - (base.flags - ? base.flags.expose - : true); - - const baseUpdates = - (base.flags - ? base.flags.update - : false); - - const baseComposes = - (base.flags - ? base.flags.compose - : true); - - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } - - const exposeDependencies = new Set(); - - let anyStepsCompute = false; - let anyStepsTransform = false; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - const message = - `Errors in step #${i + 1}` + - (isBase ? ` (base)` : ``) + - (step.annotation ? ` (${step.annotation})` : ``); - - aggregate.nest({message}, ({push}) => { - if (step.flags) { - let flagsErrored = false; - - if (!step.flags.compose && !isBase) { - push(new TypeError(`All steps but base must compose`)); - flagsErrored = true; - } - - if (!step.flags.expose) { - push(new TypeError(`All steps must expose`)); - flagsErrored = true; - } - - if (flagsErrored) { - return; - } - } - - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose.compute; - const stepTransforms = !!expose.transform; - - if (!stepComputes && !stepTransforms) { - push(new TypeError(`Steps must provide compute or transform (or both)`)); - return; - } - - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - return; - } - - if (stepComputes) { - anyStepsCompute = true; - } - - if (stepTransforms) { - anyStepsTransform = true; - } - - // Unmapped dependencies are exposed on the final composition only if - // they're "public", i.e. pointing to update values of other properties - // on the CacheableObject. - for (const dependency of expose.dependencies ?? []) { - if (typeof dependency === 'string' && dependency.startsWith('#')) { - continue; - } - - exposeDependencies.add(dependency); - } - - // Mapped dependencies are always exposed on the final composition. - // These are explicitly for reading values which are named outside of - // the current compositional step. - for (const dependency of Object.values(expose.mapDependencies ?? {})) { - exposeDependencies.add(dependency); - } - }); - } - - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); - } - } - } - - aggregate.close(); - - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } - - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; - - if (baseUpdates) { - constructedDescriptor.update = base.update; - } - - if (baseExposes) { - const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); - - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - - function _prepareContinuation(callingTransformForThisStep) { - const continuationStorage = { - returnedWith: null, - providedDependencies: undefined, - providedValue: undefined, - }; - - const continuation = - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.exit = (providedValue) => { - continuationStorage.returnedWith = 'exit'; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - }; - - if (baseComposes) { - const makeRaiseLike = returnWith => - (callingTransformForThisStep - ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - continuationStorage.providedValue = providedValue; - return continuationSymbol; - } - : (providedDependencies = null) => { - continuationStorage.returnedWith = returnWith; - continuationStorage.providedDependencies = providedDependencies; - return continuationSymbol; - }); - - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); - } - - return {continuation, continuationStorage}; - } - - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; - - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); - - const availableDependencies = {...initialDependencies}; - - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; - - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); - - const expose = - (step.flags - ? step.expose - : step); - - const callingTransformForThisStep = - expectingTransform && expose.transform; - - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); - - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); - - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } - - debug(() => color.bright(`end composition - exit (inferred)`)); - - return result; - } - - const {returnedWith} = continuationStorage; - - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; - - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); - - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } - } - - const {providedValue, providedDependencies} = continuationStorage; - - const continuingWithValue = - (expectingTransform - ? (callingTransformForThisStep - ? providedValue ?? null - : valueSoFar ?? null) - : undefined); - - const continuingWithDependencies = - (providedDependencies - ? _assignDependencies(providedDependencies, expose) - : null); - - const continuationArgs = []; - if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); - if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - - debug(() => { - const base = `step #${i+1} - result: ` + returnedWith; - const parts = []; - - if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } - } - - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } - - if (empty(parts)) { - return base; - } else { - return [base + ' ->', ...parts]; - } - }); - - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); - - case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); - - case 'continuation': - if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); - return continuationIfApplicable(...continuationArgs); - } else { - Object.assign(availableDependencies, continuingWithDependencies); - break; - } - } - } - } - - const transformFn = - (value, initialDependencies, continuationIfApplicable) => - _computeOrTransform(value, initialDependencies, continuationIfApplicable); - - const computeFn = - (initialDependencies, continuationIfApplicable) => - _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - - if (baseComposes) { - if (anyStepsTransform) expose.transform = transformFn; - if (anyStepsCompute) expose.compute = computeFn; - } else if (baseUpdates) { - expose.transform = transformFn; - } else { - expose.compute = computeFn; - } - } - - return constructedDescriptor; - }, - - // Evaluates a function with composite debugging enabled, turns debugging - // off again, and returns the result of the function. This is mostly syntax - // sugar, but also helps avoid unit tests avoid accidentally printing debug - // info for a bunch of unrelated composites (due to property enumeration - // when displaying an unexpected result). Use as so: - // - // Without debugging: - // t.same(thing.someProp, value) - // - // With debugging: - // t.same(Thing.composite.debug(() => thing.someProp), value) - // - debug(fn) { - Thing.composite.from.debug = true; - const value = fn(); - Thing.composite.from.debug = false; - return value; - }, - - // -- Compositional steps for compositions to nest -- - - // Provides dependencies exactly as they are (or null if not defined) to the - // continuation. Although this can *technically* be used to alias existing - // dependencies to some other name within the middle of a composition, it's - // intended to be used only as a composition's base - doing so makes the - // composition as a whole suitable as a step in some other composition, - // providing the listed (internal) dependencies to later steps just like - // other compositional steps. - export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `Thing.composite.export`, - flags: {expose: true, compose: true}, - - expose: { - options: {mappingEntries}, - dependencies: Object.values(mapping), - - compute({'#options': {mappingEntries}, ...dependencies}, continuation) { - const exports = {}; - - // Note: This is slightly different behavior from filterProperties, - // as defined in sugar.js, which doesn't fall back to null for - // properties which don't exist on the original object. - for (const [exportKey, dependencyKey] of mappingEntries) { - exports[exportKey] = - (Object.hasOwn(dependencies, dependencyKey) - ? dependencies[dependencyKey] - : null); - } - - return continuation.raise(exports); - } - }, - }; - }, - - // -- Compositional steps for top-level property descriptors -- - - // Exposes a dependency exactly as it is; this is typically the base of a - // composition which was created to serve as one property's descriptor. - // Since this serves as a base, specify a value for {update} to indicate - // that the property as a whole updates (and some previous compositional - // step works with that update value). Set {update: true} to only enable - // the update flag, or set update to an object to specify a descriptor - // (e.g. for custom value validation). - // - // Please note that this *doesn't* verify that the dependency exists, so - // if you provide the wrong name or it hasn't been set by a previous - // compositional step, the property will be exposed as undefined instead - // of null. - // - exposeDependency(dependency, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeDependency`, - flags: {expose: true, update: !!update}, - - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Exposes a constant value exactly as it is; like exposeDependency, this - // is typically the base of a composition serving as a particular property - // descriptor. It generally follows steps which will conditionally early - // exit with some other value, with the exposeConstant base serving as the - // fallback default value. Like exposeDependency, set {update} to true or - // an object to indicate that the property as a whole updates. - exposeConstant(value, { - update = false, - } = {}) { - return { - annotation: `Thing.composite.exposeConstant`, - flags: {expose: true, update: !!update}, - - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, - }, - - update: - (typeof update === 'object' - ? update - : null), - }; - }, - - // Checks the availability of a dependency or the update value and provides - // the result to later steps under '#availability' (by default). This is - // mainly intended for use by the more specific utilities, which you should - // consider using instead. Customize {mode} to select one of these modes, - // or leave unset and default to 'null': - // - // * 'null': Check that the value isn't null. - // * 'empty': Check that the value is neither null nor an empty array. - // * 'falsy': Check that the value isn't false when treated as a boolean - // (nor an empty array). Keep in mind this will also be false - // for values like zero and the empty string! - // - withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - mode = 'null', - to = '#availability', - }) { - if (!['null', 'empty', 'falsy'].includes(mode)) { - throw new TypeError(`Expected mode to be null, empty, or falsy`); - } - - if (fromUpdateValue && fromDependency) { - throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`); - } - - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } - - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; - - if (fromDependency) { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {to}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {to}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), - }, - }; - } - }, - - // Exposes a dependency as it is, or continues if it's unavailable. - // See withResultOfAvailabilityCheck for {mode} options! - exposeDependencyOrContinue(dependency, { - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); - }, - - // Exposes the update value of an {update: true} property as it is, - // or continues if it's unavailable. See withResultOfAvailabilityCheck - // for {mode} options! - exposeUpdateValueOrContinue({ - mode = 'null', - } = {}) { - return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [ - Thing.composite.withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, - }), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), - }, - - { - transform: (value, {}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if an availability check fails. - // This is for internal use only - use `earlyExitWithoutDependency` or - // `earlyExitWIthoutUpdateValue` instead. - earlyExitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, - }) { - return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); - }, - - // Early exits if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutDependency(dependency, { - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Early exits if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - earlyExitWithoutUpdateValue({ - mode = 'null', - value = null, - } = {}) { - return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - Thing.composite.earlyExitIfAvailabilityCheckFailed({value}), - ]); - }, - - // Raises if a dependency isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutDependency(dependency, { - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [ - Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // Raises if this property's update value isn't available. - // See withResultOfAvailabilityCheck for {mode} options! - raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, - } = {}) { - return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [ - Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, - - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); - }, - - // -- Compositional steps for processing data -- - - // Resolves the contribsByRef contained in the provided dependency, - // providing (named by the second argument) the result. "Resolving" - // means mapping the "who" reference of each contribution to an artist - // object, and filtering out those whose "who" doesn't match any artist. - withResolvedContribs({from, to}) { - return { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, - - expose: { - dependencies: ['artistData'], - mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => - continuation({ - to: Thing.findArtistsFromContribs(from, artistData), - }), - }, - }; - }, - - // Resolves a reference by using the provided find function to match it - // within the provided thingData dependency. This will early exit if the - // data dependency is null, or, if earlyExitIfNotFound is set to true, - // if the find function doesn't match anything for the reference. - // Otherwise, the data object is provided on the output dependency; - // or null, if the reference doesn't match anything or itself was null - // to begin with. - withResolvedReference({ - ref, - data, - to, - find: findFunction, - earlyExitIfNotFound = false, - }) { - return Thing.composite.from(`Thing.composite.withResolvedReference`, [ - Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - Thing.composite.earlyExitWithoutDependency(data), - - { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {match: to}, - - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && earlyExitIfNotFound) { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, - }, - ]); - }, - - // Check out the info on Thing.common.reverseReferenceList! - // This is its composable form. - withReverseReferenceList({ - data, - to = '#reverseReferenceList', - refList: refListProperty, - }) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {to}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ]); - }, - }; + static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 6f242fc864028a12321255ba04a88c6190801510 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 14:34:12 -0300 Subject: data: isolate withResolvedContribs internal behavior --- src/data/things/thing.js | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 01aa8b1b..9bfed080 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -249,14 +249,16 @@ export default class Thing extends CacheableObject { // filtered out. (So if the list is all empty, chances are that either the // reference list is somehow messed up, or artistData isn't being provided // properly.) - dynamicContribs: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: ['artistData', contribsByRefProperty], - compute: ({artistData, [contribsByRefProperty]: contribsByRef}) => - Thing.findArtistsFromContribs(contribsByRef, artistData), - }, - }), + dynamicContribs(contribsByRefProperty) { + return Thing.composite.from(`Thing.common.dynamicContribs`, [ + Thing.composite.withResolvedContribs({ + from: contribsByRefProperty, + to: '#contribs', + }), + + Thing.composite.exposeDependency('#contribs'), + ]); + }, // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. @@ -348,17 +350,5 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - static findArtistsFromContribs(contribsByRef, artistData) { - if (empty(contribsByRef)) return null; - - return ( - contribsByRef - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who)); - } - static composite = composite; } -- cgit 1.3.0-6-gf8a5 From 137bd813980d77441a86303ac6c04b61d9ccb8da Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:14:37 -0300 Subject: data: isolate internals of dynamicThingsFromReferenceList --- src/data/things/thing.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9bfed080..9f77c3fc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -194,26 +194,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. dynamicThingsFromReferenceList( - refs, + refList, data, findFunction ) { return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ - Thing.composite.earlyExitWithoutDependency(refs, {value: []}), - Thing.composite.earlyExitWithoutDependency(data, {value: []}), - - { - flags: {expose: true}, - expose: { - mapDependencies: {refs, data}, - options: {findFunction}, - - compute: ({refs, data, '#options': {findFunction}}) => - refs - .map(ref => findFunction(ref, data, {mode: 'quiet'})) - .filter(Boolean), - }, - }, + Thing.composite.withResolvedReferenceList({ + refList, + data, + to: '#things', + find: findFunction, + notFoundMode: 'filter', + }), + + Thing.composite.exposeDependency('#things'), ]); }, -- cgit 1.3.0-6-gf8a5 From 2d7c536ee91a8f5bf8f16db1fc2d0a4d8bb4fc85 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:22:58 -0300 Subject: data: dynamicThingsFromReferenceList -> resolvedReferenceList --- src/data/things/thing.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9f77c3fc..f36b08bc 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,14 +193,14 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - dynamicThingsFromReferenceList( - refList, + resolvedReferenceList({ + list, data, - findFunction - ) { - return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [ + find: findFunction, + }) { + return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - refList, + list, data, to: '#things', find: findFunction, -- cgit 1.3.0-6-gf8a5 From 007c70642a60ed83bd840f550aa06563d4ba6a99 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:31:41 -0300 Subject: data: reverseReferenceList refList -> list --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f36b08bc..915474d4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -275,10 +275,10 @@ export default class Thing extends CacheableObject { // wiki data provided, not the requesting Thing itself. reverseReferenceList({ data, - refList, + list, }) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, refList}), + Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), ]); }, -- cgit 1.3.0-6-gf8a5 From 2437ac322a4c44f2fd9f6a77ac7a65bbb3afc2c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:42:47 -0300 Subject: data: dynamicThingFromSingleReference -> resolvedReference --- src/data/things/thing.js | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 915474d4..36a1f58a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -193,40 +193,23 @@ export default class Thing extends CacheableObject { // Corresponding dynamic property to referenceList, which takes the values // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. - resolvedReferenceList({ - list, - data, - find: findFunction, - }) { + resolvedReferenceList({list, data, find}) { return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ Thing.composite.withResolvedReferenceList({ - list, - data, - to: '#things', - find: findFunction, + list, data, find, notFoundMode: 'filter', }), - - Thing.composite.exposeDependency('#things'), + Thing.composite.exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. - dynamicThingFromSingleReference: ( - singleReferenceProperty, - thingDataProperty, - findFn - ) => ({ - flags: {expose: true}, - - expose: { - dependencies: [singleReferenceProperty, thingDataProperty], - compute: ({ - [singleReferenceProperty]: ref, - [thingDataProperty]: thingData, - }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null), - }, - }), + resolvedReference({ref, data, find}) { + return Thing.composite.from(`Thing.common.resolvedReference`, [ + Thing.composite.withResolvedReference({ref, data, find}), + Thing.composite.exposeDependency('#resolvedReference'), + ]); + }, // Corresponding dynamic property to contribsByRef, which takes the values // in the provided property and searches the object's artistData for @@ -273,10 +256,7 @@ export default class Thing extends CacheableObject { // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. - reverseReferenceList({ - data, - list, - }) { + reverseReferenceList({data, list}) { return Thing.composite.from(`Thing.common.reverseReferenceList`, [ Thing.composite.withReverseReferenceList({data, list}), Thing.composite.exposeDependency('#reverseReferenceList'), -- cgit 1.3.0-6-gf8a5 From c6ba294c4fef425074f2352b640cc02c4768ee6e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:49:56 -0300 Subject: data: unused import fixes --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 36a1f58a..968dd102 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; +import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; import { -- cgit 1.3.0-6-gf8a5 From 78d293d5f4eea7ed6ee6f3cddd3ffcf73c5056a0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 09:40:55 -0300 Subject: data: directly import from #composite; define own utils at module --- src/data/things/thing.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 968dd102..0716931a 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -8,6 +8,15 @@ import find from '#find'; import {empty} from '#sugar'; import {getKebabCase} from '#wiki-data'; +import { + from as compositeFrom, + exposeDependency, + withReverseReferenceList, + withResolvedContribs, + withResolvedReference, + withResolvedReferenceList, +} from '#composite'; + import { isAdditionalFileList, isBoolean, @@ -27,7 +36,6 @@ import { } from '#validators'; import CacheableObject from './cacheable-object.js'; -import * as composite from './composite.js'; export default class Thing extends CacheableObject { static referenceType = Symbol('Thing.referenceType'); @@ -194,20 +202,20 @@ export default class Thing extends CacheableObject { // in the provided property and searches the specified wiki data for // matching actual Thing-subclass objects. resolvedReferenceList({list, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReferenceList`, [ - Thing.composite.withResolvedReferenceList({ + return compositeFrom(`Thing.common.resolvedReferenceList`, [ + withResolvedReferenceList({ list, data, find, notFoundMode: 'filter', }), - Thing.composite.exposeDependency('#resolvedReferenceList'), + exposeDependency('#resolvedReferenceList'), ]); }, // Corresponding function for a single reference. resolvedReference({ref, data, find}) { - return Thing.composite.from(`Thing.common.resolvedReference`, [ - Thing.composite.withResolvedReference({ref, data, find}), - Thing.composite.exposeDependency('#resolvedReference'), + return compositeFrom(`Thing.common.resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency('#resolvedReference'), ]); }, @@ -227,13 +235,13 @@ export default class Thing extends CacheableObject { // reference list is somehow messed up, or artistData isn't being provided // properly.) dynamicContribs(contribsByRefProperty) { - return Thing.composite.from(`Thing.common.dynamicContribs`, [ - Thing.composite.withResolvedContribs({ + return compositeFrom(`Thing.common.dynamicContribs`, [ + withResolvedContribs({ from: contribsByRefProperty, to: '#contribs', }), - Thing.composite.exposeDependency('#contribs'), + exposeDependency('#contribs'), ]); }, @@ -257,9 +265,9 @@ export default class Thing extends CacheableObject { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. reverseReferenceList({data, list}) { - return Thing.composite.from(`Thing.common.reverseReferenceList`, [ - Thing.composite.withReverseReferenceList({data, list}), - Thing.composite.exposeDependency('#reverseReferenceList'), + return compositeFrom(`Thing.common.reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency('#reverseReferenceList'), ]); }, @@ -323,6 +331,4 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } - - static composite = composite; } -- cgit 1.3.0-6-gf8a5 From c86de8a2be3867c14ca92c8e6799fd9b325305ec Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 09:55:29 -0300 Subject: data: move composite utilities related to wiki data into thing.js --- src/data/things/thing.js | 166 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 6 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0716931a..1077a652 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,16 +5,14 @@ import {inspect} from 'node:util'; import {color} from '#cli'; import find from '#find'; -import {empty} from '#sugar'; -import {getKebabCase} from '#wiki-data'; +import {empty, stitchArrays} from '#sugar'; +import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, + earlyExitWithoutDependency, exposeDependency, - withReverseReferenceList, - withResolvedContribs, - withResolvedReference, - withResolvedReferenceList, + raiseWithoutDependency, } from '#composite'; import { @@ -332,3 +330,159 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } + +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. +export function withResolvedContribs({from, to}) { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { + mapDependencies: {from}, + compute: ({from}, continuation) => + continuation({ + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), + }), + }, + + withResolvedReferenceList({ + list: '#whoByRef', + data: 'artistData', + to: '#who', + find: find.artist, + notFoundMode: 'null', + }), + + { + dependencies: ['#who', '#what'], + mapContinuation: {to}, + compute({'#who': who, '#what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + to: stitchArrays({who, what}), + }); + }, + }, + ]); +} + +// Resolves a reference by using the provided find function to match it +// within the provided thingData dependency. This will early exit if the +// data dependency is null, or, if earlyExitIfNotFound is set to true, +// if the find function doesn't match anything for the reference. +// Otherwise, the data object is provided on the output dependency; +// or null, if the reference doesn't match anything or itself was null +// to begin with. +export function withResolvedReference({ + ref, + data, + find: findFunction, + to = '#resolvedReference', + earlyExitIfNotFound = false, +}) { + return compositeFrom(`withResolvedReference`, [ + raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + earlyExitWithoutDependency(data), + + { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, + + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, + }, + ]); +} + +// Resolves a list of references, with each reference matched with provided +// data in the same way as withResolvedReference. This will early exit if the +// data dependency is null (even if the reference list is empty). By default +// it will filter out references which don't match, but this can be changed +// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +export function withResolvedReferenceList({ + list, + data, + find: findFunction, + to = '#resolvedReferenceList', + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(list, { + map: {to}, + raise: {to: []}, + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {list, data}, + mapContinuation: {matches: to}, + + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { + let matches = + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + + if (!matches.includes(null)) { + return continuation.raise({matches}); + } + + switch (notFoundMode) { + case 'filter': + matches = matches.filter(value => value !== null); + return continuation.raise({matches}); + + case 'exit': + return continuation.exit([]); + + case 'null': + return continuation.raise({matches}); + } + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + list: refListProperty, + to = '#reverseReferenceList', +}) { + return compositeFrom(`Thing.common.reverseReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + { + dependencies: ['this'], + mapDependencies: {data}, + mapContinuation: {to}, + options: {refListProperty}, + + compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => + continuation({ + to: data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ]); +} -- cgit 1.3.0-6-gf8a5 From 3437936e6127192d30a308b68731cd4aa33555e7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:10:44 -0300 Subject: data: earlyExit -> exit in misc. utility names --- src/data/things/thing.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1077a652..5d407153 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -10,7 +10,7 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { from as compositeFrom, - earlyExitWithoutDependency, + exitWithoutDependency, exposeDependency, raiseWithoutDependency, } from '#composite'; @@ -389,7 +389,7 @@ export function withResolvedReference({ }) { return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), - earlyExitWithoutDependency(data), + exitWithoutDependency(data), { options: {findFunction, earlyExitIfNotFound}, @@ -426,8 +426,7 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), - + exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { map: {to}, raise: {to: []}, @@ -471,7 +470,7 @@ export function withReverseReferenceList({ to = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - earlyExitWithoutDependency(data, {value: []}), + exitWithoutDependency(data, {value: []}), { dependencies: ['this'], -- cgit 1.3.0-6-gf8a5 From b076c87e435bbe2403122158ee03e4934c220c6c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:32:41 -0300 Subject: data: earlyExitIfNotFound -> notFoundMode --- src/data/things/thing.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5d407153..93f19799 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -375,31 +375,34 @@ export function withResolvedContribs({from, to}) { // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if earlyExitIfNotFound is set to true, -// if the find function doesn't match anything for the reference. -// Otherwise, the data object is provided on the output dependency; -// or null, if the reference doesn't match anything or itself was null -// to begin with. +// data dependency is null, or, if notFoundMode is set to 'exit', if the find +// function doesn't match anything for the reference. Otherwise, the data +// object is provided on the output dependency; or null, if the reference +// doesn't match anything or itself was null to begin with. export function withResolvedReference({ ref, data, find: findFunction, to = '#resolvedReference', - earlyExitIfNotFound = false, + notFoundMode = 'null', }) { + if (!['exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be exit or null`); + } + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), exitWithoutDependency(data), { - options: {findFunction, earlyExitIfNotFound}, + options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, mapContinuation: {match: to}, - compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) { + if (match === null && notFoundMode === 'exit') { return continuation.exit(null); } -- cgit 1.3.0-6-gf8a5 From fcdc788a3b9efe308518ccdce89f8db0dd5618f6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:36:32 -0300 Subject: data: composite docs update --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 93f19799..9b564ee9 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,7 +9,7 @@ import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { - from as compositeFrom, + compositeFrom, exitWithoutDependency, exposeDependency, raiseWithoutDependency, -- cgit 1.3.0-6-gf8a5 From 4541b2aa65a2f5ccfb7f9a13d5605311fd8ef801 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 11:37:58 -0300 Subject: data: composite "to" -> "into" --- src/data/things/thing.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9b564ee9..16003b00 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -236,7 +236,7 @@ export default class Thing extends CacheableObject { return compositeFrom(`Thing.common.dynamicContribs`, [ withResolvedContribs({ from: contribsByRefProperty, - to: '#contribs', + into: '#contribs', }), exposeDependency('#contribs'), @@ -335,12 +335,12 @@ export default class Thing extends CacheableObject { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, to}) { +export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency(from, { mode: 'empty', - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, }), { @@ -355,18 +355,18 @@ export function withResolvedContribs({from, to}) { withResolvedReferenceList({ list: '#whoByRef', data: 'artistData', - to: '#who', + into: '#who', find: find.artist, notFoundMode: 'null', }), { dependencies: ['#who', '#what'], - mapContinuation: {to}, + mapContinuation: {into}, compute({'#who': who, '#what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - to: stitchArrays({who, what}), + into: stitchArrays({who, what}), }); }, }, @@ -383,7 +383,7 @@ export function withResolvedReference({ ref, data, find: findFunction, - to = '#resolvedReference', + into = '#resolvedReference', notFoundMode = 'null', }) { if (!['exit', 'null'].includes(notFoundMode)) { @@ -391,13 +391,13 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), + raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), exitWithoutDependency(data), { options: {findFunction, notFoundMode}, mapDependencies: {ref, data}, - mapContinuation: {match: to}, + mapContinuation: {match: into}, compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { const match = findFunction(ref, data, {mode: 'quiet'}); @@ -421,7 +421,7 @@ export function withResolvedReferenceList({ list, data, find: findFunction, - to = '#resolvedReferenceList', + into = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { @@ -431,15 +431,15 @@ export function withResolvedReferenceList({ return compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { - map: {to}, - raise: {to: []}, + map: {into}, + raise: {into: []}, mode: 'empty', }), { options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: to}, + mapContinuation: {matches: into}, compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = @@ -470,7 +470,7 @@ export function withResolvedReferenceList({ export function withReverseReferenceList({ data, list: refListProperty, - to = '#reverseReferenceList', + into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ exitWithoutDependency(data, {value: []}), @@ -478,12 +478,12 @@ export function withReverseReferenceList({ { dependencies: ['this'], mapDependencies: {data}, - mapContinuation: {to}, + mapContinuation: {into}, options: {refListProperty}, compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => continuation({ - to: data.filter(thing => thing[refListProperty].includes(thisThing)), + into: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, ]); -- cgit 1.3.0-6-gf8a5 From 8b379954c9d74f0d47ac32ef395627353940c728 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 11:56:05 -0300 Subject: data: use key/value-style for all compositional utility args --- src/data/things/thing.js | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 16003b00..98dec3c3 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -205,7 +205,8 @@ export default class Thing extends CacheableObject { list, data, find, notFoundMode: 'filter', }), - exposeDependency('#resolvedReferenceList'), + + exposeDependency({dependency: '#resolvedReferenceList'}), ]); }, @@ -213,7 +214,7 @@ export default class Thing extends CacheableObject { resolvedReference({ref, data, find}) { return compositeFrom(`Thing.common.resolvedReference`, [ withResolvedReference({ref, data, find}), - exposeDependency('#resolvedReference'), + exposeDependency({dependency: '#resolvedReference'}), ]); }, @@ -239,7 +240,7 @@ export default class Thing extends CacheableObject { into: '#contribs', }), - exposeDependency('#contribs'), + exposeDependency({dependency: '#contribs'}), ]); }, @@ -265,7 +266,7 @@ export default class Thing extends CacheableObject { reverseReferenceList({data, list}) { return compositeFrom(`Thing.common.reverseReferenceList`, [ withReverseReferenceList({data, list}), - exposeDependency('#reverseReferenceList'), + exposeDependency({dependency: '#reverseReferenceList'}), ]); }, @@ -337,7 +338,8 @@ export default class Thing extends CacheableObject { // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, into}) { return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency(from, { + raiseWithoutDependency({ + dependency: from, mode: 'empty', map: {into}, raise: {into: []}, @@ -391,8 +393,15 @@ export function withResolvedReference({ } return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency(ref, {map: {into}, raise: {into: null}}), - exitWithoutDependency(data), + raiseWithoutDependency({ + dependency: ref, + map: {into}, + raise: {into: null}, + }), + + exitWithoutDependency({ + dependency: data, + }), { options: {findFunction, notFoundMode}, @@ -429,11 +438,16 @@ export function withResolvedReferenceList({ } return compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency(data, {value: []}), - raiseWithoutDependency(list, { + exitWithoutDependency({ + dependency: data, + value: [], + }), + + raiseWithoutDependency({ + dependency: list, + mode: 'empty', map: {into}, raise: {into: []}, - mode: 'empty', }), { @@ -473,7 +487,10 @@ export function withReverseReferenceList({ into = '#reverseReferenceList', }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ - exitWithoutDependency(data, {value: []}), + exitWithoutDependency({ + dependency: data, + value: [], + }), { dependencies: ['this'], -- cgit 1.3.0-6-gf8a5 From eb00f2993a1aaaba171ad6c918656552f80bb748 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 12:38:34 -0300 Subject: data: import Thing.common utilities directly Also rename 'color' (from #cli) to 'colors'. --- src/data/things/thing.js | 554 +++++++++++++++++++++++++---------------------- 1 file changed, 293 insertions(+), 261 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 98dec3c3..19f00b3e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -3,7 +3,7 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; @@ -41,297 +41,329 @@ export default class Thing extends CacheableObject { static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); - // Regularly reused property descriptors, for ease of access and generally - // duplicating less code across wiki data types. These are specialized utility - // functions, so check each for how its own arguments behave! - static common = { - name: (defaultName) => ({ - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }), + // Default custom inspect function, which may be overridden by Thing + // subclasses. This will be used when displaying aggregate errors and other + // command-line logging - it's the place to provide information useful in + // identifying the Thing being presented. + [inspect.custom]() { + const cname = this.constructor.name; - color: () => ({ - flags: {update: true, expose: true}, - update: {validate: isColor}, - }), + return ( + (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) + + (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '') + ); + } - directory: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }), + static getReference(thing) { + if (!thing.constructor[Thing.referenceType]) { + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + } - urls: () => ({ - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }), + if (!thing.directory) { + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + } - // A file extension! Or the default, if provided when calling this. - fileExtension: (defaultFileExtension = null) => ({ - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }), + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; + } +} + +// Property descriptor templates +// +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! + +export function name(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} + +export function color() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} - // Straightforward flag descriptor for a variety of property purposes. - // Provide a default value, true or false! - flag: (defaultValue = false) => { - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; +export function directory() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, }, + }; +} - // General date type, used as the descriptor for a bunch of properties. - // This isn't dynamic though - it won't inherit from a date stored on - // another object, for example. - simpleDate: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDate}, - }), +export function urls() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: (value) => value ?? []}, + }; +} - // General string type. This should probably generally be avoided in favor - // of more specific validation, but using it makes it easy to find where we - // might want to improve later, and it's a useful shorthand meanwhile. - simpleString: () => ({ - flags: {update: true, expose: true}, - update: {validate: isString}, - }), +// A file extension! Or the default, if provided when calling this. +export function fileExtension(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} - // External function. These should only be used as dependencies for other - // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }), +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! +export function flag(defaultValue = false) { + // TODO: ^ Are you actually kidding me + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } - // Super simple "contributions by reference" list, used for a variety of - // properties (Artists, Cover Artists, etc). This is the property which is - // externally provided, in the form: - // - // [ - // {who: 'Artist Name', what: 'Viola'}, - // {who: 'artist:john-cena', what: null}, - // ... - // ] - // - // ...processed from YAML, spreadsheet, or any other kind of input. - contribsByRef: () => ({ - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }), + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} - // Artist commentary! Generally present on tracks and albums. - commentary: () => ({ - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }), +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. +export function simpleDate() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} - // This is a somewhat more involved data structure - it's for additional - // or "bonus" files associated with albums or tracks (or anything else). - // It's got this form: - // - // [ - // {title: 'Booklet', files: ['Booklet.pdf']}, - // { - // title: 'Wallpaper', - // description: 'Cool Wallpaper!', - // files: ['1440x900.png', '1920x1080.png'] - // }, - // {title: 'Alternate Covers', description: null, files: [...]}, - // ... - // ] - // - additionalFiles: () => ({ - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }), +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. +export function simpleString() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} - // A reference list! Keep in mind this is for general references to wiki - // objects of (usually) other Thing subclasses, not specifically leitmotif - // references in tracks (although that property uses referenceList too!). - // - // The underlying function validateReferenceList expects a string like - // 'artist' or 'track', but this utility keeps from having to hard-code the - // string in multiple places by referencing the value saved on the class - // instead. - referenceList: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; - }, +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. +export function externalFunction() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} - // Corresponding function for a single reference. - singleReference: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; - }, +// Super simple "contributions by reference" list, used for a variety of +// properties (Artists, Cover Artists, etc). This is the property which is +// externally provided, in the form: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...processed from YAML, spreadsheet, or any other kind of input. +export function contribsByRef() { + return { + flags: {update: true, expose: true}, + update: {validate: isContributionList}, + }; +} - // Corresponding dynamic property to referenceList, which takes the values - // in the provided property and searches the specified wiki data for - // matching actual Thing-subclass objects. - resolvedReferenceList({list, data, find}) { - return compositeFrom(`Thing.common.resolvedReferenceList`, [ - withResolvedReferenceList({ - list, data, find, - notFoundMode: 'filter', - }), +// Artist commentary! Generally present on tracks and albums. +export function commentary() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} - exposeDependency({dependency: '#resolvedReferenceList'}), - ]); +// This is a somewhat more involved data structure - it's for additional +// or "bonus" files associated with albums or tracks (or anything else). +// It's got this form: +// +// [ +// {title: 'Booklet', files: ['Booklet.pdf']}, +// { +// title: 'Wallpaper', +// description: 'Cool Wallpaper!', +// files: ['1440x900.png', '1920x1080.png'] +// }, +// {title: 'Alternate Covers', description: null, files: [...]}, +// ... +// ] +// +export function additionalFiles() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], }, + }; +} - // Corresponding function for a single reference. - resolvedReference({ref, data, find}) { - return compositeFrom(`Thing.common.resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); - }, +// A reference list! Keep in mind this is for general references to wiki +// objects of (usually) other Thing subclasses, not specifically leitmotif +// references in tracks (although that property uses referenceList too!). +// +// The underlying function validateReferenceList expects a string like +// 'artist' or 'track', but this utility keeps from having to hard-code the +// string in multiple places by referencing the value saved on the class +// instead. +export function referenceList(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Corresponding dynamic property to contribsByRef, which takes the values - // in the provided property and searches the object's artistData for - // matching actual Artist objects. The computed structure has the same form - // as contribsByRef, but with Artist objects instead of string references: - // - // [ - // {who: (an Artist), what: 'Viola'}, - // {who: (an Artist), what: null}, - // ... - // ] - // - // Contributions whose "who" values don't match anything in artistData are - // filtered out. (So if the list is all empty, chances are that either the - // reference list is somehow messed up, or artistData isn't being provided - // properly.) - dynamicContribs(contribsByRefProperty) { - return compositeFrom(`Thing.common.dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), + return { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList(referenceType)}, + }; +} - exposeDependency({dependency: '#contribs'}), - ]); - }, +// Corresponding function for a single reference. +export function singleReference(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Nice 'n simple shorthand for an exposed-only flag which is true when any - // contributions are present in the specified property. - contribsPresent: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + return { + flags: {update: true, expose: true}, + update: {validate: validateReference(referenceType)}, + }; +} + +// Corresponding dynamic property to referenceList, which takes the values +// in the provided property and searches the specified wiki data for +// matching actual Thing-subclass objects. +export function resolvedReferenceList({list, data, find}) { + return compositeFrom(`resolvedReferenceList`, [ + withResolvedReferenceList({ + list, data, find, + notFoundMode: 'filter', }), - // Neat little shortcut for "reversing" the reference lists stored on other - // things - for example, tracks specify a "referenced tracks" property, and - // you would use this to compute a corresponding "referenced *by* tracks" - // property. Naturally, the passed ref list property is of the things in the - // wiki data provided, not the requesting Thing itself. - reverseReferenceList({data, list}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); - }, + exposeDependency({dependency: '#resolvedReferenceList'}), + ]); +} - // General purpose wiki data constructor, for properties like artistData, - // trackData, etc. - wikiData: (thingClass) => ({ - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }), +// Corresponding function for a single reference. +export function resolvedReference({ref, data, find}) { + return compositeFrom(`resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency({dependency: '#resolvedReference'}), + ]); +} - // This one's kinda tricky: it parses artist "references" from the - // commentary content, and finds the matching artist for each reference. - // This is mostly useful for credits and listings on artist pages. - commentatorArtists: () => ({ - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], - }, +// Corresponding dynamic property to contribsByRef, which takes the values +// in the provided property and searches the object's artistData for +// matching actual Artist objects. The computed structure has the same form +// as contribsByRef, but with Artist objects instead of string references: +// +// [ +// {who: (an Artist), what: 'Viola'}, +// {who: (an Artist), what: null}, +// ... +// ] +// +// Contributions whose "who" values don't match anything in artistData are +// filtered out. (So if the list is all empty, chances are that either the +// reference list is somehow messed up, or artistData isn't being provided +// properly.) +export function dynamicContribs(contribsByRefProperty) { + return compositeFrom(`dynamicContribs`, [ + withResolvedContribs({ + from: contribsByRefProperty, + into: '#contribs', }), - }; - - // Default custom inspect function, which may be overridden by Thing - // subclasses. This will be used when displaying aggregate errors and other - // command-line logging - it's the place to provide information useful in - // identifying the Thing being presented. - [inspect.custom]() { - const cname = this.constructor.name; - return ( - (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) + - (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '') - ); - } + exposeDependency({dependency: '#contribs'}), + ]); +} - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. +export function contribsPresent(contribsByRefProperty) { + return { + flags: {expose: true}, + expose: { + dependencies: [contribsByRefProperty], + compute({ + [contribsByRefProperty]: contribsByRef, + }) { + return !empty(contribsByRef); + }, } + }; +} - if (!thing.directory) { - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - } +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. +export function reverseReferenceList({data, list}) { + return compositeFrom(`reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency({dependency: '#reverseReferenceList'}), + ]); +} - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. +export function wikiData(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; } +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. +export function commentatorArtists(){ + return { + flags: {expose: true}, + + expose: { + dependencies: ['artistData', 'commentary'], + + compute: ({artistData, commentary}) => + artistData && commentary + ? Array.from( + new Set( + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g) + ).map(({groups: {who}}) => + find.artist(who, artistData, {mode: 'quiet'}) + ) + ) + ) + : [], + }, + }; +} + +// Compositional utilities + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -479,14 +511,14 @@ export function withResolvedReferenceList({ ]); } -// Check out the info on Thing.common.reverseReferenceList! +// Check out the info on reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ data, list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ + return compositeFrom(`withReverseReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], -- cgit 1.3.0-6-gf8a5 From 9db4b91c66f8b9b98d098bfe446e29f5b3caee53 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 14:53:25 -0300 Subject: data: withResolvedContribs: use default "into" --- src/data/things/thing.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19f00b3e..9d8b2ea2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -286,12 +286,8 @@ export function resolvedReference({ref, data, find}) { // properly.) export function dynamicContribs(contribsByRefProperty) { return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), - - exposeDependency({dependency: '#contribs'}), + withResolvedContribs({from: contribsByRefProperty}), + exposeDependency({dependency: '#resolvedContribs'}), ]); } @@ -368,7 +364,10 @@ export function commentatorArtists(){ // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({from, into}) { +export function withResolvedContribs({ + from, + into = '#resolvedContribs', +}) { return compositeFrom(`withResolvedContribs`, [ raiseWithoutDependency({ dependency: from, -- cgit 1.3.0-6-gf8a5 From bbccaf51222cb4bed73466164496f5bc1030292c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 17:30:54 -0300 Subject: data: roll paired "byRef" and "dynamic" properties into one --- src/data/things/thing.js | 147 ++++++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 73 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9d8b2ea2..91ad96af 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -11,8 +11,11 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { compositeFrom, exitWithoutDependency, + exposeConstant, exposeDependency, + exposeDependencyOrContinue, raiseWithoutDependency, + withUpdateValueAsDependency, } from '#composite'; import { @@ -162,22 +165,31 @@ export function externalFunction() { }; } -// Super simple "contributions by reference" list, used for a variety of -// properties (Artists, Cover Artists, etc). This is the property which is -// externally provided, in the form: +// Strong 'n sturdy contribution list, rolling a list of references (provided +// as this property's update value) and the resolved results (as get exposed) +// into one property. Update value will look something like this: // -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] // -// ...processed from YAML, spreadsheet, or any other kind of input. -export function contribsByRef() { - return { - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }; +// ...typically as processed from YAML, spreadsheet, or elsewhere. +// Exposes as the same, but with the "who" replaced with matches found in +// artistData - which means this always depends on an `artistData` property +// also existing on this object! +// +export function contributionList() { + return compositeFrom(`contributionList`, [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({ + value: [], + update: {validate: isContributionList}, + }), + ]); } // Artist commentary! Generally present on tracks and albums. @@ -222,88 +234,77 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. -export function referenceList(thingClass) { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); +export function referenceList({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; -} - -// Corresponding function for a single reference. -export function singleReference(thingClass) { const {[Thing.referenceType]: referenceType} = thingClass; if (!referenceType) { throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; -} + return compositeFrom(`referenceList`, [ + withUpdateValueAsDependency(), -// Corresponding dynamic property to referenceList, which takes the values -// in the provided property and searches the specified wiki data for -// matching actual Thing-subclass objects. -export function resolvedReferenceList({list, data, find}) { - return compositeFrom(`resolvedReferenceList`, [ withResolvedReferenceList({ - list, data, find, + data, find, + list: '#updateValue', notFoundMode: 'filter', }), - exposeDependency({dependency: '#resolvedReferenceList'}), + exposeDependency({ + dependency: '#resolvedReferenceList', + update: { + validate: validateReferenceList(referenceType), + }, + }), ]); } // Corresponding function for a single reference. -export function resolvedReference({ref, data, find}) { - return compositeFrom(`resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); -} +export function singleReference({ + class: thingClass, + data, + find, +}) { + if (!thingClass) { + throw new TypeError(`Expected a Thing class`); + } -// Corresponding dynamic property to contribsByRef, which takes the values -// in the provided property and searches the object's artistData for -// matching actual Artist objects. The computed structure has the same form -// as contribsByRef, but with Artist objects instead of string references: -// -// [ -// {who: (an Artist), what: 'Viola'}, -// {who: (an Artist), what: null}, -// ... -// ] -// -// Contributions whose "who" values don't match anything in artistData are -// filtered out. (So if the list is all empty, chances are that either the -// reference list is somehow messed up, or artistData isn't being provided -// properly.) -export function dynamicContribs(contribsByRefProperty) { - return compositeFrom(`dynamicContribs`, [ - withResolvedContribs({from: contribsByRefProperty}), - exposeDependency({dependency: '#resolvedContribs'}), + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } + + return compositeFrom(`singleReference`, [ + withUpdateValueAsDependency(), + + withResolvedReference({ref: '#updateValue', data, find}), + + exposeDependency({ + dependency: '#resolvedReference', + update: { + validate: validateReference(referenceType), + }, + }), ]); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsByRefProperty) { +export function contribsPresent(contribsProperty) { return { flags: {expose: true}, expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + dependencies: [contribsProperty], + compute: ({[contribsProperty]: contribs}) => + !empty(contribs), + }, }; } @@ -380,13 +381,13 @@ export function withResolvedContribs({ mapDependencies: {from}, compute: ({from}, continuation) => continuation({ - '#whoByRef': from.map(({who}) => who), + '#artistRefs': from.map(({who}) => who), '#what': from.map(({what}) => what), }), }, withResolvedReferenceList({ - list: '#whoByRef', + list: '#artistRefs', data: 'artistData', into: '#who', find: find.artist, -- cgit 1.3.0-6-gf8a5 From 3ebe98d51d94a3e5277d65b2a4d2b5b433449214 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 11:29:53 -0300 Subject: data: withResolvedReferenceList: handle undefined matches --- src/data/things/thing.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 91ad96af..79d8ae0e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -491,19 +491,20 @@ export function withResolvedReferenceList({ let matches = list.map(ref => findFunction(ref, data, {mode: 'quiet'})); - if (!matches.includes(null)) { + if (matches.every(match => match)) { return continuation.raise({matches}); } switch (notFoundMode) { case 'filter': - matches = matches.filter(value => value !== null); + matches = matches.filter(match => match); return continuation.raise({matches}); case 'exit': return continuation.exit([]); case 'null': + matches = matches.map(match => match ?? null); return continuation.raise({matches}); } }, -- cgit 1.3.0-6-gf8a5 From 15b0b5422a3de8da52e14666909418405bdb8c39 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:09:16 -0300 Subject: data: update commentatorArtists --- src/data/things/thing.js | 55 +++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 22 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..9e7f940f 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import { @@ -334,29 +334,40 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists(){ - return { - flags: {expose: true}, +export function commentatorArtists() { + return compositeFrom(`commentatorArtists`, [ + exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - }; + + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: find.artist, + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, + }, + ]); } // Compositional utilities -- cgit 1.3.0-6-gf8a5 From e01b73d286fbb11ac8ded59b4c23738dff195171 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:22:10 -0300 Subject: data: dimensions utility --- src/data/things/thing.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 9e7f940f..0484b589 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,6 +25,7 @@ import { isColor, isContributionList, isDate, + isDimensions, isDirectory, isFileExtension, isName, @@ -122,6 +123,15 @@ export function fileExtension(defaultFileExtension = null) { }; } +// Plain ol' image dimensions. This is a two-item array of positive integers, +// corresponding to width and height respectively. +export function dimensions() { + return { + flags: {update: true, expose: true}, + update: {validate: isDimensions}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { -- cgit 1.3.0-6-gf8a5 From cd3e2ae7384d82f0f2758beb0ae38ce0fe9f5e09 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 16:25:22 -0300 Subject: data: duration utility --- src/data/things/thing.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0484b589..169fc1ca 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -27,6 +27,7 @@ import { isDate, isDimensions, isDirectory, + isDuration, isFileExtension, isName, isString, @@ -132,6 +133,15 @@ export function dimensions() { }; } +// Duration! This is a number of seconds, possibly floating point, always +// at minimum zero. +export function duration() { + return { + flags: {update: true, expose: true}, + update: {validate: isDuration}, + }; +} + // Straightforward flag descriptor for a variety of property purposes. // Provide a default value, true or false! export function flag(defaultValue = false) { -- cgit 1.3.0-6-gf8a5 From 6fe22802d8220b983a488f4efee1834bacbdb166 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 17:20:48 -0300 Subject: data: cleaner withResolvedReferenceList notFoundMode implementation --- src/data/things/thing.js | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 169fc1ca..96ac9b12 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -514,29 +514,45 @@ export function withResolvedReferenceList({ }), { - options: {findFunction, notFoundMode}, mapDependencies: {list, data}, - mapContinuation: {matches: into}, + options: {findFunction}, - compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { - let matches = - list.map(ref => findFunction(ref, data, {mode: 'quiet'})); + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, - if (matches.every(match => match)) { - return continuation.raise({matches}); - } + { + dependencies: ['#matches'], + mapContinuation: {into}, - switch (notFoundMode) { - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({matches}); + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({into: matches}) + : continuation()), + }, + + { + dependencies: ['#matches'], + options: {notFoundMode}, + mapContinuation: {into}, + compute({ + '#matches': matches, + '#options': {notFoundMode}, + }, continuation) { + switch (notFoundMode) { case 'exit': return continuation.exit([]); + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({into: matches}); + case 'null': matches = matches.map(match => match ?? null); - return continuation.raise({matches}); + return continuation.raise({into: matches}); } }, }, -- cgit 1.3.0-6-gf8a5 From c82784ebb4e5141bfe97664f3252303b3e833863 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:13:44 -0300 Subject: data: withPropertyFrom{Object,List}, fillMissingListItems utils --- src/data/things/thing.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 96ac9b12..a87e6ed6 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withPropertyFromList, withUpdateValueAsDependency, } from '#composite'; @@ -408,14 +409,8 @@ export function withResolvedContribs({ raise: {into: []}, }), - { - mapDependencies: {from}, - compute: ({from}, continuation) => - continuation({ - '#artistRefs': from.map(({who}) => who), - '#what': from.map(({what}) => what), - }), - }, + withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), + withPropertyFromList({list: from, property: 'what', into: '#what'}), withResolvedReferenceList({ list: '#artistRefs', -- cgit 1.3.0-6-gf8a5 From 7a21c665d888b0db4c47c72049f7649bf1dabcde Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:47:38 -0300 Subject: data: withPropertiesFrom{Object,List} --- src/data/things/thing.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a87e6ed6..52f0b773 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,7 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, - withPropertyFromList, + withPropertiesFromList, withUpdateValueAsDependency, } from '#composite'; @@ -409,21 +409,24 @@ export function withResolvedContribs({ raise: {into: []}, }), - withPropertyFromList({list: from, property: 'who', into: '#artistRefs'}), - withPropertyFromList({list: from, property: 'what', into: '#what'}), + withPropertiesFromList({ + list: from, + properties: ['who', 'what'], + prefix: '#contribs', + }), withResolvedReferenceList({ - list: '#artistRefs', + list: '#contribs.who', data: 'artistData', - into: '#who', + into: '#contribs.who', find: find.artist, notFoundMode: 'null', }), { - dependencies: ['#who', '#what'], + dependencies: ['#contribs.who', '#contribs.what'], mapContinuation: {into}, - compute({'#who': who, '#what': what}, continuation) { + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ into: stitchArrays({who, what}), -- cgit 1.3.0-6-gf8a5 From 9109356037ce98af378765302841c957cc96b8d8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:33:04 -0300 Subject: data: exitWithoutContribs utility --- src/data/things/thing.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 52f0b773..fe9000b4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -436,6 +436,23 @@ export function withResolvedContribs({ ]); } +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. +export function exitWithoutContribs({ + contribs, + value = null, +}) { + return compositeFrom(`exitWithoutContribs`, [ + withResolvedContribs({from: contribs}), + exitWithoutDependency({ + dependency: '#resolvedContribs', + mode: 'empty', + value, + }), + ]); +} + // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the // data dependency is null, or, if notFoundMode is set to 'exit', if the find -- cgit 1.3.0-6-gf8a5 From f242d1dec3cd905e74eec6ce518781843d5f65d9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:40:15 -0300 Subject: data: update contribsPresent syntax & implementation --- src/data/things/thing.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 79d8ae0e..0f47dc90 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -15,6 +15,7 @@ import { exposeDependency, exposeDependencyOrContinue, raiseWithoutDependency, + withResultOfAvailabilityCheck, withUpdateValueAsDependency, } from '#composite'; @@ -297,15 +298,11 @@ export function singleReference({ // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent(contribsProperty) { - return { - flags: {expose: true}, - expose: { - dependencies: [contribsProperty], - compute: ({[contribsProperty]: contribs}) => - !empty(contribs), - }, - }; +export function contribsPresent({contribs}) { + return compositeFrom(`contribsPresent`, [ + withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), + exposeDependency({dependency: '#availability'}), + ]); } // Neat little shortcut for "reversing" the reference lists stored on other -- cgit 1.3.0-6-gf8a5 From c4f6c41a248ba9ef4f802cc03c20757d417540e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 21:08:06 -0300 Subject: data: WIP cached composition nonsense --- src/data/things/thing.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index b1a9a802..19954b19 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -512,7 +512,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`withResolvedReferenceList`, [ + const composite = compositeFrom(`withResolvedReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], @@ -526,13 +526,19 @@ export function withResolvedReferenceList({ }), { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + cache: 'aggressive', + annotation: `withResolvedReferenceList.getMatches`, + flags: {expose: true, compose: true}, + + compute: { + mapDependencies: {list, data}, + options: {findFunction}, + + compute: ({list, data, '#options': {findFunction}}, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, }, { @@ -569,6 +575,9 @@ export function withResolvedReferenceList({ }, }, ]); + + console.log(composite.expose); + return composite; } // Check out the info on reverseReferenceList! -- cgit 1.3.0-6-gf8a5 From 9dd9d5c328da8ad1d90cd33d4a13efac92104398 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 12 Sep 2023 14:34:20 -0300 Subject: data: more syntax WIP --- src/data/things/thing.js | 591 ++++++++++++++++++++++++++++++----------------- 1 file changed, 374 insertions(+), 217 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 19954b19..5cfeaeb2 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -203,15 +203,18 @@ export function externalFunction() { // also existing on this object! // export function contributionList() { - return compositeFrom(`contributionList`, [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({ - value: [], - update: {validate: isContributionList}, - }), - ]); + return compositeFrom({ + annotation: `contributionList`, + + update: {validate: isContributionList}, + + steps: [ + withUpdateValueAsDependency(), + withResolvedContribs({from: '#updateValue'}), + exposeDependencyOrContinue({dependency: '#resolvedContribs'}), + exposeConstant({value: []}), + ], + }); } // Artist commentary! Generally present on tracks and albums. @@ -259,7 +262,7 @@ export function additionalFiles() { export function referenceList({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -270,29 +273,40 @@ export function referenceList({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`referenceList`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `referenceList`, - withResolvedReferenceList({ - data, find, - list: '#updateValue', - notFoundMode: 'filter', - }), + update: { + validate: validateReferenceList(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReferenceList', - update: { - validate: validateReferenceList(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], + }); } // Corresponding function for a single reference. export function singleReference({ class: thingClass, data, - find, + find: findFunction, }) { if (!thingClass) { throw new TypeError(`Expected a Thing class`); @@ -303,27 +317,56 @@ export function singleReference({ throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); } - return compositeFrom(`singleReference`, [ - withUpdateValueAsDependency(), + return compositeFrom({ + annotation: `singleReference`, - withResolvedReference({ref: '#updateValue', data, find}), + update: { + validate: validateReference(referenceType), + }, - exposeDependency({ - dependency: '#resolvedReference', - update: { - validate: validateReference(referenceType), - }, - }), - ]); + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.findFunction': findFunction, + }, + + steps: [ + withUpdateValueAsDependency(), + + withResolvedReference({ + ref: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReference'}), + ], + }); } // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({contribs}) { - return compositeFrom(`contribsPresent`, [ - withResultOfAvailabilityCheck({fromDependency: contribs, mode: 'empty'}), - exposeDependency({dependency: '#availability'}), - ]); +export function contribsPresent({ + contribs, +}) { + return compositeFrom({ + annotation: `contribsPresent`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + steps: [ + withResultOfAvailabilityCheck({ + fromDependency: '#composition.contribs', + mode: 'empty', + }), + + exposeDependency({dependency: '#availability'}), + ], + }); } // Neat little shortcut for "reversing" the reference lists stored on other @@ -332,10 +375,23 @@ export function contribsPresent({contribs}) { // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. export function reverseReferenceList({data, list}) { - return compositeFrom(`reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); + return compositeFrom({ + annotation: `reverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + '#composition.list': list, + }, + + steps: [ + withReverseReferenceList({ + data: '#composition.data', + list: '#composition.list', + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], + }); } // General purpose wiki data constructor, for properties like artistData, @@ -353,39 +409,51 @@ export function wikiData(thingClass) { // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. export function commentatorArtists() { - return compositeFrom(`commentatorArtists`, [ - exitWithoutDependency({dependency: 'commentary', mode: 'falsy', value: []}), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), + return compositeFrom({ + annotation: `commentatorArtists`, + + constantDependencies: { + '#composition.findFunction': find.artists, }, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: find.artist, - }), + steps: [ + exitWithoutDependency({ + dependency: 'commentary', + mode: 'falsy', + value: [], + }), - { - flags: {expose: true}, + { + dependencies: ['commentary'], + compute: ({commentary}, continuation) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), + }, - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + into: '#artists', + find: '#composition.findFunction', + }), + + { + flags: {expose: true}, + + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), + }, }, - }, - ]); + ], + }); } // Compositional utilities @@ -398,39 +466,54 @@ export function withResolvedContribs({ from, into = '#resolvedContribs', }) { - return compositeFrom(`withResolvedContribs`, [ - raiseWithoutDependency({ - dependency: from, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - withPropertiesFromList({ - list: from, - properties: ['who', 'what'], - prefix: '#contribs', - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: find.artist, - notFoundMode: 'null', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - mapContinuation: {into}, - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - into: stitchArrays({who, what}), - }); - }, + return compositeFrom({ + annotation: `withResolvedContribs`, + + mapDependencies: { + '#composition.from': from, }, - ]); + + mapContinuation: { + '#composition.into': into, + }, + + constantDependencies: { + '#composition.findFunction': find.artist, + '#composition.notFoundMode': 'null', + }, + + steps: [ + raiseWithoutDependency({ + dependency: '#composition.from', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + withPropertiesFromList({ + list: '#composition.from', + prefix: '#contribs', + properties: ['who', 'what'], + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: '#composition.findFunction', + notFoundMode: '#composition.notFoundMode', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + compute({'#contribs.who': who, '#contribs.what': what}, continuation) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); + }, + }, + ], + }); } // Shorthand for exiting if the contribution list (usually a property's update @@ -440,14 +523,37 @@ export function exitWithoutContribs({ contribs, value = null, }) { - return compositeFrom(`exitWithoutContribs`, [ - withResolvedContribs({from: contribs}), - exitWithoutDependency({ - dependency: '#resolvedContribs', - mode: 'empty', - value, - }), - ]); + return compositeFrom({ + annotation: `exitWithoutContribs`, + + mapDependencies: { + '#composition.contribs': contribs, + }, + + constantDependencies: { + '#composition.value': value, + }, + + steps: [ + withResolvedContribs({from: '#composition.contribs'}), + + withResultOfAvailabilityCheck({ + fromDependency: '#resolvedContribs', + mode: 'empty', + }), + + { + dependencies: ['#availability', '#composition.value'], + compute: ({ + '#availability': availability, + '#composition.value': value, + }, continuation) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], + }); } // Resolves a reference by using the provided find function to match it @@ -461,39 +567,54 @@ export function withResolvedReference({ data, find: findFunction, into = '#resolvedReference', - notFoundMode = 'null', + notFoundMode, }) { - if (!['exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be exit or null`); - } - - return compositeFrom(`withResolvedReference`, [ - raiseWithoutDependency({ - dependency: ref, - map: {into}, - raise: {into: null}, - }), - - exitWithoutDependency({ - dependency: data, - }), - - { - options: {findFunction, notFoundMode}, - mapDependencies: {ref, data}, - mapContinuation: {match: into}, + return compositeFrom({ + annotation: `withResolvedReference`, + + mapDependencies: { + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, - compute({ref, data, '#options': {findFunction, notFoundMode}}, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); + constantDependencies: { + '#composition.notFoundMode': 'null', + }, - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } + mapContinuation: { + '#composition.into': into, + }, - return continuation.raise({match}); + steps: [ + raiseWithoutDependency({ + dependency: '#composition.ref', + raise: {'#composition.into': null}, + }), + + exitWithoutDependency({ + dependency: '#composition.data', + }), + + { + compute({ + '#composition.ref': ref, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); + }, }, - }, - ]); + ], + }); } // Resolves a list of references, with each reference matched with provided @@ -505,79 +626,93 @@ export function withResolvedReferenceList({ list, data, find: findFunction, + notFoundMode, into = '#resolvedReferenceList', - notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - const composite = compositeFrom(`withResolvedReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - raiseWithoutDependency({ - dependency: list, - mode: 'empty', - map: {into}, - raise: {into: []}, - }), - - { - cache: 'aggressive', - annotation: `withResolvedReferenceList.getMatches`, - flags: {expose: true, compose: true}, - - compute: { - mapDependencies: {list, data}, - options: {findFunction}, - - compute: ({list, data, '#options': {findFunction}}, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, + return compositeFrom({ + annotation: `withResolvedReferenceList`, + + mapDependencies: { + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + '#composition.notFoundMode': notFoundMode, }, - { - dependencies: ['#matches'], - mapContinuation: {into}, + constantDependencies: { + '#composition.notFoundMode': 'filter', + }, - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({into: matches}) - : continuation()), + mapContinuation: { + '#composition.into': into, }, - { - dependencies: ['#matches'], - options: {notFoundMode}, - mapContinuation: {into}, - - compute({ - '#matches': matches, - '#options': {notFoundMode}, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({into: matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({into: matches}); - } + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + raiseWithoutDependency({ + dependency: '#composition.list', + raise: {'#composition.into': []}, + mode: 'empty', + }), + + { + dependencies: [ + '#composition.list', + '#composition.data', + '#composition.findFunction', + ], + + compute: ({ + '#composition.list': list, + '#composition.data': data, + '#composition.findFunction': findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), + }, + + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - }, - ]); - console.log(composite.expose); - return composite; + { + dependencies: ['#matches', '#composition.notFoundMode'], + compute({ + '#matches': matches, + '#composition.notFoundMode': notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], + }); } // Check out the info on reverseReferenceList! @@ -587,22 +722,44 @@ export function withReverseReferenceList({ list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`withReverseReferenceList`, [ - exitWithoutDependency({ - dependency: data, - value: [], - }), - - { - dependencies: ['this'], - mapDependencies: {data}, - mapContinuation: {into}, - options: {refListProperty}, - - compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) => - continuation({ - into: data.filter(thing => thing[refListProperty].includes(thisThing)), - }), + return compositeFrom({ + annotation: `withReverseReferenceList`, + + mapDependencies: { + '#composition.data': data, + }, + + constantDependencies: { + '#composition.refListProperty': refListProperty, + }, + + mapContinuation: { + '#composition.into': into, }, - ]); + + steps: [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), + }, + ], + }); } -- cgit 1.3.0-6-gf8a5 From 7cd3bdc4998ae1fc1b9ab4bb721d2727f64511e1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 15 Sep 2023 20:03:25 -0300 Subject: data: miscellaneous composite template updates --- src/data/things/thing.js | 543 +++++++++++++++++++++++------------------------ 1 file changed, 263 insertions(+), 280 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 5cfeaeb2..d1a8fdc1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,6 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; +import {oneOf} from '#validators'; import { compositeFrom, @@ -14,10 +15,11 @@ import { exposeConstant, exposeDependency, exposeDependencyOrContinue, - raiseWithoutDependency, + input, + raiseOutputWithoutDependency, + templateCompositeFrom, withResultOfAvailabilityCheck, withPropertiesFromList, - withUpdateValueAsDependency, } from '#composite'; import { @@ -208,7 +210,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedContribs({from: '#updateValue'}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), @@ -288,7 +290,7 @@ export function referenceList({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReferenceList({ @@ -332,7 +334,7 @@ export function singleReference({ '#composition.findFunction': findFunction, }, - steps: [ + steps: () => [ withUpdateValueAsDependency(), withResolvedReference({ @@ -358,7 +360,7 @@ export function contribsPresent({ '#composition.contribs': contribs, }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ fromDependency: '#composition.contribs', mode: 'empty', @@ -383,7 +385,7 @@ export function reverseReferenceList({data, list}) { '#composition.list': list, }, - steps: [ + steps: () => [ withReverseReferenceList({ data: '#composition.data', list: '#composition.list', @@ -416,7 +418,7 @@ export function commentatorArtists() { '#composition.findFunction': find.artists, }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: 'commentary', mode: 'falsy', @@ -462,99 +464,97 @@ export function commentatorArtists() { // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist // object, and filtering out those whose "who" doesn't match any artist. -export function withResolvedContribs({ - from, - into = '#resolvedContribs', -}) { - return compositeFrom({ - annotation: `withResolvedContribs`, - - mapDependencies: { - '#composition.from': from, - }, - - mapContinuation: { - '#composition.into': into, - }, - - constantDependencies: { - '#composition.findFunction': find.artist, - '#composition.notFoundMode': 'null', - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.from', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - withPropertiesFromList({ - list: '#composition.from', - prefix: '#contribs', - properties: ['who', 'what'], - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - into: '#contribs.who', - find: '#composition.findFunction', - notFoundMode: '#composition.notFoundMode', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - compute({'#contribs.who': who, '#contribs.what': what}, continuation) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - '#composition.into': stitchArrays({who, what}), - }); - }, +export const withResolvedContribs = templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + // todo: validate + from: input(), + + findFunction: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedContribs', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + into: '#contribs.who', + find: input('find'), + notFoundMode: input('notFoundMode'), + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + '#composition.into': stitchArrays({who, what}), + }); }, - ], - }); -} + }, + ], +}); // Shorthand for exiting if the contribution list (usually a property's update // value) resolves to empty - ensuring that the later computed results are only // returned if these contributions are present. -export function exitWithoutContribs({ - contribs, - value = null, -}) { - return compositeFrom({ - annotation: `exitWithoutContribs`, - - mapDependencies: { - '#composition.contribs': contribs, +export const exitWithoutContribs = templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + // todo: validate + contribs: input(), + + value: input({null: true}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), }, - - constantDependencies: { - '#composition.value': value, - }, - - steps: [ - withResolvedContribs({from: '#composition.contribs'}), - - withResultOfAvailabilityCheck({ - fromDependency: '#resolvedContribs', - mode: 'empty', - }), - - { - dependencies: ['#availability', '#composition.value'], - compute: ({ - '#availability': availability, - '#composition.value': value, - }, continuation) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], - }); -} + ], +}); // Resolves a reference by using the provided find function to match it // within the provided thingData dependency. This will early exit if the @@ -562,204 +562,187 @@ export function exitWithoutContribs({ // function doesn't match anything for the reference. Otherwise, the data // object is provided on the output dependency; or null, if the reference // doesn't match anything or itself was null to begin with. -export function withResolvedReference({ - ref, - data, - find: findFunction, - into = '#resolvedReference', - notFoundMode, -}) { - return compositeFrom({ - annotation: `withResolvedReference`, - - mapDependencies: { - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'null', - }, - - mapContinuation: { - '#composition.into': into, - }, - - steps: [ - raiseWithoutDependency({ - dependency: '#composition.ref', - raise: {'#composition.into': null}, - }), - - exitWithoutDependency({ - dependency: '#composition.data', - }), - - { - compute({ - '#composition.ref': ref, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raise({match}); - }, +export const withResolvedReference = templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + // todo: validate + ref: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('null', 'exit'), + defaultValue: 'null', + }), + }, + + outputs: { + into: '#resolvedReference', + }, + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({into: null}), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + input('notFoundMode'), + ], + + compute({ + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); + + if (match === null && notFoundMode === 'exit') { + return continuation.exit(null); + } + + return continuation.raise({match}); }, - ], - }); -} + }, + ], +}); // Resolves a list of references, with each reference matched with provided // data in the same way as withResolvedReference. This will early exit if the // data dependency is null (even if the reference list is empty). By default // it will filter out references which don't match, but this can be changed // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export function withResolvedReferenceList({ - list, - data, - find: findFunction, - notFoundMode, - into = '#resolvedReferenceList', -}) { - if (!['filter', 'exit', 'null'].includes(notFoundMode)) { - throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); - } - - return compositeFrom({ - annotation: `withResolvedReferenceList`, - - mapDependencies: { - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - '#composition.notFoundMode': notFoundMode, - }, - - constantDependencies: { - '#composition.notFoundMode': 'filter', +export const withResolvedReferenceList = templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + // todo: validate + list: input(), + + // todo: validate + data: input(), + + find: input({type: 'function'}), + + notFoundMode: input({ + validate: oneOf('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: { + into: '#resolvedReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({into: []}), + }), + + { + dependencies: [input('list'), input('data'), input('find')], + compute: ({ + [input('list')]: list, + [input('data')]: data, + [input('find')]: findFunction, + }, continuation) => + continuation({ + '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), + }), }, - mapContinuation: { - '#composition.into': into, + { + dependencies: ['#matches'], + compute: ({'#matches': matches}, continuation) => + (matches.every(match => match) + ? continuation.raise({'#continuation.into': matches}) + : continuation()), }, - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - raiseWithoutDependency({ - dependency: '#composition.list', - raise: {'#composition.into': []}, - mode: 'empty', - }), - - { - dependencies: [ - '#composition.list', - '#composition.data', - '#composition.findFunction', - ], - - compute: ({ - '#composition.list': list, - '#composition.data': data, - '#composition.findFunction': findFunction, - }, continuation) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), + { + dependencies: ['#matches', input('notFoundMode')], + compute({ + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }, continuation) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + matches = matches.filter(match => match); + return continuation.raise({'#continuation.into': matches}); + + case 'null': + matches = matches.map(match => match ?? null); + return continuation.raise({'#continuation.into': matches}); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } }, - - { - dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => - (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) - : continuation()), - }, - - { - dependencies: ['#matches', '#composition.notFoundMode'], - compute({ - '#matches': matches, - '#composition.notFoundMode': notFoundMode, - }, continuation) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); - - case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], - }); -} + }, + ], +}); // Check out the info on reverseReferenceList! // This is its composable form. -export function withReverseReferenceList({ - data, - list: refListProperty, - into = '#reverseReferenceList', -}) { - return compositeFrom({ - annotation: `withReverseReferenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.refListProperty': refListProperty, - }, - - mapContinuation: { - '#composition.into': into, +export const withReverseReferenceList = templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + // todo: validate + data: input(), + + list: input({type: 'string'}), + }, + + outputs: { + into: '#reverseReferenceList', + }, + + steps: () => [ + exitWithoutDependency({ + dependency: '#composition.data', + value: [], + }), + + { + dependencies: [ + 'this', + '#composition.data', + '#composition.refListProperty', + ], + + compute: ({ + this: thisThing, + '#composition.data': data, + '#composition.refListProperty': refListProperty, + }, continuation) => + continuation({ + '#composition.into': + data.filter(thing => thing[refListProperty].includes(thisThing)), + }), }, - - steps: [ - exitWithoutDependency({ - dependency: '#composition.data', - value: [], - }), - - { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], - - compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, - }, continuation) => - continuation({ - '#composition.into': - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], - }); -} + ], +}); -- cgit 1.3.0-6-gf8a5 From b4dd9d3f288130acdd9fefa2321b4b547f348b32 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 16 Sep 2023 13:03:26 -0300 Subject: data: more WIP syntax updates --- src/data/things/thing.js | 72 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 23 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index d1a8fdc1..45e91238 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -35,6 +35,7 @@ import { isFileExtension, isName, isString, + isType, isURL, validateArrayItems, validateInstanceOf, @@ -211,8 +212,7 @@ export function contributionList() { update: {validate: isContributionList}, steps: () => [ - withUpdateValueAsDependency(), - withResolvedContribs({from: '#updateValue'}), + withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), ], @@ -261,27 +261,61 @@ export function additionalFiles() { // 'artist' or 'track', but this utility keeps from having to hard-code the // string in multiple places by referencing the value saved on the class // instead. +export const referenceList = templateCompositeFrom({ + annotation: `referenceList`, + + compose: false, + + inputs: { + class: input({ + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, + }), + + find: input({type: 'function'}), + + // todo: validate + data: input(), + }, + + update: { + dependencies: [ + input.staticValue('class'), + ], + + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; + }, + }, + + steps: () => [ + withResolvedReferenceList({ + list: '#updateValue', + data: '#composition.data', + find: '#composition.findFunction', + }), + + exposeDependency({dependency: '#resolvedReferenceList'}), + ], +}) export function referenceList({ class: thingClass, data, find: findFunction, }) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } - - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - return compositeFrom({ annotation: `referenceList`, - update: { - validate: validateReferenceList(referenceType), - }, - mapDependencies: { '#composition.data': data, }, @@ -292,14 +326,6 @@ export function referenceList({ steps: () => [ withUpdateValueAsDependency(), - - withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), ], }); } -- cgit 1.3.0-6-gf8a5 From fd102ee597e2ad2ba8f0950ce1a16fd34029963d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 18 Sep 2023 13:26:18 -0300 Subject: data: MORE composite wip --- src/data/things/thing.js | 265 +++++++++++++++++++++-------------------------- 1 file changed, 118 insertions(+), 147 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 45e91238..a5f0b78d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -5,7 +5,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import find from '#find'; -import {empty, stitchArrays, unique} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; import {oneOf} from '#validators'; @@ -253,6 +253,18 @@ export function additionalFiles() { }; } +const thingClassInput = { + validate(thingClass) { + isType(thingClass, 'function'); + + if (!Object.hasOwn(thingClass, Thing.referenceType)) { + throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); + } + + return true; + }, +}; + // A reference list! Keep in mind this is for general references to wiki // objects of (usually) other Thing subclasses, not specifically leitmotif // references in tracks (although that property uses referenceList too!). @@ -267,18 +279,7 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input({ - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, - }), - + class: input(thingClassInput), find: input({type: 'function'}), // todo: validate @@ -300,127 +301,100 @@ export const referenceList = templateCompositeFrom({ steps: () => [ withResolvedReferenceList({ - list: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', + list: input.updateValue(), + data: input('data'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReferenceList'}), ], -}) -export function referenceList({ - class: thingClass, - data, - find: findFunction, -}) { - return compositeFrom({ - annotation: `referenceList`, - - mapDependencies: { - '#composition.data': data, - }, - - constantDependencies: { - '#composition.findFunction': findFunction, - }, - - steps: () => [ - withUpdateValueAsDependency(), - ], - }); -} +}); // Corresponding function for a single reference. -export function singleReference({ - class: thingClass, - data, - find: findFunction, -}) { - if (!thingClass) { - throw new TypeError(`Expected a Thing class`); - } +export const singleReference = templateCompositeFrom({ + annotation: `singleReference`, - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } + compose: false, - return compositeFrom({ - annotation: `singleReference`, + inputs: { + class: input(thingClassInput), + find: input({type: 'function'}), - update: { - validate: validateReference(referenceType), - }, + // todo: validate + data: input(), + }, - mapDependencies: { - '#composition.data': data, - }, + update: { + dependencies: [ + input.staticValue('class'), + ], - constantDependencies: { - '#composition.findFunction': findFunction, + compute({ + [input.staticValue('class')]: thingClass, + }) { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, + }, - steps: () => [ - withUpdateValueAsDependency(), - - withResolvedReference({ - ref: '#updateValue', - data: '#composition.data', - find: '#composition.findFunction', - }), + steps: () => [ + withResolvedReference({ + ref: input.updateValue(), + data: input('data'), + find: input('findFunction'), + }), - exposeDependency({dependency: '#resolvedReference'}), - ], - }); -} + exposeDependency({dependency: '#resolvedReference'}), + ], +}); // Nice 'n simple shorthand for an exposed-only flag which is true when any // contributions are present in the specified property. -export function contribsPresent({ - contribs, -}) { - return compositeFrom({ - annotation: `contribsPresent`, +export const contribsPresent = templateCompositeFrom({ + annotation: `contribsPresent`, - mapDependencies: { - '#composition.contribs': contribs, - }, + compose: false, - steps: () => [ - withResultOfAvailabilityCheck({ - fromDependency: '#composition.contribs', - mode: 'empty', - }), + inputs: { + contribs: input({type: 'string'}), + }, - exposeDependency({dependency: '#availability'}), - ], - }); -} + steps: () => [ + withResultOfAvailabilityCheck({ + fromDependency: input('contribs'), + mode: input.value('empty'), + }), + + exposeDependency({dependency: '#availability'}), + ], +}); // Neat little shortcut for "reversing" the reference lists stored on other // things - for example, tracks specify a "referenced tracks" property, and // you would use this to compute a corresponding "referenced *by* tracks" // property. Naturally, the passed ref list property is of the things in the // wiki data provided, not the requesting Thing itself. -export function reverseReferenceList({data, list}) { - return compositeFrom({ - annotation: `reverseReferenceList`, +export const reverseReferenceList = templateCompositeFrom({ + annotation: `reverseReferenceList`, - mapDependencies: { - '#composition.data': data, - '#composition.list': list, - }, + compose: false, - steps: () => [ - withReverseReferenceList({ - data: '#composition.data', - list: '#composition.list', - }), + inputs: { + // todo: validate + data: input(), - exposeDependency({dependency: '#reverseReferenceList'}), - ], - }); -} + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseReferenceList'}), + ], +}); // General purpose wiki data constructor, for properties like artistData, // trackData, etc. @@ -436,53 +410,50 @@ export function wikiData(thingClass) { // This one's kinda tricky: it parses artist "references" from the // commentary content, and finds the matching artist for each reference. // This is mostly useful for credits and listings on artist pages. -export function commentatorArtists() { - return compositeFrom({ - annotation: `commentatorArtists`, +export const commentatorArtists = templateCompositeFrom({ + annotation: `commentatorArtists`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'commentary', + mode: input.value('falsy'), + value: input.value([]), + }), - constantDependencies: { - '#composition.findFunction': find.artists, + { + dependencies: ['commentary'], + compute: (continuation, {commentary}) => + continuation({ + '#artistRefs': + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/(?.*?):<\/i>/g)) + .map(({groups: {who}}) => who), + }), }, - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: 'falsy', - value: [], - }), - - { - dependencies: ['commentary'], - compute: ({commentary}, continuation) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, + withResolvedReferenceList({ + list: '#artistRefs', + data: 'artistData', + find: input.value(find.artist), + }).outputs({ + '#resolvedReferenceList': '#artists', + }), + + { + flags: {expose: true}, - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - into: '#artists', - find: '#composition.findFunction', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, + expose: { + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists), }, - ], - }); -} + }, + ], +}); // Compositional utilities -- cgit 1.3.0-6-gf8a5 From 8db50e29b5a1cfddfddf499129b697ecabfadcb0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 13:01:25 -0300 Subject: data: moar WIP composite syntax! --- src/data/things/thing.js | 69 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 33 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a5f0b78d..cff2f498 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -476,15 +476,15 @@ export const withResolvedContribs = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedContribs', - }, + outputs: ['#resolvedContribs'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('from'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedContribs']: [], + }), }), withPropertiesFromList({ @@ -496,9 +496,10 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - into: '#contribs.who', find: input('find'), notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', }), { @@ -510,7 +511,7 @@ export const withResolvedContribs = templateCompositeFrom({ }) { filterMultipleArrays(who, what, (who, _what) => who); return continuation({ - '#composition.into': stitchArrays({who, what}), + ['#resolvedContribs']: stitchArrays({who, what}), }); }, }, @@ -577,14 +578,14 @@ export const withResolvedReference = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReference', - }, + outputs: ['#resolvedReference'], steps: () => [ raiseOutputWithoutDependency({ dependency: input('ref'), - output: input.value({into: null}), + output: input.value({ + ['#resolvedReference']: null, + }), }), exitWithoutDependency({ @@ -611,7 +612,9 @@ export const withResolvedReference = templateCompositeFrom({ return continuation.exit(null); } - return continuation.raise({match}); + return continuation.raiseOutput({ + ['#resolvedReference']: match, + }); }, }, ], @@ -640,9 +643,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ }), }, - outputs: { - into: '#resolvedReferenceList', - }, + outputs: ['#resolvedReferenceList'], steps: () => [ exitWithoutDependency({ @@ -653,7 +654,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ raiseOutputWithoutDependency({ dependency: input('list'), mode: input.value('empty'), - output: input.value({into: []}), + output: input.value({ + ['#resolvedReferenceList']: [], + }), }), { @@ -672,7 +675,9 @@ export const withResolvedReferenceList = templateCompositeFrom({ dependencies: ['#matches'], compute: ({'#matches': matches}, continuation) => (matches.every(match => match) - ? continuation.raise({'#continuation.into': matches}) + ? continuation.raiseOutput({ + ['#resolvedReferenceList']: matches, + }) : continuation()), }, @@ -687,12 +692,16 @@ export const withResolvedReferenceList = templateCompositeFrom({ return continuation.exit([]); case 'filter': - matches = matches.filter(match => match); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); case 'null': - matches = matches.map(match => match ?? null); - return continuation.raise({'#continuation.into': matches}); + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); default: throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); @@ -714,30 +723,24 @@ export const withReverseReferenceList = templateCompositeFrom({ list: input({type: 'string'}), }, - outputs: { - into: '#reverseReferenceList', - }, + outputs: ['#reverseReferenceList'], steps: () => [ exitWithoutDependency({ - dependency: '#composition.data', + dependency: input('data'), value: [], }), { - dependencies: [ - 'this', - '#composition.data', - '#composition.refListProperty', - ], + dependencies: [input.myself(), input('data'), input('list')], compute: ({ - this: thisThing, - '#composition.data': data, - '#composition.refListProperty': refListProperty, + [input.myself()]: thisThing, + [input('data')]: data, + [input('list')]: refListProperty, }, continuation) => continuation({ - '#composition.into': + ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), }), }, -- cgit 1.3.0-6-gf8a5 From cc4bf401f4d1df63ce33191ae82af6327c7da568 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 17:33:50 -0300 Subject: data: fix many validation errors --- src/data/things/thing.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cff2f498..a75ff3e1 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -211,7 +211,7 @@ export function contributionList() { update: {validate: isContributionList}, - steps: () => [ + steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), exposeConstant({value: []}), @@ -468,8 +468,6 @@ export const withResolvedContribs = templateCompositeFrom({ // todo: validate from: input(), - findFunction: input({type: 'function'}), - notFoundMode: input({ validate: oneOf('exit', 'filter', 'null'), defaultValue: 'null', @@ -496,7 +494,7 @@ export const withResolvedContribs = templateCompositeFrom({ withResolvedReferenceList({ list: '#contribs.who', data: 'artistData', - find: input('find'), + find: input.value(find.artist), notFoundMode: input('notFoundMode'), }).outputs({ ['#resolvedReferenceList']: '#contribs.who', @@ -728,7 +726,7 @@ export const withReverseReferenceList = templateCompositeFrom({ steps: () => [ exitWithoutDependency({ dependency: input('data'), - value: [], + value: input.value([]), }), { -- cgit 1.3.0-6-gf8a5 From a2704c0992beb4ddfeb67813d4f8adac0ae6af7d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 18:30:48 -0300 Subject: data: syntax fixes --- src/data/things/thing.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a75ff3e1..265cfe18 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -209,6 +209,8 @@ export function contributionList() { return compositeFrom({ annotation: `contributionList`, + compose: false, + update: {validate: isContributionList}, steps: [ @@ -598,12 +600,12 @@ export const withResolvedReference = templateCompositeFrom({ input('notFoundMode'), ], - compute({ + compute(continuation, { [input('ref')]: ref, [input('data')]: data, [input('find')]: findFunction, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { const match = findFunction(ref, data, {mode: 'quiet'}); if (match === null && notFoundMode === 'exit') { @@ -659,11 +661,11 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: [input('list'), input('data'), input('find')], - compute: ({ + compute: (continuation, { [input('list')]: list, [input('data')]: data, [input('find')]: findFunction, - }, continuation) => + }) => continuation({ '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), }), @@ -671,7 +673,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches'], - compute: ({'#matches': matches}, continuation) => + compute: (continuation, {'#matches': matches}) => (matches.every(match => match) ? continuation.raiseOutput({ ['#resolvedReferenceList']: matches, @@ -681,10 +683,10 @@ export const withResolvedReferenceList = templateCompositeFrom({ { dependencies: ['#matches', input('notFoundMode')], - compute({ + compute(continuation, { ['#matches']: matches, [input('notFoundMode')]: notFoundMode, - }, continuation) { + }) { switch (notFoundMode) { case 'exit': return continuation.exit([]); @@ -732,11 +734,11 @@ export const withReverseReferenceList = templateCompositeFrom({ { dependencies: [input.myself(), input('data'), input('list')], - compute: ({ + compute: (continuation, { [input.myself()]: thisThing, [input('data')]: data, [input('list')]: refListProperty, - }, continuation) => + }) => continuation({ ['#reverseReferenceList']: data.filter(thing => thing[refListProperty].includes(thisThing)), -- cgit 1.3.0-6-gf8a5 From cd73f85962f542f9b44feb2a7616bc0d9aac797b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 11:05:00 -0300 Subject: data: miscellaneous utility updates --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 265cfe18..f63a619d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -363,7 +363,7 @@ export const contribsPresent = templateCompositeFrom({ steps: () => [ withResultOfAvailabilityCheck({ - fromDependency: input('contribs'), + from: input('contribs'), mode: input.value('empty'), }), -- cgit 1.3.0-6-gf8a5 From e3e8a904c24e71f303a1f29c8f1700478d929901 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:37:36 -0300 Subject: data: miscellaneous syntax fixes --- src/data/things/thing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f63a619d..0dea1fa4 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -216,7 +216,7 @@ export function contributionList() { steps: [ withResolvedContribs({from: input.updateValue()}), exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: []}), + exposeConstant({value: input.value([])}), ], }); } @@ -343,7 +343,7 @@ export const singleReference = templateCompositeFrom({ withResolvedReference({ ref: input.updateValue(), data: input('data'), - find: input('findFunction'), + find: input('find'), }), exposeDependency({dependency: '#resolvedReference'}), -- cgit 1.3.0-6-gf8a5 From c1018a0163ae28dc122aad7cb292a5e805c3d25a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:30:49 -0300 Subject: data: fix update collation from steps --- src/data/things/thing.js | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 0dea1fa4..ca1018eb 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -281,24 +281,19 @@ export const referenceList = templateCompositeFrom({ compose: false, inputs: { - class: input(thingClassInput), + class: input.staticValue(thingClassInput), + find: input({type: 'function'}), // todo: validate data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReferenceList(referenceType)}; }, steps: () => [ @@ -326,17 +321,11 @@ export const singleReference = templateCompositeFrom({ data: input(), }, - update: { - dependencies: [ - input.staticValue('class'), - ], - - compute({ - [input.staticValue('class')]: thingClass, - }) { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const {[Thing.referenceType]: referenceType} = thingClass; + return {validate: validateReference(referenceType)}; }, steps: () => [ -- cgit 1.3.0-6-gf8a5 From 4852931ecf2c7ce63851ea6f3a60c9d5b142ae6f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 16:04:06 -0300 Subject: data: minor fixes --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ca1018eb..77f549fe 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -602,7 +602,7 @@ export const withResolvedReference = templateCompositeFrom({ } return continuation.raiseOutput({ - ['#resolvedReference']: match, + ['#resolvedReference']: match ?? null, }); }, }, -- cgit 1.3.0-6-gf8a5 From b4137b02f09761b78c520e5514381cda714dcf6d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:14:11 -0300 Subject: data: fix calls to oneOf instead of is --- src/data/things/thing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 77f549fe..ef547f74 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -7,7 +7,7 @@ import {colors} from '#cli'; import find from '#find'; import {stitchArrays, unique} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {oneOf} from '#validators'; +import {is} from '#validators'; import { compositeFrom, @@ -460,7 +460,7 @@ export const withResolvedContribs = templateCompositeFrom({ from: input(), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'null', }), }, @@ -562,7 +562,7 @@ export const withResolvedReference = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('null', 'exit'), + validate: is('null', 'exit'), defaultValue: 'null', }), }, @@ -627,7 +627,7 @@ export const withResolvedReferenceList = templateCompositeFrom({ find: input({type: 'function'}), notFoundMode: input({ - validate: oneOf('exit', 'filter', 'null'), + validate: is('exit', 'filter', 'null'), defaultValue: 'filter', }), }, -- cgit 1.3.0-6-gf8a5 From b5cfc2a793f22da60606a4dd7387fcf3d3163843 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 25 Sep 2023 14:23:23 -0300 Subject: data: misc. improvements for input validation & infrastructure --- src/data/things/thing.js | 72 ++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 30 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index ef547f74..290be59b 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -25,8 +25,8 @@ import { import { isAdditionalFileList, isBoolean, - isCommentary, isColor, + isCommentary, isContributionList, isDate, isDimensions, @@ -41,12 +41,13 @@ import { validateInstanceOf, validateReference, validateReferenceList, + validateWikiData, } from '#validators'; import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); + static referenceType = Symbol.for('Thing.referenceType'); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); @@ -283,10 +284,8 @@ export const referenceList = templateCompositeFrom({ inputs: { class: input.staticValue(thingClassInput), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), - - // todo: validate - data: input(), }, update: ({ @@ -316,9 +315,7 @@ export const singleReference = templateCompositeFrom({ inputs: { class: input(thingClassInput), find: input({type: 'function'}), - - // todo: validate - data: input(), + data: inputWikiData({allowMixedTypes: false}), }, update: ({ @@ -347,7 +344,10 @@ export const contribsPresent = templateCompositeFrom({ compose: false, inputs: { - contribs: input({type: 'string'}), + contribs: input.staticDependency({ + validate: isContributionList, + acceptsNull: true, + }), }, steps: () => [ @@ -371,9 +371,7 @@ export const reverseReferenceList = templateCompositeFrom({ compose: false, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, @@ -448,6 +446,21 @@ export const commentatorArtists = templateCompositeFrom({ // Compositional utilities +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData(referenceType), + acceptsNull: true, + }); +} + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -456,8 +469,10 @@ export const withResolvedContribs = templateCompositeFrom({ annotation: `withResolvedContribs`, inputs: { - // todo: validate - from: input(), + from: input({ + validate: isContributionList, + acceptsNull: true, + }), notFoundMode: input({ validate: is('exit', 'filter', 'null'), @@ -514,10 +529,12 @@ export const exitWithoutContribs = templateCompositeFrom({ annotation: `exitWithoutContribs`, inputs: { - // todo: validate - contribs: input(), + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), - value: input({null: true}), + value: input({defaultValue: null}), }, steps: () => [ @@ -553,12 +570,9 @@ export const withResolvedReference = templateCompositeFrom({ annotation: `withResolvedReference`, inputs: { - // todo: validate - ref: input(), - - // todo: validate - data: input(), + ref: input({type: 'string', acceptsNull: true}), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -618,12 +632,12 @@ export const withResolvedReferenceList = templateCompositeFrom({ annotation: `withResolvedReferenceList`, inputs: { - // todo: validate - list: input(), - - // todo: validate - data: input(), + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + data: inputWikiData({allowMixedTypes: false}), find: input({type: 'function'}), notFoundMode: input({ @@ -706,9 +720,7 @@ export const withReverseReferenceList = templateCompositeFrom({ annotation: `withReverseReferenceList`, inputs: { - // todo: validate - data: input(), - + data: inputWikiData({allowMixedTypes: false}), list: input({type: 'string'}), }, -- cgit 1.3.0-6-gf8a5 From f7376bb5eb2671de7242872ec0c4615b5e244aba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:12:56 -0300 Subject: data: misc minor fixes --- src/data/things/thing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 290be59b..f1302e17 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -456,7 +456,7 @@ export function inputWikiData({ allowMixedTypes = false, } = {}) { return input({ - validate: validateWikiData(referenceType), + validate: validateWikiData({referenceType, allowMixedTypes}), acceptsNull: true, }); } -- cgit 1.3.0-6-gf8a5 From ab7591e45e7e31b4e2c0e2f81e224672145993fa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:01:21 -0300 Subject: data, test: refactor utilities into own file Primarily for more precies test coverage mapping, but also to make navigation a bit easier and consolidate complex functions with lots of imports out of the same space as other, more simple or otherwise specialized files. --- src/data/things/thing.js | 713 +---------------------------------------------- 1 file changed, 2 insertions(+), 711 deletions(-) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index f1302e17..a47f8506 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -1,48 +1,9 @@ -// Thing: base class for wiki data types, providing wiki-specific utility -// functions on top of essential CacheableObject behavior. +// Thing: base class for wiki data types, providing interfaces generally useful +// to all wiki data objects on top of foundational CacheableObject behavior. import {inspect} from 'node:util'; import {colors} from '#cli'; -import find from '#find'; -import {stitchArrays, unique} from '#sugar'; -import {filterMultipleArrays, getKebabCase} from '#wiki-data'; -import {is} from '#validators'; - -import { - compositeFrom, - exitWithoutDependency, - exposeConstant, - exposeDependency, - exposeDependencyOrContinue, - input, - raiseOutputWithoutDependency, - templateCompositeFrom, - withResultOfAvailabilityCheck, - withPropertiesFromList, -} from '#composite'; - -import { - isAdditionalFileList, - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDimensions, - isDirectory, - isDuration, - isFileExtension, - isName, - isString, - isType, - isURL, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, - validateWikiData, -} from '#validators'; import CacheableObject from './cacheable-object.js'; @@ -77,673 +38,3 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } } - -// Property descriptor templates -// -// Regularly reused property descriptors, for ease of access and generally -// duplicating less code across wiki data types. These are specialized utility -// functions, so check each for how its own arguments behave! - -export function name(defaultName) { - return { - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }; -} - -export function color() { - return { - flags: {update: true, expose: true}, - update: {validate: isColor}, - }; -} - -export function directory() { - return { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }; -} - -export function urls() { - return { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }; -} - -// A file extension! Or the default, if provided when calling this. -export function fileExtension(defaultFileExtension = null) { - return { - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }; -} - -// Plain ol' image dimensions. This is a two-item array of positive integers, -// corresponding to width and height respectively. -export function dimensions() { - return { - flags: {update: true, expose: true}, - update: {validate: isDimensions}, - }; -} - -// Duration! This is a number of seconds, possibly floating point, always -// at minimum zero. -export function duration() { - return { - flags: {update: true, expose: true}, - update: {validate: isDuration}, - }; -} - -// Straightforward flag descriptor for a variety of property purposes. -// Provide a default value, true or false! -export function flag(defaultValue = false) { - // TODO: ^ Are you actually kidding me - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; -} - -// General date type, used as the descriptor for a bunch of properties. -// This isn't dynamic though - it won't inherit from a date stored on -// another object, for example. -export function simpleDate() { - return { - flags: {update: true, expose: true}, - update: {validate: isDate}, - }; -} - -// General string type. This should probably generally be avoided in favor -// of more specific validation, but using it makes it easy to find where we -// might want to improve later, and it's a useful shorthand meanwhile. -export function simpleString() { - return { - flags: {update: true, expose: true}, - update: {validate: isString}, - }; -} - -// External function. These should only be used as dependencies for other -// properties, so they're left unexposed. -export function externalFunction() { - return { - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }; -} - -// Strong 'n sturdy contribution list, rolling a list of references (provided -// as this property's update value) and the resolved results (as get exposed) -// into one property. Update value will look something like this: -// -// [ -// {who: 'Artist Name', what: 'Viola'}, -// {who: 'artist:john-cena', what: null}, -// ... -// ] -// -// ...typically as processed from YAML, spreadsheet, or elsewhere. -// Exposes as the same, but with the "who" replaced with matches found in -// artistData - which means this always depends on an `artistData` property -// also existing on this object! -// -export function contributionList() { - return compositeFrom({ - annotation: `contributionList`, - - compose: false, - - update: {validate: isContributionList}, - - steps: [ - withResolvedContribs({from: input.updateValue()}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: input.value([])}), - ], - }); -} - -// Artist commentary! Generally present on tracks and albums. -export function commentary() { - return { - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }; -} - -// This is a somewhat more involved data structure - it's for additional -// or "bonus" files associated with albums or tracks (or anything else). -// It's got this form: -// -// [ -// {title: 'Booklet', files: ['Booklet.pdf']}, -// { -// title: 'Wallpaper', -// description: 'Cool Wallpaper!', -// files: ['1440x900.png', '1920x1080.png'] -// }, -// {title: 'Alternate Covers', description: null, files: [...]}, -// ... -// ] -// -export function additionalFiles() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }; -} - -const thingClassInput = { - validate(thingClass) { - isType(thingClass, 'function'); - - if (!Object.hasOwn(thingClass, Thing.referenceType)) { - throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`); - } - - return true; - }, -}; - -// A reference list! Keep in mind this is for general references to wiki -// objects of (usually) other Thing subclasses, not specifically leitmotif -// references in tracks (although that property uses referenceList too!). -// -// The underlying function validateReferenceList expects a string like -// 'artist' or 'track', but this utility keeps from having to hard-code the -// string in multiple places by referencing the value saved on the class -// instead. -export const referenceList = templateCompositeFrom({ - annotation: `referenceList`, - - compose: false, - - inputs: { - class: input.staticValue(thingClassInput), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReferenceList(referenceType)}; - }, - - steps: () => [ - withResolvedReferenceList({ - list: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReferenceList'}), - ], -}); - -// Corresponding function for a single reference. -export const singleReference = templateCompositeFrom({ - annotation: `singleReference`, - - compose: false, - - inputs: { - class: input(thingClassInput), - find: input({type: 'function'}), - data: inputWikiData({allowMixedTypes: false}), - }, - - update: ({ - [input.staticValue('class')]: thingClass, - }) => { - const {[Thing.referenceType]: referenceType} = thingClass; - return {validate: validateReference(referenceType)}; - }, - - steps: () => [ - withResolvedReference({ - ref: input.updateValue(), - data: input('data'), - find: input('find'), - }), - - exposeDependency({dependency: '#resolvedReference'}), - ], -}); - -// Nice 'n simple shorthand for an exposed-only flag which is true when any -// contributions are present in the specified property. -export const contribsPresent = templateCompositeFrom({ - annotation: `contribsPresent`, - - compose: false, - - inputs: { - contribs: input.staticDependency({ - validate: isContributionList, - acceptsNull: true, - }), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('contribs'), - mode: input.value('empty'), - }), - - exposeDependency({dependency: '#availability'}), - ], -}); - -// Neat little shortcut for "reversing" the reference lists stored on other -// things - for example, tracks specify a "referenced tracks" property, and -// you would use this to compute a corresponding "referenced *by* tracks" -// property. Naturally, the passed ref list property is of the things in the -// wiki data provided, not the requesting Thing itself. -export const reverseReferenceList = templateCompositeFrom({ - annotation: `reverseReferenceList`, - - compose: false, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: input('data'), - list: input('list'), - }), - - exposeDependency({dependency: '#reverseReferenceList'}), - ], -}); - -// General purpose wiki data constructor, for properties like artistData, -// trackData, etc. -export function wikiData(thingClass) { - return { - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }; -} - -// This one's kinda tricky: it parses artist "references" from the -// commentary content, and finds the matching artist for each reference. -// This is mostly useful for credits and listings on artist pages. -export const commentatorArtists = templateCompositeFrom({ - annotation: `commentatorArtists`, - - compose: false, - - steps: () => [ - exitWithoutDependency({ - dependency: 'commentary', - mode: input.value('falsy'), - value: input.value([]), - }), - - { - dependencies: ['commentary'], - compute: (continuation, {commentary}) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, - - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - find: input.value(find.artist), - }).outputs({ - '#resolvedReferenceList': '#artists', - }), - - { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, - }, - ], -}); - -// Compositional utilities - -// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] -// value because classes aren't initialized by when templateCompositeFrom gets -// called (see: circular imports). So the reference types have to be hard-coded, -// which somewhat defeats the point of storing them on the class in the first -// place... -export function inputWikiData({ - referenceType = '', - allowMixedTypes = false, -} = {}) { - return input({ - validate: validateWikiData({referenceType, allowMixedTypes}), - acceptsNull: true, - }); -} - -// Resolves the contribsByRef contained in the provided dependency, -// providing (named by the second argument) the result. "Resolving" -// means mapping the "who" reference of each contribution to an artist -// object, and filtering out those whose "who" doesn't match any artist. -export const withResolvedContribs = templateCompositeFrom({ - annotation: `withResolvedContribs`, - - inputs: { - from: input({ - validate: isContributionList, - acceptsNull: true, - }), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedContribs'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('from'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedContribs']: [], - }), - }), - - withPropertiesFromList({ - list: input('from'), - properties: input.value(['who', 'what']), - prefix: input.value('#contribs'), - }), - - withResolvedReferenceList({ - list: '#contribs.who', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input('notFoundMode'), - }).outputs({ - ['#resolvedReferenceList']: '#contribs.who', - }), - - { - dependencies: ['#contribs.who', '#contribs.what'], - - compute(continuation, { - ['#contribs.who']: who, - ['#contribs.what']: what, - }) { - filterMultipleArrays(who, what, (who, _what) => who); - return continuation({ - ['#resolvedContribs']: stitchArrays({who, what}), - }); - }, - }, - ], -}); - -// Shorthand for exiting if the contribution list (usually a property's update -// value) resolves to empty - ensuring that the later computed results are only -// returned if these contributions are present. -export const exitWithoutContribs = templateCompositeFrom({ - annotation: `exitWithoutContribs`, - - inputs: { - contribs: input({ - validate: isContributionList, - acceptsNull: true, - }), - - value: input({defaultValue: null}), - }, - - steps: () => [ - withResolvedContribs({ - from: input('contribs'), - }), - - withResultOfAvailabilityCheck({ - from: '#resolvedContribs', - mode: input.value('empty'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null, or, if notFoundMode is set to 'exit', if the find -// function doesn't match anything for the reference. Otherwise, the data -// object is provided on the output dependency; or null, if the reference -// doesn't match anything or itself was null to begin with. -export const withResolvedReference = templateCompositeFrom({ - annotation: `withResolvedReference`, - - inputs: { - ref: input({type: 'string', acceptsNull: true}), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('null', 'exit'), - defaultValue: 'null', - }), - }, - - outputs: ['#resolvedReference'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('ref'), - output: input.value({ - ['#resolvedReference']: null, - }), - }), - - exitWithoutDependency({ - dependency: input('data'), - }), - - { - dependencies: [ - input('ref'), - input('data'), - input('find'), - input('notFoundMode'), - ], - - compute(continuation, { - [input('ref')]: ref, - [input('data')]: data, - [input('find')]: findFunction, - [input('notFoundMode')]: notFoundMode, - }) { - const match = findFunction(ref, data, {mode: 'quiet'}); - - if (match === null && notFoundMode === 'exit') { - return continuation.exit(null); - } - - return continuation.raiseOutput({ - ['#resolvedReference']: match ?? null, - }); - }, - }, - ], -}); - -// Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). -export const withResolvedReferenceList = templateCompositeFrom({ - annotation: `withResolvedReferenceList`, - - inputs: { - list: input({ - validate: validateArrayItems(isString), - acceptsNull: true, - }), - - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), - }, - - outputs: ['#resolvedReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - raiseOutputWithoutDependency({ - dependency: input('list'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedReferenceList']: [], - }), - }), - - { - dependencies: [input('list'), input('data'), input('find')], - compute: (continuation, { - [input('list')]: list, - [input('data')]: data, - [input('find')]: findFunction, - }) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, - - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedReferenceList']: matches, - }) - : continuation()), - }, - - { - dependencies: ['#matches', input('notFoundMode')], - compute(continuation, { - ['#matches']: matches, - [input('notFoundMode')]: notFoundMode, - }) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.filter(match => match), - }); - - case 'null': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.map(match => match ?? null), - }); - - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, - ], -}); - -// Check out the info on reverseReferenceList! -// This is its composable form. -export const withReverseReferenceList = templateCompositeFrom({ - annotation: `withReverseReferenceList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseReferenceList'], - - steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: thisThing, - [input('data')]: data, - [input('list')]: refListProperty, - }) => - continuation({ - ['#reverseReferenceList']: - data.filter(thing => thing[refListProperty].includes(thisThing)), - }), - }, - ], -}); -- cgit 1.3.0-6-gf8a5 From 4e2dae523e7bf8b49272bd6afcba86a8157af4a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 18 Oct 2023 14:25:27 -0300 Subject: data: add [Thing.friendlyName] property to some Thing subclasses --- src/data/things/thing.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/data/things/thing.js') diff --git a/src/data/things/thing.js b/src/data/things/thing.js index a47f8506..def7e914 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -9,6 +9,7 @@ import CacheableObject from './cacheable-object.js'; export default class Thing extends CacheableObject { static referenceType = Symbol.for('Thing.referenceType'); + static friendlyName = Symbol.for(`Thing.friendlyName`); static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); -- cgit 1.3.0-6-gf8a5