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