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/composite.js | 1179 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1179 insertions(+) create mode 100644 src/data/things/composite.js (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js new file mode 100644 index 00000000..1be60cd1 --- /dev/null +++ b/src/data/things/composite.js @@ -0,0 +1,1179 @@ +// 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! +// + +import {empty, filterProperties, openAggregate} from '#sugar'; + +import Thing from './thing.js'; + +export {compositeFrom as from}; +function compositeFrom(firstArg, secondArg) { + const debug = fn => { + if (compositeFrom.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) +// +export function debug(fn) { + compositeFrom.debug = true; + const value = fn(); + compositeFrom.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 {_export as export}; +function _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. +// +export function 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. +export function 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! +// +export function 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! +export function exposeDependencyOrContinue(dependency, { + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + 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! +export function exposeUpdateValueOrContinue({ + mode = 'null', +} = {}) { + return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + 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 has failed. +// This is for internal use only - use `earlyExitWithoutDependency` or +// `earlyExitWIthoutUpdateValue` instead. +export function earlyExitIfAvailabilityCheckFailed({ + availability = '#availability', + value = null, +} = {}) { + return compositeFrom(`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! +export function earlyExitWithoutDependency(dependency, { + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function earlyExitWithoutUpdateValue({ + mode = 'null', + value = null, +} = {}) { + return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), + earlyExitIfAvailabilityCheckFailed({value}), + ]); +} + +// Raises if a dependency isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export function raiseWithoutDependency(dependency, { + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + 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! +export function raiseWithoutUpdateValue({ + mode = 'null', + map = {}, + raise = {}, +} = {}) { + return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + 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. +export function 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. +export function withResolvedReference({ + ref, + data, + to, + find: findFunction, + earlyExitIfNotFound = false, +}) { + return compositeFrom(`Thing.composite.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}); + }, + }, + ]); +} + +// Check out the info on Thing.common.reverseReferenceList! +// This is its composable form. +export function withReverseReferenceList({ + data, + to = '#reverseReferenceList', + refList: refListProperty, +}) { + 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 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/composite.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1be60cd1..bf2d11ea 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -331,6 +331,7 @@ // syntax as for other compositional steps, and it'll work out cleanly! // +import find from '#find'; import {empty, filterProperties, openAggregate} from '#sugar'; import Thing from './thing.js'; @@ -1102,20 +1103,33 @@ export function raiseWithoutUpdateValue({ // 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 { - annotation: `Thing.composite.withResolvedContribs`, - flags: {expose: true, compose: true}, + return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ + Thing.composite.earlyExitWithoutDependency('artistData', { + value: [], + }), - expose: { + Thing.composite.raiseWithoutDependency(from, { + mode: 'empty', + map: {to}, + raise: {to: []}, + }), + + { dependencies: ['artistData'], mapDependencies: {from}, mapContinuation: {to}, compute: ({artistData, from}, continuation) => continuation({ - to: Thing.findArtistsFromContribs(from, artistData), + to: + from + .map(({who, what}) => ({ + who: find.artist(who, artistData, {mode: 'quiet'}), + what, + })) + .filter(({who}) => who), }), }, - }; + ]); } // Resolves a reference by using the provided find function to match it -- cgit 1.3.0-6-gf8a5 From 117f1e6b707dfe102b968e421b21906d03100dc8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 14:57:16 -0300 Subject: data: new withResolvedReferenceList utility --- src/data/things/composite.js | 107 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 19 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bf2d11ea..18a5f434 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,15 @@ +import find from '#find'; +import {filterMultipleArrays} from '#wiki-data'; + +import { + empty, + filterProperties, + openAggregate, + stitchArrays, +} from '#sugar'; + +import Thing from './thing.js'; + // 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 @@ -331,11 +343,6 @@ // syntax as for other compositional steps, and it'll work out cleanly! // -import find from '#find'; -import {empty, filterProperties, openAggregate} from '#sugar'; - -import Thing from './thing.js'; - export {compositeFrom as from}; function compositeFrom(firstArg, secondArg) { const debug = fn => { @@ -1104,10 +1111,6 @@ export function raiseWithoutUpdateValue({ // object, and filtering out those whose "who" doesn't match any artist. export function withResolvedContribs({from, to}) { return Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.earlyExitWithoutDependency('artistData', { - value: [], - }), - Thing.composite.raiseWithoutDependency(from, { mode: 'empty', map: {to}, @@ -1115,20 +1118,32 @@ export function withResolvedContribs({from, to}) { }), { - dependencies: ['artistData'], mapDependencies: {from}, - mapContinuation: {to}, - compute: ({artistData, from}, continuation) => + compute: ({from}, continuation) => continuation({ - to: - from - .map(({who, what}) => ({ - who: find.artist(who, artistData, {mode: 'quiet'}), - what, - })) - .filter(({who}) => who), + '#whoByRef': from.map(({who}) => who), + '#what': from.map(({what}) => what), }), }, + + withResolvedReferenceList({ + refList: '#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}), + }); + }, + }, ]); } @@ -1168,6 +1183,60 @@ export function withResolvedReference({ ]); } +// 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({ + refList, + data, + to, + find: findFunction, + notFoundMode = 'filter', +}) { + if (!['filter', 'exit', 'null'].includes(notFoundMode)) { + throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); + } + + return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + earlyExitWithoutDependency(data, {value: []}), + + raiseWithoutDependency(refList, { + map: {to}, + raise: [], + mode: 'empty', + }), + + { + options: {findFunction, notFoundMode}, + mapDependencies: {refList, data}, + mapContinuation: {matches: to}, + + compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + const matches = + refList.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 contination.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({ -- cgit 1.3.0-6-gf8a5 From 91624e5d61a1473e143bad8860c8c2ccffec38fe Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 15:10:48 -0300 Subject: data: misc. eslint-caught fixes in composite.js --- src/data/things/composite.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 18a5f434..4f1abdb4 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,3 +1,6 @@ +import {inspect} from 'node:util'; + +import {color} from '#cli'; import find from '#find'; import {filterMultipleArrays} from '#wiki-data'; @@ -482,11 +485,11 @@ function compositeFrom(firstArg, secondArg) { if (!baseComposes) { if (baseUpdates) { if (!anyStepsTransform) { - push(new TypeError(`Expected at least one step to transform`)); + aggregate.push(new TypeError(`Expected at least one step to transform`)); } } else { if (!anyStepsCompute) { - push(new TypeError(`Expected at least one step to compute`)); + aggregate.push(new TypeError(`Expected at least one step to compute`)); } } } @@ -1087,8 +1090,8 @@ export function raiseWithoutUpdateValue({ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { - mapDependencies: {availability}, - compute: ({availability}, continuation) => + dependencies: ['#availability'], + compute: ({'#availability': availability}, continuation) => (availability ? continuation.raise() : continuation()), @@ -1214,7 +1217,7 @@ export function withResolvedReferenceList({ mapContinuation: {matches: to}, compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { - const matches = + let matches = refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { @@ -1224,7 +1227,7 @@ export function withResolvedReferenceList({ switch (notFoundMode) { case 'filter': matches = matches.filter(value => value !== null); - return contination.raise({matches}); + return continuation.raise({matches}); case 'exit': return continuation.exit([]); -- 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/composite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4f1abdb4..e930e228 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1207,7 +1207,7 @@ export function withResolvedReferenceList({ raiseWithoutDependency(refList, { map: {to}, - raise: [], + raise: {to: []}, mode: 'empty', }), -- 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/composite.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e930e228..7f3463cf 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1130,7 +1130,7 @@ export function withResolvedContribs({from, to}) { }, withResolvedReferenceList({ - refList: '#whoByRef', + list: '#whoByRef', data: 'artistData', to: '#who', find: find.artist, @@ -1192,7 +1192,7 @@ export function withResolvedReference({ // 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({ - refList, + list, data, to, find: findFunction, @@ -1205,7 +1205,7 @@ export function withResolvedReferenceList({ return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), - raiseWithoutDependency(refList, { + raiseWithoutDependency(list, { map: {to}, raise: {to: []}, mode: 'empty', @@ -1213,12 +1213,12 @@ export function withResolvedReferenceList({ { options: {findFunction, notFoundMode}, - mapDependencies: {refList, data}, + mapDependencies: {list, data}, mapContinuation: {matches: to}, - compute({refList, data, '#options': {findFunction, notFoundMode}}, continuation) { + compute({list, data, '#options': {findFunction, notFoundMode}}, continuation) { let matches = - refList.map(ref => findFunction(ref, data, {mode: 'quiet'})); + list.map(ref => findFunction(ref, data, {mode: 'quiet'})); if (!matches.includes(null)) { return continuation.raise({matches}); -- 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/composite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7f3463cf..138814d9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1244,8 +1244,8 @@ export function withResolvedReferenceList({ // This is its composable form. export function withReverseReferenceList({ data, + list: refListProperty, to = '#reverseReferenceList', - refList: refListProperty, }) { return compositeFrom(`Thing.common.reverseReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), -- 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/composite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 138814d9..d3f76b11 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1160,8 +1160,8 @@ export function withResolvedContribs({from, to}) { export function withResolvedReference({ ref, data, - to, find: findFunction, + to = '#resolvedReference', earlyExitIfNotFound = false, }) { return compositeFrom(`Thing.composite.withResolvedReference`, [ @@ -1194,8 +1194,8 @@ export function withResolvedReference({ export function withResolvedReferenceList({ list, data, - to, find: findFunction, + to = '#resolvedReferenceList', notFoundMode = 'filter', }) { if (!['filter', 'exit', 'null'].includes(notFoundMode)) { -- cgit 1.3.0-6-gf8a5 From 659620b7522d0e36ca15a54716b46d83f0bfc4f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:45:56 -0300 Subject: data: move composite helper functions to top function scope --- src/data/things/composite.js | 390 +++++++++++++++++++++---------------------- 1 file changed, 195 insertions(+), 195 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index d3f76b11..805331a9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -496,255 +496,255 @@ function compositeFrom(firstArg, secondArg) { aggregate.close(); - const constructedDescriptor = {}; - - if (annotation) { - constructedDescriptor.annotation = annotation; - } + 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; + } + } - constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, - }; + if (options) { + filteredDependencies['#options'] = options; + } - if (baseUpdates) { - constructedDescriptor.update = base.update; + return filteredDependencies; } - 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; } - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } + const assignDependencies = {}; - const assignDependencies = {}; + for (const [from, to] of Object.entries(mapContinuation)) { + assignDependencies[to] = continuationAssignment[from] ?? null; + } - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; - } + return assignDependencies; + } - return assignDependencies; - } + function _prepareContinuation(callingTransformForThisStep) { + const continuationStorage = { + returnedWith: null, + providedDependencies: undefined, + providedValue: undefined, + }; - 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; + }; - const continuation = + if (baseComposes) { + const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'continuation'; + continuationStorage.returnedWith = returnWith; 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}; + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { - const expectingTransform = initialValue !== noTransformSymbol; + return {continuation, continuationStorage}; + } - let valueSoFar = - (expectingTransform - ? initialValue - : undefined); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); - const availableDependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); - } else { - debug(() => color.bright(`begin composition - not transforming`)); - } + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + const availableDependencies = {...initialDependencies}; - debug(() => [ - `step #${i+1}` + - (isBase - ? ` (base):` - : ` of ${steps.length}:`), - step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const expose = - (step.flags - ? step.expose - : step); + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const callingTransformForThisStep = - expectingTransform && expose.transform; + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); - const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); + const expose = + (step.flags + ? step.expose + : step); - debug(() => [ - `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const callingTransformForThisStep = + expectingTransform && expose.transform; - const result = - (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); - if (result !== continuationSymbol) { - debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + debug(() => [ + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, + `with dependencies:`, filteredDependencies]); - if (baseComposes) { - throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); - } + const result = + (callingTransformForThisStep + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.compute(filteredDependencies, continuation)); - debug(() => color.bright(`end composition - exit (inferred)`)); + if (result !== continuationSymbol) { + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - return result; + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - const {returnedWith} = continuationStorage; + debug(() => color.bright(`end composition - exit (inferred)`)); + + return result; + } - if (returnedWith === 'exit') { - const {providedValue} = continuationStorage; + const {returnedWith} = continuationStorage; - debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (baseComposes) { - return continuationIfApplicable.exit(providedValue); - } else { - return providedValue; - } + 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); - } - } + const {providedValue, providedDependencies} = continuationStorage; - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); - } else { - parts.push(`(no deps)`); - } + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); + + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - if (empty(parts)) { - return base; + 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 { - return [base + ' ->', ...parts]; + parts.push(`value:`, providedValue); } - }); + } - switch (returnedWith) { - case 'raise': - debug(() => - (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); - return continuationIfApplicable(...continuationArgs); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - 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; - } + 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 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 transformFn = (value, initialDependencies, continuationIfApplicable) => -- cgit 1.3.0-6-gf8a5 From 1594885c506ed76c0f4f1dc58ab14a4fabba6be5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 6 Sep 2023 17:46:36 -0300 Subject: data: don't pass dependencies without expose properties --- src/data/things/composite.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 805331a9..21cf365f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -501,6 +501,10 @@ function compositeFrom(firstArg, secondArg) { mapDependencies, options, }) { + if (!dependencies && !mapDependencies && !options) { + return null; + } + const filteredDependencies = (dependencies ? filterProperties(availableDependencies, dependencies) @@ -629,8 +633,12 @@ function compositeFrom(firstArg, secondArg) { const result = (callingTransformForThisStep - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.compute(filteredDependencies, continuation)); + ? (filteredDependencies + ? expose.transform(valueSoFar, filteredDependencies, continuation) + : expose.transform(valueSoFar, continuation)) + : (filteredDependencies + ? expose.compute(filteredDependencies, continuation) + : expose.compute(continuation))); if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -998,7 +1006,7 @@ export function exposeUpdateValueOrContinue({ }, { - transform: (value, {}, continuation) => + transform: (value, continuation) => continuation.exit(value), }, ]); -- 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/composite.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 21cf365f..bcc52a2a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -808,7 +808,7 @@ function _export(mapping) { const mappingEntries = Object.entries(mapping); return { - annotation: `Thing.composite.export`, + annotation: `export`, flags: {expose: true, compose: true}, expose: { @@ -853,7 +853,7 @@ export function exposeDependency(dependency, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeDependency`, + annotation: `exposeDependency`, flags: {expose: true, update: !!update}, expose: { @@ -878,7 +878,7 @@ export function exposeConstant(value, { update = false, } = {}) { return { - annotation: `Thing.composite.exposeConstant`, + annotation: `exposeConstant`, flags: {expose: true, update: !!update}, expose: { @@ -934,7 +934,7 @@ export function withResultOfAvailabilityCheck({ if (fromDependency) { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`, + annotation: `withResultOfAvailabilityCheck.fromDependency`, flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, @@ -946,7 +946,7 @@ export function withResultOfAvailabilityCheck({ }; } else { return { - annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`, + annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { mapContinuation: {to}, @@ -963,7 +963,7 @@ export function withResultOfAvailabilityCheck({ export function exposeDependencyOrContinue(dependency, { mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [ + return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, mode, @@ -991,7 +991,7 @@ export function exposeDependencyOrContinue(dependency, { export function exposeUpdateValueOrContinue({ mode = 'null', } = {}) { - return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [ + return compositeFrom(`exposeUpdateValueOrContinue`, [ withResultOfAvailabilityCheck({ fromUpdateValue: true, mode, @@ -1019,7 +1019,7 @@ export function earlyExitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1042,7 +1042,7 @@ export function earlyExitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1054,7 +1054,7 @@ export function earlyExitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [ + return compositeFrom(`earlyExitWithoutDependency`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), earlyExitIfAvailabilityCheckFailed({value}), ]); @@ -1067,7 +1067,7 @@ export function raiseWithoutDependency(dependency, { map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutDependency`, [ + return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), { @@ -1094,7 +1094,7 @@ export function raiseWithoutUpdateValue({ map = {}, raise = {}, } = {}) { - return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [ + return compositeFrom(`raiseWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), { @@ -1121,8 +1121,8 @@ export function raiseWithoutUpdateValue({ // 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 Thing.composite.from(`Thing.composite.withResolvedContribs`, [ - Thing.composite.raiseWithoutDependency(from, { + return compositeFrom(`withResolvedContribs`, [ + raiseWithoutDependency(from, { mode: 'empty', map: {to}, raise: {to: []}, @@ -1172,7 +1172,7 @@ export function withResolvedReference({ to = '#resolvedReference', earlyExitIfNotFound = false, }) { - return compositeFrom(`Thing.composite.withResolvedReference`, [ + return compositeFrom(`withResolvedReference`, [ raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}), earlyExitWithoutDependency(data), @@ -1210,7 +1210,7 @@ export function withResolvedReferenceList({ throw new TypeError(`Expected notFoundMode to be filter, exit, or null`); } - return compositeFrom(`Thing.composite.withResolvedReferenceList`, [ + return compositeFrom(`withResolvedReferenceList`, [ earlyExitWithoutDependency(data, {value: []}), raiseWithoutDependency(list, { -- 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/composite.js | 167 ------------------------------------------- 1 file changed, 167 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index bcc52a2a..7cba1e97 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,18 +1,13 @@ import {inspect} from 'node:util'; import {color} from '#cli'; -import find from '#find'; -import {filterMultipleArrays} from '#wiki-data'; import { empty, filterProperties, openAggregate, - stitchArrays, } from '#sugar'; -import Thing from './thing.js'; - // 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 @@ -794,8 +789,6 @@ export function debug(fn) { 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 @@ -834,8 +827,6 @@ function _export(mapping) { }; } -// -- 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 @@ -1113,161 +1104,3 @@ export function raiseWithoutUpdateValue({ }, ]); } - -// -- 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. -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/composite.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7cba1e97..84a98290 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1004,13 +1004,13 @@ export function exposeUpdateValueOrContinue({ } // Early exits if an availability check has failed. -// This is for internal use only - use `earlyExitWithoutDependency` or -// `earlyExitWIthoutUpdateValue` instead. -export function earlyExitIfAvailabilityCheckFailed({ +// This is for internal use only - use `exitWithoutDependency` or +// `exitWithoutUpdateValue` instead. +export function exitIfAvailabilityCheckFailed({ availability = '#availability', value = null, } = {}) { - return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [ + return compositeFrom(`exitIfAvailabilityCheckFailed`, [ { mapDependencies: {availability}, compute: ({availability}, continuation) => @@ -1029,25 +1029,25 @@ export function earlyExitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutDependency(dependency, { +export function exitWithoutDependency(dependency, { mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } // Early exits if this property's update value isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function earlyExitWithoutUpdateValue({ +export function exitWithoutUpdateValue({ mode = 'null', value = null, } = {}) { - return compositeFrom(`earlyExitWithoutDependency`, [ + return compositeFrom(`exitWithoutUpdateValue`, [ withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - earlyExitIfAvailabilityCheckFailed({value}), + exitIfAvailabilityCheckFailed({value}), ]); } -- 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/composite.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 84a98290..b9cd6bfb 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -104,7 +104,7 @@ import { // on a provided dependency name, and then providing a result in another // also-provided dependency name: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -126,10 +126,11 @@ import { // // And how you might work that into a composition: // -// static Track[Thing.getPropertyDescriptors].coverArtists = -// Thing.composite.from([ -// Track.composite.doSomethingWhichMightEarlyExit(), -// Thing.composite.withResolvedContribs({ +// Track.coverArtists = +// compositeFrom([ +// doSomethingWhichMightEarlyExit(), +// +// withResolvedContribs({ // from: 'coverArtistContribsByRef', // to: '#coverArtistContribs', // }), @@ -138,9 +139,8 @@ import { // flags: {expose: true}, // expose: { // dependencies: ['#coverArtistContribs'], -// compute({'#coverArtistContribs': coverArtistContribs}) { -// return coverArtistContribs.map(({who}) => who); -// }, +// compute: ({'#coverArtistContribs': coverArtistContribs}) => +// coverArtistContribs.map(({who}) => who), // }, // }, // ]); @@ -169,7 +169,7 @@ import { // Consider the `withResolvedContribs` example adjusted to make use of // two of these options below: // -// static Thing.composite.withResolvedContribs = ({ +// withResolvedContribs = ({ // from: contribsByRefDependency, // to: outputDependency, // }) => ({ @@ -194,7 +194,7 @@ import { // With a little destructuring and restructuring JavaScript sugar, the // above can be simplified some more: // -// static Thing.composite.withResolvedContribs = ({from, to}) => ({ +// withResolvedContribs = ({from, to}) => ({ // flags: {expose: true, compose: true}, // expose: { // dependencies: ['artistData'], @@ -281,7 +281,7 @@ import { // // 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 +// the compositeFrom() 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 @@ -341,8 +341,7 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // -export {compositeFrom as from}; -function compositeFrom(firstArg, secondArg) { +export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -373,7 +372,7 @@ function compositeFrom(firstArg, secondArg) { const aggregate = openAggregate({ message: - `Errors preparing Thing.composite.from() composition` + + `Errors preparing composition` + (annotation ? ` (${annotation})` : ''), }); @@ -780,9 +779,9 @@ function compositeFrom(firstArg, secondArg) { // t.same(thing.someProp, value) // // With debugging: -// t.same(Thing.composite.debug(() => thing.someProp), value) +// t.same(debugComposite(() => thing.someProp), value) // -export function debug(fn) { +export function debugComposite(fn) { compositeFrom.debug = true; const value = fn(); compositeFrom.debug = false; -- cgit 1.3.0-6-gf8a5 From ba04498715423c165cdb254676cc211c48b7c8ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 10:38:16 -0300 Subject: data: remove unused export() raising utility --- src/data/things/composite.js | 38 -------------------------------------- 1 file changed, 38 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b9cd6bfb..f59e7d75 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -788,44 +788,6 @@ export function debugComposite(fn) { return value; } -// 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 {_export as export}; -function _export(mapping) { - const mappingEntries = Object.entries(mapping); - - return { - annotation: `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); - } - }, - }; -} - // 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 -- 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/composite.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f59e7d75..976f7804 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -106,7 +106,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -132,7 +132,7 @@ import { // // withResolvedContribs({ // from: 'coverArtistContribsByRef', -// to: '#coverArtistContribs', +// into: '#coverArtistContribs', // }), // // { @@ -171,7 +171,7 @@ import { // // withResolvedContribs = ({ // from: contribsByRefDependency, -// to: outputDependency, +// into: outputDependency, // }) => ({ // flags: {expose: true, compose: true}, // expose: { @@ -199,11 +199,11 @@ import { // expose: { // dependencies: ['artistData'], // mapDependencies: {from}, -// mapContinuation: {to}, +// mapContinuation: {into}, // compute({artistData, from: contribsByRef}, continuation) { // if (!artistData) return null; // return continuation({ -// to: (..resolve contributions one way or another..), +// into: (..resolve contributions one way or another..), // }); // }, // }, @@ -505,8 +505,8 @@ export function compositeFrom(firstArg, secondArg) { : {}); if (mapDependencies) { - for (const [to, from] of Object.entries(mapDependencies)) { - filteredDependencies[to] = availableDependencies[from] ?? null; + for (const [into, from] of Object.entries(mapDependencies)) { + filteredDependencies[into] = availableDependencies[from] ?? null; } } @@ -524,8 +524,8 @@ export function compositeFrom(firstArg, secondArg) { const assignDependencies = {}; - for (const [from, to] of Object.entries(mapContinuation)) { - assignDependencies[to] = continuationAssignment[from] ?? null; + for (const [from, into] of Object.entries(mapContinuation)) { + assignDependencies[into] = continuationAssignment[from] ?? null; } return assignDependencies; @@ -861,7 +861,7 @@ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, mode = 'null', - to = '#availability', + into = '#availability', }) { if (!['null', 'empty', 'falsy'].includes(mode)) { throw new TypeError(`Expected mode to be null, empty, or falsy`); @@ -890,10 +890,10 @@ export function withResultOfAvailabilityCheck({ flags: {expose: true, compose: true}, expose: { mapDependencies: {from: fromDependency}, - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, compute: ({from, '#options': {mode}}, continuation) => - continuation({to: checkAvailability(from, mode)}), + continuation({into: checkAvailability(from, mode)}), }, }; } else { @@ -901,10 +901,10 @@ export function withResultOfAvailabilityCheck({ annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, flags: {expose: true, compose: true}, expose: { - mapContinuation: {to}, + mapContinuation: {into}, options: {mode}, transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {to: checkAvailability(value, mode)}), + continuation(value, {into: checkAvailability(value, mode)}), }, }; } -- 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/composite.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 976f7804..5b6de901 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -801,9 +801,10 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency(dependency, { +export function exposeDependency({ + dependency, update = false, -} = {}) { +}) { return { annotation: `exposeDependency`, flags: {expose: true, update: !!update}, @@ -826,9 +827,10 @@ export function exposeDependency(dependency, { // 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. -export function exposeConstant(value, { +export function exposeConstant({ + value, update = false, -} = {}) { +}) { return { annotation: `exposeConstant`, flags: {expose: true, update: !!update}, @@ -912,9 +914,10 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue(dependency, { +export function exposeDependencyOrContinue({ + dependency, mode = 'null', -} = {}) { +}) { return compositeFrom(`exposeDependencyOrContinue`, [ withResultOfAvailabilityCheck({ fromDependency: dependency, @@ -990,10 +993,11 @@ export function exitIfAvailabilityCheckFailed({ // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency(dependency, { +export function exitWithoutDependency({ + dependency, mode = 'null', value = null, -} = {}) { +}) { return compositeFrom(`exitWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), exitIfAvailabilityCheckFailed({value}), @@ -1014,11 +1018,12 @@ export function exitWithoutUpdateValue({ // Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency(dependency, { +export function raiseWithoutDependency({ + dependency, mode = 'null', map = {}, raise = {}, -} = {}) { +}) { return compositeFrom(`raiseWithoutDependency`, [ withResultOfAvailabilityCheck({fromDependency: dependency, mode}), -- 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/composite.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 5b6de901..fd52aa0f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import { empty, @@ -346,8 +346,8 @@ export function compositeFrom(firstArg, secondArg) { if (compositeFrom.debug === true) { const label = (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); + ? colors.dim(`[composite: ${annotation}]`) + : colors.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { console.log(label, ...result.map(value => @@ -594,9 +594,9 @@ export function compositeFrom(firstArg, secondArg) { const availableDependencies = {...initialDependencies}; if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { - debug(() => color.bright(`begin composition - not transforming`)); + debug(() => colors.bright(`begin composition - not transforming`)); } for (let i = 0; i < steps.length; i++) { @@ -641,7 +641,7 @@ export function compositeFrom(firstArg, secondArg) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => color.bright(`end composition - exit (inferred)`)); + debug(() => colors.bright(`end composition - exit (inferred)`)); return result; } @@ -652,7 +652,7 @@ export function compositeFrom(firstArg, secondArg) { const {providedValue} = continuationStorage; debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + debug(() => colors.bright(`end composition - exit (explicit)`)); if (baseComposes) { return continuationIfApplicable.exit(providedValue); @@ -708,17 +708,17 @@ export function compositeFrom(firstArg, secondArg) { case 'raise': debug(() => (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); + ? colors.bright(`end composition - raise (base: explicit)`) + : colors.bright(`end composition - raise`))); return continuationIfApplicable(...continuationArgs); case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); + debug(() => colors.bright(`end composition - raiseAbove`)); return continuationIfApplicable.raise(...continuationArgs); case 'continuation': if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raise (inferred)`)); return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, continuingWithDependencies); -- cgit 1.3.0-6-gf8a5 From d33effa272c3388640974648fe2888a284c6701c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 14:50:41 -0300 Subject: data: withAlbum: perform proper availability check on album --- src/data/things/composite.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fd52aa0f..29f5770c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -853,8 +853,9 @@ export function exposeConstant({ // 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. +// * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. +// This will outright error for undefined. // * '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! @@ -879,7 +880,7 @@ export function withResultOfAvailabilityCheck({ const checkAvailability = (value, mode) => { switch (mode) { - case 'null': return value !== null; + case 'null': return value !== null && value !== undefined; case 'empty': return !empty(value); case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; -- 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/composite.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 29f5770c..96abf4af 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1071,3 +1071,18 @@ export function raiseWithoutUpdateValue({ }, ]); } + +export function withUpdateValueAsDependency({ + into = '#updateValue', +} = {}) { + return { + annotation: `withUpdateValueAsDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapContinuation: {into}, + transform: (value, continuation) => + continuation(value, {into: value}), + }, + }; +} -- cgit 1.3.0-6-gf8a5 From 65260d7fc2790ece0c13820ba18bc821163f164e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 8 Sep 2023 12:19:49 -0300 Subject: data: new withFlattenedArray, withUnflattenedArray utilities --- src/data/things/composite.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 96abf4af..1f6482f6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1072,6 +1072,8 @@ export function raiseWithoutUpdateValue({ ]); } +// Turns an updating property's update value into a dependency, so it can be +// conveniently passed to other functions. export function withUpdateValueAsDependency({ into = '#updateValue', } = {}) { @@ -1086,3 +1088,76 @@ export function withUpdateValueAsDependency({ }, }; } + +// Flattens an array with one level of nested arrays, providing as dependencies +// both the flattened array as well as the original starting indices of each +// successive source array. +export function withFlattenedArray({ + from, + into = '#flattenedArray', + intoIndices = '#flattenedIndices', +}) { + return { + annotation: `withFlattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from}, + mapContinuation: {into, intoIndices}, + + compute({from: sourceArray}, continuation) { + const into = sourceArray.flat(); + const intoIndices = []; + + let lastEndIndex = 0; + for (const {length} of sourceArray) { + intoIndices.push(lastEndIndex); + lastEndIndex += length; + } + + return continuation({into, intoIndices}); + }, + }, + }; +} + +// After mapping the contents of a flattened array in-place (being careful to +// retain the original indices by replacing unmatched results with null instead +// of filtering them out), this function allows for recombining them. It will +// filter out null and undefined items by default (pass {filter: false} to +// disable this). +export function withUnflattenedArray({ + from, + fromIndices = '#flattenedIndices', + into = '#unflattenedArray', + filter = true, +}) { + return { + annotation: `withUnflattenedArray`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {from, fromIndices}, + mapContinuation: {into}, + compute({from, fromIndices}, continuation) { + const arrays = []; + + for (let i = 0; i < fromIndices.length; i++) { + const startIndex = fromIndices[i]; + const endIndex = + (i === fromIndices.length - 1 + ? from.length + : fromIndices[i + 1]); + + const values = from.slice(startIndex, endIndex); + arrays.push( + (filter + ? values.filter(value => value !== null && value !== undefined) + : values)); + } + + return continuation({into: arrays}); + }, + }, + }; +} -- 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/composite.js | 127 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1f6482f6..a5adc3e9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1089,6 +1089,133 @@ export function withUpdateValueAsDependency({ }; } +// Gets a property of some object (in a dependency) and provides that value. +// If the object itself is null, the provided dependency will also always be +// null. By default, it'll also be null if the object doesn't have the listed +// property (or its value is undefined/null); provide a value on {missing} to +// default to something else here. +export function withPropertyFromObject({ + object, + property, + into = null, +}) { + into ??= + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`); + + return { + annotation: `withPropertyFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + mapContinuation: {into}, + options: {property}, + + compute({object, '#options': {property}}, continuation) { + if (object === null || object === undefined) return continuation({into: null}); + if (!Object.hasOwn(object, property)) return continuation({into: null}); + return continuation({into: object[property] ?? null}); + }, + }, + }; +} + +// Gets a property from each of a list of objects (in a dependency) and +// provides the results. This doesn't alter any list indices, so positions +// which were null in the original list are kept null here. Objects which don't +// have the specified property are also included in-place as null, by default; +// provide a value on {missing} to default to something else here. +export function withPropertyFromList({ + list, + property, + into = null, + missing = null, +}) { + into ??= + (list.startsWith('#') + ? `${list}.${property}` + : `#${list}.${property}`); + + return { + annotation: `withPropertyFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {property}, + + compute({list, '#options': {property}}, continuation) { + if (list === undefined || empty(list)) { + return continuation({into: []}); + } + + return continuation({ + into: + list.map(item => { + if (item === null || item === undefined) return null; + if (!Object.hasOwn(item, property)) return missing; + return item[property] ?? missing; + }), + }); + }, + }, + }; +} + +// Replaces items of a list, which are null or undefined, with some fallback +// value, either a constant (set {value}) or from a dependency ({dependency}). +// By default, this replaces the passed dependency. +export function fillMissingListItems({ + list, + value, + dependency, + into = list, +}) { + if (value !== undefined && dependency !== undefined) { + throw new TypeError(`Don't provide both value and dependency`); + } + + if (value === undefined && dependency === undefined) { + throw new TypeError(`Missing value or dependency`); + } + + if (dependency) { + return { + annotation: `fillMissingListItems.fromDependency`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list, dependency}, + mapContinuation: {into}, + + compute: ({list, dependency}, continuation) => + continuation({ + into: list.map(item => item ?? dependency), + }), + }, + }; + } else { + return { + annotation: `fillMissingListItems.fromValue`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + mapContinuation: {into}, + options: {value}, + + compute: ({list, '#options': {value}}, continuation) => + continuation({ + into: list.map(item => item ?? value), + }), + }, + }; + } +} + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. -- cgit 1.3.0-6-gf8a5 From 726118e7e8eefa9002562ca2dd0a4f6deb8a05b9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 08:26:33 -0300 Subject: data: refactor {missing} out of withPropertyFrom{Object,List} --- src/data/things/composite.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index a5adc3e9..b37b8e31 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1090,10 +1090,8 @@ export function withUpdateValueAsDependency({ } // Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, the provided dependency will also always be -// null. By default, it'll also be null if the object doesn't have the listed -// property (or its value is undefined/null); provide a value on {missing} to -// default to something else here. +// If the object itself is null, or the object doesn't have the listed property, +// the provided dependency will also be null. export function withPropertyFromObject({ object, property, @@ -1113,11 +1111,10 @@ export function withPropertyFromObject({ mapContinuation: {into}, options: {property}, - compute({object, '#options': {property}}, continuation) { - if (object === null || object === undefined) return continuation({into: null}); - if (!Object.hasOwn(object, property)) return continuation({into: null}); - return continuation({into: object[property] ?? null}); - }, + compute: ({object, '#options': {property}}, continuation) => + (object === null || object === undefined + ? continuation({into: null}) + : continuation({into: object[property] ?? null})), }, }; } @@ -1125,13 +1122,11 @@ export function withPropertyFromObject({ // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't -// have the specified property are also included in-place as null, by default; -// provide a value on {missing} to default to something else here. +// have the specified property are retained in-place as null. export function withPropertyFromList({ list, property, into = null, - missing = null, }) { into ??= (list.startsWith('#') @@ -1154,11 +1149,10 @@ export function withPropertyFromList({ return continuation({ into: - list.map(item => { - if (item === null || item === undefined) return null; - if (!Object.hasOwn(item, property)) return missing; - return item[property] ?? missing; - }), + list.map(item => + (item === null || item === undefined + ? null + : item[property] ?? null)), }); }, }, -- 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/composite.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b37b8e31..e3225563 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1119,6 +1119,39 @@ export function withPropertyFromObject({ }; } +// Gets the listed properties from some object, providing each property's value +// as a dependency prefixed with the same name as the object (by default). +// If the object itself is null, all provided dependencies will be null; +// if it's missing only select properties, those will be provided as null. +export function withPropertiesFromObject({ + object, + properties, + prefix = + (object.startsWith('#') + ? object + : `#${object}`), +}) { + return { + annotation: `withPropertiesFromObject`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {object}, + options: {prefix, properties}, + + compute: ({object, '#options': {prefix, properties}}, continuation) => + continuation( + Object.fromEntries( + properties.map(property => [ + `${prefix}.${property}`, + (object === null || object === undefined + ? null + : object[property] ?? null), + ]))), + }, + }; +} + // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions // which were null in the original list are kept null here. Objects which don't @@ -1159,6 +1192,45 @@ export function withPropertyFromList({ }; } +// Gets the listed properties from each of a list of objects, providing lists +// of property values each into a dependency prefixed with the same name as the +// list (by default). Like withPropertyFromList, this doesn't alter indices. +export function withPropertiesFromList({ + list, + properties, + prefix = + (list.startsWith('#') + ? list + : `#${list}`), +}) { + return { + annotation: `withPropertiesFromList`, + flags: {expose: true, compose: true}, + + expose: { + mapDependencies: {list}, + options: {prefix, properties}, + + compute({list, '#options': {prefix, properties}}, continuation) { + const lists = + Object.fromEntries( + properties.map(property => [`${prefix}.${property}`, []])); + + for (const item of list) { + for (const property of properties) { + lists[`${prefix}.${property}`].push( + (item === null || item === undefined + ? null + : item[property] ?? null)); + } + } + + return continuation(lists); + } + } + } +} + // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). // By default, this replaces the passed dependency. -- cgit 1.3.0-6-gf8a5 From a9b96deeca6b2dacb7fac309c47e7bc6289270e6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 09:29:51 -0300 Subject: data: be more permissive of steps w/ no special expose behavior --- src/data/things/composite.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e3225563..2dd92f17 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -432,13 +432,8 @@ export function compositeFrom(firstArg, secondArg) { ? 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; - } + const stepComputes = !!expose?.compute; + const stepTransforms = !!expose?.transform; if ( stepTransforms && !stepComputes && @@ -459,7 +454,7 @@ export function compositeFrom(firstArg, secondArg) { // 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 ?? []) { + for (const dependency of expose?.dependencies ?? []) { if (typeof dependency === 'string' && dependency.startsWith('#')) { continue; } @@ -470,22 +465,14 @@ export function compositeFrom(firstArg, secondArg) { // 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 ?? {})) { + for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } }); } - if (!baseComposes) { - if (baseUpdates) { - if (!anyStepsTransform) { - aggregate.push(new TypeError(`Expected at least one step to transform`)); - } - } else { - if (!anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); - } - } + if (!baseComposes && !baseUpdates && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute`)); } aggregate.close(); @@ -615,6 +602,11 @@ export function compositeFrom(firstArg, secondArg) { ? step.expose : step); + if (!expose) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + const callingTransformForThisStep = expectingTransform && expose.transform; -- cgit 1.3.0-6-gf8a5 From 272a2f47102451a277d099d032e6f4d0ad673d80 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 9 Sep 2023 18:20:00 -0300 Subject: data: handle missing expose specially in base This is for better compatibility with an updating base that doesn't transform its update value, but attempts to behave reasonably for non-transforming contexts as well. --- src/data/things/composite.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2dd92f17..3a63f22d 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -603,7 +603,31 @@ export function compositeFrom(firstArg, secondArg) { : step); if (!expose) { - debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + if (!isBase) { + debug(() => `step #${i+1} - no expose description, nothing to do for this step`); + continue; + } + + if (expectingTransform) { + debug(() => `step #${i+1} (base) - no expose description, returning so-far update value:`, valueSoFar); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(valueSoFar); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return valueSoFar; + } + } else { + debug(() => `step #${i+1} (base) - no expose description, nothing to continue with`); + if (continuationIfApplicable) { + debug(() => colors.bright(`end composition - raise (inferred - composing)`)); + return continuationIfApplicable(); + } else { + debug(() => colors.bright(`end composition - exit (inferred - not composing)`)); + return null; + } + } + continue; } -- 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/composite.js | 111 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 8 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3a63f22d..26124b56 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {TupleMap} from '#wiki-data'; import { empty, @@ -341,6 +342,8 @@ import { // syntax as for other compositional steps, and it'll work out cleanly! // +const globalCompositeCache = {}; + export function compositeFrom(firstArg, secondArg) { const debug = fn => { if (compositeFrom.debug === true) { @@ -567,8 +570,8 @@ export function compositeFrom(firstArg, secondArg) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol('continuation symbol'); - const noTransformSymbol = Symbol('no-transform symbol'); + const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); + const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { const expectingTransform = initialValue !== noTransformSymbol; @@ -634,21 +637,83 @@ export function compositeFrom(firstArg, secondArg) { const callingTransformForThisStep = expectingTransform && expose.transform; + let continuationStorage; + const filteredDependencies = _filterDependencies(availableDependencies, expose); - const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); - const result = + let result; + + const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? expose.transform(valueSoFar, filteredDependencies, continuation) - : expose.transform(valueSoFar, continuation)) + ? ['transform', valueSoFar, filteredDependencies] + : ['transform', valueSoFar]) : (filteredDependencies - ? expose.compute(filteredDependencies, continuation) - : expose.compute(continuation))); + ? ['compute', filteredDependencies] + : ['compute'])); + + const naturalEvaluate = () => { + const [name, ...args] = getExpectedEvaluation(); + let continuation; + ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); + return expose[name](...args, continuation); + } + + switch (step.cache) { + // Warning! Highly WIP! + case 'aggressive': { + const hrnow = () => { + const hrTime = process.hrtime(); + return hrTime[0] * 1000000000 + hrTime[1]; + }; + + const [name, ...args] = getExpectedEvaluation(); + + let cache = globalCompositeCache[step.annotation]; + if (!cache) { + cache = globalCompositeCache[step.annotation] = { + transform: new TupleMap(), + compute: new TupleMap(), + times: { + read: [], + evaluate: [], + }, + }; + } + + const tuplefied = args + .flatMap(arg => [ + Symbol.for('compositeFrom: tuplefied arg divider'), + ...(typeof arg !== 'object' || Array.isArray(arg) + ? [arg] + : Object.entries(arg).flat()), + ]); + + const readTime = hrnow(); + const cacheContents = cache[name].get(tuplefied); + cache.times.read.push(hrnow() - readTime); + + if (cacheContents) { + ({result, continuationStorage} = cacheContents); + } else { + const evaluateTime = hrnow(); + result = naturalEvaluate(); + cache.times.evaluate.push(hrnow() - evaluateTime); + cache[name].set(tuplefied, {result, continuationStorage}); + } + + break; + } + + default: { + result = naturalEvaluate(); + break; + } + } if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); @@ -775,6 +840,7 @@ export function compositeFrom(firstArg, secondArg) { if (baseComposes) { if (anyStepsTransform) expose.transform = transformFn; if (anyStepsCompute) expose.compute = computeFn; + if (base.cacheComposition) expose.cache = base.cacheComposition; } else if (baseUpdates) { expose.transform = transformFn; } else { @@ -785,6 +851,35 @@ export function compositeFrom(firstArg, secondArg) { return constructedDescriptor; } +export function displayCompositeCacheAnalysis() { + const showTimes = (cache, key) => { + const times = cache.times[key].slice().sort(); + + const all = times; + const worst10pc = times.slice(-times.length / 10); + const best10pc = times.slice(0, times.length / 10); + const middle50pc = times.slice(times.length / 4, -times.length / 4); + const middle80pc = times.slice(times.length / 10, -times.length / 10); + + const fmt = val => `${(val / 1000).toFixed(2)}ms`.padStart(9); + const avg = times => times.reduce((a, b) => a + b, 0) / times.length; + + const left = ` - ${key}: `; + const indn = ' '.repeat(left.length); + console.log(left + `${fmt(avg(all))} (all ${all.length})`); + console.log(indn + `${fmt(avg(worst10pc))} (worst 10%)`); + console.log(indn + `${fmt(avg(best10pc))} (best 10%)`); + console.log(indn + `${fmt(avg(middle80pc))} (middle 80%)`); + console.log(indn + `${fmt(avg(middle50pc))} (middle 50%)`); + }; + + for (const [annotation, cache] of Object.entries(globalCompositeCache)) { + console.log(`Cached ${annotation}:`); + showTimes(cache, 'evaluate'); + showTimes(cache, 'read'); + } +} + // 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 -- 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/composite.js | 65 ++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 30 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26124b56..32a61033 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -344,7 +344,9 @@ import { const globalCompositeCache = {}; -export function compositeFrom(firstArg, secondArg) { +export function compositeFrom(description) { + const {annotation, steps: composition} = description; + const debug = fn => { if (compositeFrom.debug === true) { const label = @@ -363,13 +365,6 @@ export function compositeFrom(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(); @@ -974,6 +969,7 @@ export function exposeConstant({ export function withResultOfAvailabilityCheck({ fromUpdateValue, fromDependency, + modeDependency, mode = 'null', into = '#availability', }) { @@ -1026,31 +1022,40 @@ export function withResultOfAvailabilityCheck({ // Exposes a dependency as it is, or continues if it's unavailable. // See withResultOfAvailabilityCheck for {mode} options! -export function exposeDependencyOrContinue({ - dependency, - mode = 'null', -}) { - return compositeFrom(`exposeDependencyOrContinue`, [ - withResultOfAvailabilityCheck({ - fromDependency: dependency, - mode, - }), +export const exposeDependencyOrContinue = + templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, - { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => - (availability - ? continuation() - : continuation.raise()), + inputs: { + dependency: input(), + mode: input.default('null'), }, - { - mapDependencies: {dependency}, - compute: ({dependency}, continuation) => - continuation.exit(dependency), - }, - ]); -} + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? continuation() + : continuation.raise()), + }, + + { + dependencies: [input('#dependency')], + compute: (continuation, { + [input('#dependency')]: dependency, + }) => + continuation.exit(dependency), + }, + ], + }); // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck -- cgit 1.3.0-6-gf8a5 From 88ae3f19a38782ca1396b8bc131d1adffb9699e2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 14 Sep 2023 08:54:34 -0300 Subject: data: update syntax for essential compositional utilities Also withPropertyFromObject because some commits were messed up along the way... WIP as usual. --- src/data/things/composite.js | 422 ++++++++++++++++++++----------------------- 1 file changed, 198 insertions(+), 224 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 32a61033..3e766b2c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -953,11 +953,10 @@ export function exposeConstant({ }; } -// 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': +// Checks the availability of a dependency 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 default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). // * 'empty': Check that the value is neither null nor an empty array. @@ -966,274 +965,249 @@ export function exposeConstant({ // (nor an empty array). Keep in mind this will also be false // for values like zero and the empty string! // -export function withResultOfAvailabilityCheck({ - fromUpdateValue, - fromDependency, - modeDependency, - mode = 'null', - into = '#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`); - } +const availabilityCheckMode = { + validate: oneOf('null', 'empty', 'falsy'), + defaultValue: 'null', +}; - if (!fromUpdateValue && !fromDependency) { - throw new TypeError(`Missing dependency name (or fromUpdateValue)`); - } +export const withResultOfAvailabilityCheck = templateCompositeFrom({ + annotation: `withResultOfAvailabilityCheck`, - const checkAvailability = (value, mode) => { - switch (mode) { - case 'null': return value !== null && value !== undefined; - case 'empty': return !empty(value); - case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); - default: return false; - } - }; + inputs: { + from: input(), + mode: input(availabilityCheckMode), + }, - if (fromDependency) { - return { - annotation: `withResultOfAvailabilityCheck.fromDependency`, - flags: {expose: true, compose: true}, - expose: { - mapDependencies: {from: fromDependency}, - mapContinuation: {into}, - options: {mode}, - compute: ({from, '#options': {mode}}, continuation) => - continuation({into: checkAvailability(from, mode)}), - }, - }; - } else { - return { - annotation: `withResultOfAvailabilityCheck.fromUpdateValue`, - flags: {expose: true, compose: true}, - expose: { - mapContinuation: {into}, - options: {mode}, - transform: (value, {'#options': {mode}}, continuation) => - continuation(value, {into: checkAvailability(value, mode)}), - }, - }; - } -} + outputs: { + into: '#availability', + }, -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = - templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, + steps: [ + { + dependencies: [input('from'), input('mode')], - inputs: { - dependency: input(), - mode: input.default('null'), - }, + compute: (continuation, { + [input('from')]: dependency, + [input('mode')]: mode, + }) => { + let availability; - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability'], - compute: (continuation, { - ['#availability']: availability, - }) => - (availability - ? continuation() - : continuation.raise()), - }, + switch (mode) { + case 'null': + availability = value !== null && value !== undefined; + break; + + case 'empty': + availability = !empty(value); + break; + + case 'falsy': + availability = !!value && (!Array.isArray(value) || !empty(value)); + break; + } - { - dependencies: [input('#dependency')], - compute: (continuation, { - [input('#dependency')]: dependency, - }) => - continuation.exit(dependency), + return continuation({into: availability}); }, - ], - }); + }, + ], +}); -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! -export function exposeUpdateValueOrContinue({ - mode = 'null', -} = {}) { - return compositeFrom(`exposeUpdateValueOrContinue`, [ +// Exposes a dependency as it is, or continues if it's unavailable. +// See withResultOfAvailabilityCheck for {mode} options! +export const exposeDependencyOrContinue = templateCompositeFrom({ + annotation: `exposeDependencyOrContinue`, + + inputs: { + dependency: input(), + mode: input(availabilityCheckMode), + }, + + steps: () => [ withResultOfAvailabilityCheck({ - fromUpdateValue: true, - mode, + from: input('dependency'), + mode: input('mode'), }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('dependency')], + compute: (continuation, { + ['#availability']: availability, + [input('dependency')]: dependency, + }) => (availability - ? continuation() - : continuation.raise()), + ? continuation.exit(dependency) + : continuation()), }, + ], +}); - { - transform: (value, continuation) => - continuation.exit(value), - }, - ]); -} +// Exposes the update value of an {update: true} property as it is, +// or continues if it's unavailable. See withResultOfAvailabilityCheck +// for {mode} options! +export const exposeUpdateValueOrContinue = templateCompositeFrom({ + annotation: `exposeUpdateValueOrContinue`, -// Early exits if an availability check has failed. -// This is for internal use only - use `exitWithoutDependency` or -// `exitWithoutUpdateValue` instead. -export function exitIfAvailabilityCheckFailed({ - availability = '#availability', - value = null, -} = {}) { - return compositeFrom(`exitIfAvailabilityCheckFailed`, [ - { - mapDependencies: {availability}, - compute: ({availability}, continuation) => - (availability - ? continuation.raise() - : continuation()), - }, + inputs: { + mode: input(availabilityCheckMode), + }, - { - options: {value}, - compute: ({'#options': {value}}, continuation) => - continuation.exit(value), - }, - ]); -} + steps: () => [ + exposeDependencyOrContinue({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); // Early exits if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutDependency({ - dependency, - mode = 'null', - value = null, -}) { - return compositeFrom(`exitWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} +export const exitWithoutDependency = templateCompositeFrom({ + annotation: `exitWithoutDependency`, -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function exitWithoutUpdateValue({ - mode = 'null', - value = null, -} = {}) { - return compositeFrom(`exitWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), - exitIfAvailabilityCheckFailed({value}), - ]); -} + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutDependency({ - dependency, - mode = 'null', - map = {}, - raise = {}, -}) { - return compositeFrom(`raiseWithoutDependency`, [ - withResultOfAvailabilityCheck({fromDependency: dependency, mode}), + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('value')], + continuation: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.exit(value)), }, + ], +}); - { - options: {raise}, - mapContinuation: map, - compute: ({'#options': {raise}}, continuation) => - continuation.raiseAbove(raise), - }, - ]); -} +// Early exits if this property's update value isn't available. +// See withResultOfAvailabilityCheck for {mode} options! +export const exitWithoutUpdateValue = templateCompositeFrom({ + annotation: `exitWithoutUpdateValue`, + + inputs: { + mode: input(availabilityCheckMode), + value: input({defaultValue: null}), + }, + + steps: [ + exitWithoutDependency({ + dependency: input.updateValue(), + mode: input('mode'), + }), + ], +}); -// Raises if this property's update value isn't available. +// Raises if a dependency isn't available. // See withResultOfAvailabilityCheck for {mode} options! -export function raiseWithoutUpdateValue({ - mode = 'null', - map = {}, - raise = {}, -} = {}) { - return compositeFrom(`raiseWithoutUpdateValue`, [ - withResultOfAvailabilityCheck({fromUpdateValue: true, mode}), +export const raiseOutputWithoutDependency = templateCompositeFrom({ + annotation: `raiseOutputWithoutDependency`, + + inputs: { + dependency: input.required(), + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, + + steps: [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), { - dependencies: ['#availability'], - compute: ({'#availability': availability}, continuation) => + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => (availability - ? continuation.raise() - : continuation()), + ? continuation() + : continuation.raiseOutputAbove(output)), }, + ], +}); - { - 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! +export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ + annotation: `raiseOutputWithoutUpdateValue`, -// Turns an updating property's update value into a dependency, so it can be -// conveniently passed to other functions. -export function withUpdateValueAsDependency({ - into = '#updateValue', -} = {}) { - return { - annotation: `withUpdateValueAsDependency`, - flags: {expose: true, compose: true}, + inputs: { + mode: input(availabilityCheckMode), + output: input({defaultValue: {}}), + }, - expose: { - mapContinuation: {into}, - transform: (value, continuation) => - continuation(value, {into: value}), + steps: [ + withResultOfAvailabilityCheck({ + from: input.updateValue(), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('output')], + compute: (continuation, { + ['#availability']: availability, + [input('output')]: output, + }) => + (availability + ? continuation() + : continuation.raiseOutputAbove(output)), }, - }; -} + ], +}); // Gets a property of some object (in a dependency) and provides that value. // If the object itself is null, or the object doesn't have the listed property, // the provided dependency will also be null. -export function withPropertyFromObject({ - object, - property, - into = null, -}) { - into ??= - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`); +export const withPropertyFromObject = templateCompositeFrom({ + annotation: `withPropertyFromObject`, - return { - annotation: `withPropertyFromObject`, - flags: {expose: true, compose: true}, + inputs: { + object: input({type: 'object', null: true}), + property: input.required({type: 'string'}), + } - expose: { - mapDependencies: {object}, - mapContinuation: {into}, - options: {property}, + outputs: { + into: { + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + default: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => + (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`), + }, + }, - compute: ({object, '#options': {property}}, continuation) => - (object === null || object === undefined + steps: [ + { + dependencies: [input('object'), input('property')], + compute: (continuation, { + [input('object')]: object, + [input('property')]: property, + }) => + (object === null ? continuation({into: null}) : continuation({into: object[property] ?? null})), }, - }; -} + ], +}); // Gets the listed properties from some object, providing each property's value // as a dependency prefixed with the same name as the object (by default). -- cgit 1.3.0-6-gf8a5 From 194676f45f54d09a3ad247e9ba4e2b3ba2e56db4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 15 Sep 2023 20:02:44 -0300 Subject: data: experimental templateCompositeFrom implementation --- src/data/things/composite.js | 374 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 3e766b2c..091faa3a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -7,6 +7,7 @@ import { empty, filterProperties, openAggregate, + decorateErrorWithIndex, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -344,6 +345,379 @@ import { const globalCompositeCache = {}; +export function input(nameOrDescription) { + if (typeof nameOrDescription === 'string') { + return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); + } else { + return { + symbol: Symbol.for('hsmusic.composite.input'), + shape: 'input', + value: nameOrDescription, + }; + } +} + +input.symbol = Symbol.for('hsmusic.composite.input'); + +input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); +input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); +input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); +input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); + +function isInputToken(token) { + if (typeof token === 'object') { + return token.symbol === Symbol.for('hsmusic.composite.input'); + } else if (typeof token === 'symbol') { + return token.description.startsWith('hsmusic.composite.input'); + } else { + return false; + } +} + +function getInputTokenShape(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.shape; + } else { + return token.description.match(/hsmusic\.composite\.(input.*?)(:|$)/)[1]; + } +} + +function getInputTokenValue(token) { + if (!isInputToken(token)) { + throw new TypeError(`Expected an input token, got ${token}`); + } + + if (typeof token === 'object') { + return token.value; + } else { + return token.description.match(/hsmusic\.composite\.input.*?:(.*)/)?.[1] ?? null; + } +} + +export function templateCompositeFrom(description) { + const compositeName = + (description.annotation + ? description.annotation + : `unnamed composite`); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + + if ('steps' in description) { + if (Array.isArray(description.steps)) { + descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + } + } + + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; + + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (getInputTokenShape(value) !== 'input') { + wrongCallsToInput.push(name); + } + } + + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); + } + + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input(), got ${shape}`)); + } + }); + + descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { + const wrongType = []; + const notPrivate = []; + + const missingDependenciesDefault = []; + const wrongDependenciesType = []; + const wrongDefaultType = []; + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value === 'object') { + if (!('dependencies' in value && 'default' in value)) { + missingDependenciesDefault.push(name); + continue; + } + + if (!Array.isArray(value.dependencies)) { + wrongDependenciesType.push(name); + } + + if (typeof value.default !== 'function') { + wrongDefaultType.push(name); + } + + continue; + } + + if (typeof value !== 'string') { + wrongType.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivate.push(name); + continue; + } + } + + for (const name of wrongType) { + const type = typeof description.outputs[name]; + push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivate) { + const into = description.outputs[name]; + push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + for (const name of missingDependenciesDefault) { + push(new Error(`${name}: Expected both dependencies & default`)); + } + + for (const name of wrongDependenciesType) { + const {dependencies} = description.outputs[name]; + push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); + } + + for (const name of wrongDefaultType) { + const type = typeof description.outputs[name].default; + push(new Error(`${name}: Expected default to be function, got ${type}`)); + } + + for (const [name, value] of Object.entries(description.outputs ?? {})) { + if (typeof value !== 'object') continue; + + map( + description.outputs[name].dependencies, + decorateErrorWithIndex(dependency => { + if (!isInputToken(dependency)) { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); + } + + const shape = getInputTokenShape(dependency); + if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { + throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + } + }), + {message: `${name}: Errors in dependencies`}); + } + }); + + descriptionAggregate.close(); + + const expectedInputNames = + (description.inputs + ? Object.keys(description.inputs) + : []); + + const expectedOutputNames = + (description.outputs + ? Object.keys(description.outputs) + : []); + + return (inputOptions = {}) => { + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + if (inputDescription.null === true) return false; + return true; + }); + + const wrongTypeInputNames = []; + const wrongInputCallInputNames = []; + + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } + + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } + } + + if (!empty(misplacedInputNames)) { + inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } + + if (!empty(missingInputNames)) { + inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } + + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + + inputOptionsAggregate.close(); + + const outputOptions = {}; + + const instantiatedTemplate = { + symbol: templateCompositeFrom.symbol, + + outputs(providedOptions) { + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + const notPrivateOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } + + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } + + if (!value.startsWith('#')) { + notPrivateOutputNames.push(name); + continue; + } + } + + if (!empty(misplacedOutputNames)) { + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + } + + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); + } + + for (const name of notPrivateOutputNames) { + const into = providedOptions[name]; + outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); + } + + outputOptionsAggregate.close(); + + Object.assign(outputOptions, providedOptions); + return instantiatedTemplate; + }, + + toDescription() { + const finalDescription = {}; + + if ('annotation' in description) { + finalDescription.annotation = description.annotation; + } + + if ('update' in description) { + finalDescription.update = description.update; + } + + if ('inputs' in description) { + const finalInputs = {}; + + for (const [name, description_] of Object.entries(description.inputs)) { + const description = description_; + if (name in inputOptions) { + if (typeof inputOptions[name] === 'string') { + finalInputs[name] = input.dependency(inputOptions[name]); + } else { + finalInputs[name] = inputOptions[name]; + } + } else if (description.defaultValue) { + finalInputs[name] = input.value(defaultValue); + } else if (description.defaultDependency) { + finalInputs[name] = input.dependency(defaultValue); + } else { + finalInputs[name] = input.value(null); + } + } + + finalDescription.inputs = finalInputs; + } + + if ('outputs' in description) { + const finalOutputs = {}; + + for (const [name, defaultDependency] of Object.entries(description.outputs)) { + if (name in outputOptions) { + finalOutputs[name] = outputOptions[name]; + } else { + finalOutputs[name] = defaultDependency; + } + } + + finalDescription.outputs = finalOutputs; + } + + if ('steps' in description) { + finalDescription.steps = description.steps; + } + + return finalDescription; + }, + + toResolvedComposition() { + const ownDescription = instantiatedTemplate.toDescription(); + + const finalDescription = {...ownDescription}; + + const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + + const steps = ownDescription.steps(); + + const resolvedSteps = + aggregate.map( + steps, + decorateErrorWithIndex(step => + (step.symbol === templateCompositeFrom.symbol + ? step.toResolvedComposition() + : step)), + {message: `Errors resolving steps`}); + + aggregate.close(); + + finalDescription.steps = resolvedSteps; + + return finalDescription; + }, + }; + + return instantiatedTemplate; + }; +} + +templateCompositeFrom.symbol = Symbol(); + export function compositeFrom(description) { const {annotation, steps: composition} = description; -- 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/composite.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 091faa3a..cd713169 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; +import {oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1357,7 +1358,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ into: '#availability', }, - steps: [ + steps: () => [ { dependencies: [input('from'), input('mode')], @@ -1440,12 +1441,12 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), - value: input({defaultValue: null}), + value: input({null: true}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1474,7 +1475,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ value: input({defaultValue: null}), }, - steps: [ + steps: () => [ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), @@ -1488,12 +1489,12 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input.required(), + dependency: input(), mode: input(availabilityCheckMode), output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), mode: input('mode'), @@ -1522,7 +1523,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ output: input({defaultValue: {}}), }, - steps: [ + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), mode: input('mode'), @@ -1549,8 +1550,8 @@ export const withPropertyFromObject = templateCompositeFrom({ inputs: { object: input({type: 'object', null: true}), - property: input.required({type: 'string'}), - } + property: input({type: 'string'}), + }, outputs: { into: { @@ -1569,7 +1570,7 @@ export const withPropertyFromObject = templateCompositeFrom({ }, }, - steps: [ + steps: () => [ { dependencies: [input('object'), input('property')], compute: (continuation, { -- 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/composite.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cd713169..f2ca2c7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -420,7 +420,7 @@ export function templateCompositeFrom(description) { const missingCallsToInput = []; const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { + for (const [name, value] of Object.entries(description.inputs ?? {})) { if (!isInputToken(value)) { missingCallsToInput.push(name); continue; @@ -533,7 +533,7 @@ export function templateCompositeFrom(description) { ? Object.keys(description.outputs) : []); - return (inputOptions = {}) => { + const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); const providedInputNames = Object.keys(inputOptions); @@ -593,7 +593,7 @@ export function templateCompositeFrom(description) { const misplacedOutputNames = []; const wrongTypeOutputNames = []; - const notPrivateOutputNames = []; + // const notPrivateOutputNames = []; for (const [name, value] of Object.entries(providedOptions)) { if (!expectedOutputNames.includes(name)) { @@ -606,10 +606,12 @@ export function templateCompositeFrom(description) { continue; } + /* if (!value.startsWith('#')) { notPrivateOutputNames.push(name); continue; } + */ } if (!empty(misplacedOutputNames)) { @@ -621,10 +623,12 @@ export function templateCompositeFrom(description) { outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); } + /* for (const name of notPrivateOutputNames) { const into = providedOptions[name]; outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); } + */ outputOptionsAggregate.close(); @@ -715,6 +719,10 @@ export function templateCompositeFrom(description) { return instantiatedTemplate; }; + + instantiate.inputs = instantiate; + + return instantiate; } templateCompositeFrom.symbol = Symbol(); -- 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/composite.js | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f2ca2c7c..c33fc03c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1349,7 +1349,7 @@ export function exposeConstant({ // for values like zero and the empty string! // -const availabilityCheckMode = { +const availabilityCheckModeInput = { validate: oneOf('null', 'empty', 'falsy'), defaultValue: 'null', }; @@ -1359,7 +1359,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ inputs: { from: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, outputs: { @@ -1403,7 +1403,7 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1432,7 +1432,7 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), }, steps: () => [ @@ -1450,7 +1450,7 @@ export const exitWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({null: true}), }, @@ -1479,7 +1479,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), value: input({defaultValue: null}), }, @@ -1498,7 +1498,7 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1527,7 +1527,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckMode), + mode: input(availabilityCheckModeInput), output: input({defaultValue: {}}), }, @@ -1562,19 +1562,21 @@ export const withPropertyFromObject = templateCompositeFrom({ }, outputs: { - into: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - default: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`), + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return ( + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value')); }, }, -- cgit 1.3.0-6-gf8a5 From 33558828e70e4dd942bec1fbdb8aea3819ed8a19 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 19 Sep 2023 10:16:52 -0300 Subject: data: declare {update} in higher-context locations --- src/data/things/composite.js | 92 ++++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 28 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c33fc03c..011f307e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1279,34 +1279,21 @@ export function debugComposite(fn) { // 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. // -export function exposeDependency({ - dependency, - update = false, -}) { +export function exposeDependency({dependency}) { return { annotation: `exposeDependency`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { mapDependencies: {dependency}, compute: ({dependency}) => dependency, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1314,25 +1301,16 @@ export function exposeDependency({ // 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. -export function exposeConstant({ - value, - update = false, -}) { +// fallback default value. +export function exposeConstant({value}) { return { annotation: `exposeConstant`, - flags: {expose: true, update: !!update}, + flags: {expose: true}, expose: { options: {value}, compute: ({'#options': {value}}) => value, }, - - update: - (typeof update === 'object' - ? update - : null), }; } @@ -1427,12 +1405,24 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ // Exposes the update value of an {update: true} property as it is, // or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! +// for {mode} options! Also provide {validate} here to conveniently +// set a custom validation check for this property's update value. export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { mode: input(availabilityCheckModeInput), + validate: input({type: 'function', null: true}), + }, + + update: { + dependencies: [input.staticValue('validate')], + compute: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), }, steps: () => [ @@ -1757,6 +1747,52 @@ export function fillMissingListItems({ } } +// Filters particular values out of a list. Note that this will always +// completely skip over null, but can be used to filter out any other +// primitive or object value. +export const excludeFromList = templateCompositeFrom({ + annotation: `excludeFromList`, + + inputs: { + list: input(), + + item: input({null: true}), + items: input({validate: isArray, null: true}), + }, + + outputs: { + dependencies: [input.staticDependency('list')], + compute: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], + }, + + steps: [ + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('item'), + input('items'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: listName, + [input('list')]: listContents, + [input('item')]: excludeItem, + [input('items')]: excludeItems, + }) => continuation({ + [listName ?? '#list']: + listContents.filter(item => { + if (excludeItem !== null && item === excludeItem) return false; + if (!empty(excludeItems) && exclueItems.includes(item)) return false; + return true; + }), + }), + }, + ], +}); + // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. -- cgit 1.3.0-6-gf8a5 From 86679ee48eee7e1000b2b2f35e4c3d1a8d1be143 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 13:01:04 -0300 Subject: data: update a bunch of template composite validation --- src/data/things/composite.js | 290 +++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 135 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 011f307e..98b04a7e 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -416,110 +416,65 @@ export function templateCompositeFrom(description) { } } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; - - for (const [name, value] of Object.entries(description.inputs ?? {})) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; - } - - if (getInputTokenShape(value) !== 'input') { - wrongCallsToInput.push(name); - } - } - - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input(), got ${shape}`)); - } - }); - - descriptionAggregate.nest({message: `Errors in output descriptions for ${compositeName}`}, ({map, push}) => { - const wrongType = []; - const notPrivate = []; - - const missingDependenciesDefault = []; - const wrongDependenciesType = []; - const wrongDefaultType = []; + descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value === 'object') { - if (!('dependencies' in value && 'default' in value)) { - missingDependenciesDefault.push(name); + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); continue; } - if (!Array.isArray(value.dependencies)) { - wrongDependenciesType.push(name); + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); } - - if (typeof value.default !== 'function') { - wrongDefaultType.push(name); - } - - continue; } - if (typeof value !== 'string') { - wrongType.push(name); - continue; + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - if (!value.startsWith('#')) { - notPrivate.push(name); - continue; + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); } - } - - for (const name of wrongType) { - const type = typeof description.outputs[name]; - push(new Error(`${name}: Expected string, got ${type}`)); - } - - for (const name of notPrivate) { - const into = description.outputs[name]; - push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - - for (const name of missingDependenciesDefault) { - push(new Error(`${name}: Expected both dependencies & default`)); - } - - for (const name of wrongDependenciesType) { - const {dependencies} = description.outputs[name]; - push(new Error(`${name}: Expected dependencies to be array, got ${dependencies}`)); - } + }); + } - for (const name of wrongDefaultType) { - const type = typeof description.outputs[name].default; - push(new Error(`${name}: Expected default to be function, got ${type}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - for (const [name, value] of Object.entries(description.outputs ?? {})) { - if (typeof value !== 'object') continue; - - map( - description.outputs[name].dependencies, - decorateErrorWithIndex(dependency => { - if (!isInputToken(dependency)) { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${dependency}`); - } - - const shape = getInputTokenShape(dependency); - if (shape !== 'input.staticValue' && shape !== 'input.staticDependency') { - throw new Error(`Expected call to input.staticValue or input.staticDependency, got ${shape}`); + if (Array.isArray(description.outputs)) { + descriptionAggregate.map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); } }), - {message: `${name}: Errors in dependencies`}); + {message: `Errors in output descriptions for ${compositeName}`}); } - }); + } descriptionAggregate.close(); @@ -772,66 +727,130 @@ export function compositeFrom(description) { ? base.flags.compose : true); - if (!baseExposes) { - aggregate.push(new TypeError(`All steps, including base, must expose`)); - } + // TODO: Check description.compose ?? true instead. + const compositionNests = baseComposes; const exposeDependencies = new Set(); + const updateDescription = {}; - let anyStepsCompute = false; - let anyStepsTransform = false; + // Steps default to exposing if using a shorthand syntax where flags aren't + // specified at all. + const stepsExpose = + steps + .map(step => + (step.flags + ? step.flags.expose ?? false + : true)); + + // Steps default to composing if using a shorthand syntax where flags aren't + // specified at all - *and* aren't the base (final step), unless the whole + // composition is nestable. + const stepsCompose = + steps + .map((step, index, {length}) => + (step.flags + ? step.flags.compose ?? false + : (index === length - 1 + ? compositionNests + : true))); + + // Steps don't update unless the corresponding flag is explicitly set. + const stepsUpdate = + steps + .map(step => + (step.flags + ? step.flags.update ?? false + : false)); + + // The expose description for a step is just the entire step object, when + // using the shorthand syntax where {flags: {expose: true}} is left implied. + const stepExposeDescriptions = + steps + .map((step, index) => + (stepsExpose[index] + ? (step.flags + ? step.expose ?? null + : step) + : null)); + + // The update description for a step, if present at all, is always set + // explicitly. + const stepUpdateDescriptions = + steps + .map((step, index) => + (stepsUpdate[index] + ? step.update ?? null + : null)); + + // Indicates presence of a {compute} function on the expose description. + const stepsCompute = + stepExposeDescriptions + .map(expose => !!expose?.compute); + + // Indicates presence of a {transform} function on the expose description. + const stepsTransform = + stepExposeDescriptions + .map(expose => !!expose?.transform); + + const anyStepsExpose = + stepsExpose.includes(true); + + const anyStepsUpdate = + stepsUpdate.includes(true); + + const anyStepsCompute = + stepsCompute.includes(true); + + const anyStepsTransform = + stepsTransform.includes(true); + + const stepEntries = stitchArrays({ + step: steps, + expose: stepExposeDescriptions, + update: stepUpdateDescriptions, + stepComposes: stepsCompose, + stepComputes: stepsCompute, + stepTransforms: stepsTransform, + }); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const isBase = i === steps.length - 1; + for (let i = 0; i < stepEntries.length; i++) { + const { + step, + expose, + update, + stepComposes, + stepComputes, + stepTransforms, + } = stepEntries[i]; + + const isBase = i === stepEntries.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; - } + if (isBase && stepComposes !== compositionNests) { + return push(new TypeError( + (compositionNests + ? `Base must compose, this composition is nestable` + : `Base must not compose, this composition isn't nestable`))); + } else if (!isBase && !stepComposes) { + return push(new TypeError( + (compositionNests + ? `All steps must compose` + : `All steps (except base) must compose`))); } - const expose = - (step.flags - ? step.expose - : step); - - const stepComputes = !!expose?.compute; - const stepTransforms = !!expose?.transform; - if ( - stepTransforms && !stepComputes && - !baseUpdates && !baseComposes + !compositionNests && !anyStepsUpdate && + stepTransforms && !stepComputes ) { - 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; + return push(new TypeError( + `Steps which only transform can't be used in a composition that doesn't update`)); } + /* // 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. @@ -849,6 +868,7 @@ export function compositeFrom(description) { for (const dependency of Object.values(expose?.mapDependencies ?? {})) { exposeDependencies.add(dependency); } + */ }); } @@ -1194,13 +1214,13 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: baseUpdates, - expose: baseExposes, - compose: baseComposes, + update: anyStepsUpdate, + expose: anyStepsExpose, + compose: compositionNests, }; - if (baseUpdates) { - constructedDescriptor.update = base.update; + if (constructedDescriptor.update) { + constructedDescriptor.update = updateDescription; } if (baseExposes) { -- 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/composite.js | 86 ++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 35 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98b04a7e..2e85374f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,7 +1,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {oneOf} from '#validators'; +import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { @@ -1193,7 +1193,7 @@ export function compositeFrom(description) { case 'raiseAbove': debug(() => colors.bright(`end composition - raiseAbove`)); - return continuationIfApplicable.raise(...continuationArgs); + return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { @@ -1360,9 +1360,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ mode: input(availabilityCheckModeInput), }, - outputs: { - into: '#availability', - }, + outputs: ['#availability'], steps: () => [ { @@ -1388,7 +1386,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ break; } - return continuation({into: availability}); + return continuation({'#availability': availability}); }, }, ], @@ -1571,35 +1569,56 @@ export const withPropertyFromObject = templateCompositeFrom({ property: input({type: 'string'}), }, - outputs: { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => { - return ( - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value')); - }, + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => { + return [ + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + ]; }, steps: () => [ { - dependencies: [input('object'), input('property')], + dependencies: [ + input.staticDependency('object'), + input.staticValue('property'), + ], + + compute: (continuation, { + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, + }) => continuation({ + '#output': + (object && property + ? (object.startsWith('#') + ? `${object}.${property}` + : `#${object}.${property}`) + : '#value'), + }), + }, + + { + dependencies: [ + '#output', + input('object'), + input('property'), + ], + compute: (continuation, { + ['#output']: output, [input('object')]: object, [input('property')]: property, - }) => - (object === null - ? continuation({into: null}) - : continuation({into: object[property] ?? null})), + }) => continuation({ + [output]: + (object === null + ? null + : object[property] ?? null), + }), }, ], }); @@ -1780,14 +1799,11 @@ export const excludeFromList = templateCompositeFrom({ items: input({validate: isArray, null: true}), }, - outputs: { - dependencies: [input.staticDependency('list')], - compute: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - }, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - steps: [ + steps: () => [ { dependencies: [ input.staticDependency('list'), -- cgit 1.3.0-6-gf8a5 From e0cec3ff368175341526ff1b3c849f82e377b286 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 17:33:27 -0300 Subject: data: work together validation internals --- src/data/things/composite.js | 70 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 13 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 2e85374f..fbdc52f5 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,10 +5,11 @@ import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; import { + decorateErrorWithIndex, empty, filterProperties, openAggregate, - decorateErrorWithIndex, + stitchArrays, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -361,6 +362,8 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); + input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); @@ -400,6 +403,35 @@ function getInputTokenValue(token) { } } +function getStaticInputMetadata(inputOptions) { + const metadata = {}; + + for (const [name, token] of Object.entries(inputOptions)) { + if (typeof token === 'string') { + metadata[input.staticDependency(name)] = token; + metadata[input.staticValue(name)] = null; + } else if (isInputToken(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + + metadata[input.staticDependency(name)] = + (tokenShape === 'input.dependency' + ? tokenValue + : null); + + metadata[input.staticValue(name)] = + (tokenShape === 'input.value' + ? tokenValue + : null); + } else { + metadata[input.staticDependency(name)] = null; + metadata[input.staticValue(name)] = null; + } + } + + return metadata; +} + export function templateCompositeFrom(description) { const compositeName = (description.annotation @@ -483,11 +515,6 @@ export function templateCompositeFrom(description) { ? Object.keys(description.inputs) : []); - const expectedOutputNames = - (description.outputs - ? Object.keys(description.outputs) - : []); - const instantiate = (inputOptions = {}) => { const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); @@ -538,6 +565,13 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const expectedOutputNames = + (Array.isArray(description.outputs) + ? description.outputs + : typeof description.outputs === 'function' + ? description.outputs(getStaticInputMetadata(inputOptions)) + : []); + const outputOptions = {}; const instantiatedTemplate = { @@ -570,7 +604,7 @@ export function templateCompositeFrom(description) { } if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames}`)); + outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } for (const name of wrongTypeOutputNames) { @@ -703,6 +737,12 @@ export function compositeFrom(description) { } }; + if (!Array.isArray(composition)) { + throw new TypeError( + `Expected steps to be array, got ${typeof composition}` + + (annotation ? ` (${annotation})` : '')); + } + const base = composition.at(-1); const steps = composition.slice(); @@ -714,17 +754,17 @@ export function compositeFrom(description) { const baseExposes = (base.flags - ? base.flags.expose + ? base.flags.expose ?? false : true); const baseUpdates = (base.flags - ? base.flags.update + ? base.flags.update ?? false : false); const baseComposes = (base.flags - ? base.flags.compose + ? base.flags.compose ?? false : true); // TODO: Check description.compose ?? true instead. @@ -850,6 +890,12 @@ export function compositeFrom(description) { `Steps which only transform can't be used in a composition that doesn't update`)); } + if (update) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + Object.assign(updateDescription, update); + } + /* // Unmapped dependencies are exposed on the final composition only if // they're "public", i.e. pointing to update values of other properties @@ -1028,8 +1074,6 @@ export function compositeFrom(description) { return null; } } - - continue; } const callingTransformForThisStep = @@ -1821,7 +1865,7 @@ export const excludeFromList = templateCompositeFrom({ [listName ?? '#list']: listContents.filter(item => { if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && exclueItems.includes(item)) return false; + if (!empty(excludeItems) && excludeItems.includes(item)) return false; return true; }), }), -- 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/composite.js | 113 +++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 43 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fbdc52f5..83879c54 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,9 +1,15 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; -import {isArray, oneOf} from '#validators'; import {TupleMap} from '#wiki-data'; +import { + isArray, + isWholeNumber, + oneOf, + validateArrayItems, +} from '#validators'; + import { decorateErrorWithIndex, empty, @@ -1876,72 +1882,93 @@ export const excludeFromList = templateCompositeFrom({ // Flattens an array with one level of nested arrays, providing as dependencies // both the flattened array as well as the original starting indices of each // successive source array. -export function withFlattenedArray({ - from, - into = '#flattenedArray', - intoIndices = '#flattenedIndices', -}) { - return { - annotation: `withFlattenedArray`, - flags: {expose: true, compose: true}, +export const withFlattenedList = templateCompositeFrom({ + annotation: `withFlattenedList`, - expose: { - mapDependencies: {from}, - mapContinuation: {into, intoIndices}, + inputs: { + list: input({type: 'array'}), + }, - compute({from: sourceArray}, continuation) { - const into = sourceArray.flat(); - const intoIndices = []; + outputs: ['#flattenedList', '#flattenedIndices'], + steps: () => [ + { + dependencies: [input('list')], + compute(continuation, { + [input('list')]: sourceList, + }) { + const flattenedList = sourceList.flat(); + const indices = []; let lastEndIndex = 0; for (const {length} of sourceArray) { - intoIndices.push(lastEndIndex); + indices.push(lastEndIndex); lastEndIndex += length; } - return continuation({into, intoIndices}); + return continuation({ + ['#flattenedList']: flattenedList, + ['#flattenedIndices']: indices, + }); }, }, - }; -} + ], +}); // After mapping the contents of a flattened array in-place (being careful to // retain the original indices by replacing unmatched results with null instead // of filtering them out), this function allows for recombining them. It will // filter out null and undefined items by default (pass {filter: false} to // disable this). -export function withUnflattenedArray({ - from, - fromIndices = '#flattenedIndices', - into = '#unflattenedArray', - filter = true, -}) { - return { - annotation: `withUnflattenedArray`, - flags: {expose: true, compose: true}, +export const withUnflattenedList = templateCompositeFrom({ + annotation: `withUnflattenedList`, - expose: { - mapDependencies: {from, fromIndices}, - mapContinuation: {into}, - compute({from, fromIndices}, continuation) { - const arrays = []; + inputs: { + list: input({ + type: 'array', + defaultDependency: '#flattenedList', + }), + + indices: input({ + validate: validateArrayItems(isWholeNumber), + defaultDependency: '#flattenedIndices', + }), + + filter: input({ + type: 'boolean', + defaultValue: true, + }), + }, - for (let i = 0; i < fromIndices.length; i++) { - const startIndex = fromIndices[i]; + outputs: ['#unflattenedList'], + + steps: () => [ + { + dependencies: [input('list'), input('indices')], + compute({ + [input('list')]: list, + [input('indices')]: indices, + [input('filter')]: filter, + }) { + const unflattenedList = []; + + for (let i = 0; i < indices.length; i++) { + const startIndex = indices[i]; const endIndex = - (i === fromIndices.length - 1 - ? from.length - : fromIndices[i + 1]); + (i === indices.length - 1 + ? list.length + : indices[i + 1]); - const values = from.slice(startIndex, endIndex); - arrays.push( + const values = list.slice(startIndex, endIndex); + unflattenedList.push( (filter ? values.filter(value => value !== null && value !== undefined) : values)); } - return continuation({into: arrays}); + return continuation({ + ['#unflattenedList']: unflattenedList, + }); }, }, - }; -} + ], +}); -- 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/composite.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 83879c54..e2dbc70b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1693,7 +1693,7 @@ export function withPropertiesFromObject({ mapDependencies: {object}, options: {prefix, properties}, - compute: ({object, '#options': {prefix, properties}}, continuation) => + compute: (continuation, {object, '#options': {prefix, properties}}) => continuation( Object.fromEntries( properties.map(property => [ @@ -1729,7 +1729,7 @@ export function withPropertyFromList({ mapContinuation: {into}, options: {property}, - compute({list, '#options': {property}}, continuation) { + compute(continuation, {list, '#options': {property}}) { if (list === undefined || empty(list)) { return continuation({into: []}); } @@ -1765,7 +1765,7 @@ export function withPropertiesFromList({ mapDependencies: {list}, options: {prefix, properties}, - compute({list, '#options': {prefix, properties}}, continuation) { + compute(continuation, {list, '#options': {prefix, properties}}) { const lists = Object.fromEntries( properties.map(property => [`${prefix}.${property}`, []])); @@ -1811,7 +1811,7 @@ export function fillMissingListItems({ mapDependencies: {list, dependency}, mapContinuation: {into}, - compute: ({list, dependency}, continuation) => + compute: (continuation, {list, dependency}) => continuation({ into: list.map(item => item ?? dependency), }), @@ -1827,7 +1827,7 @@ export function fillMissingListItems({ mapContinuation: {into}, options: {value}, - compute: ({list, '#options': {value}}, continuation) => + compute: (continuation, {list, '#options': {value}}) => continuation({ into: list.map(item => item ?? value), }), -- cgit 1.3.0-6-gf8a5 From 66544e6730bd79c9cb1c50d89421f9a08329e27d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 20 Sep 2023 18:31:30 -0300 Subject: data: make composite work --- src/data/things/composite.js | 195 +++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 74 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e2dbc70b..aa383db9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + unique, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -638,6 +639,10 @@ export function templateCompositeFrom(description) { finalDescription.annotation = description.annotation; } + if ('compose' in description) { + finalDescription.compose = description.compose; + } + if ('update' in description) { finalDescription.update = description.update; } @@ -700,7 +705,7 @@ export function templateCompositeFrom(description) { steps, decorateErrorWithIndex(step => (step.symbol === templateCompositeFrom.symbol - ? step.toResolvedComposition() + ? compositeFrom(step.toResolvedComposition()) : step)), {message: `Errors resolving steps`}); @@ -723,7 +728,7 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); export function compositeFrom(description) { - const {annotation, steps: composition} = description; + const {annotation} = description; const debug = fn => { if (compositeFrom.debug === true) { @@ -743,12 +748,37 @@ export function compositeFrom(description) { } }; - if (!Array.isArray(composition)) { + if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof composition}` + + `Expected steps to be array, got ${typeof description.steps}` + (annotation ? ` (${annotation})` : '')); } + const composition = + description.steps.map(step => + ('toResolvedComposition' in step + ? compositeFrom(step.toResolvedComposition()) + : step)); + + const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + + // These dependencies were all provided by the composition which this one is + // nested inside, so input('name')-shaped tokens are going to be evaluated + // in the context of the containing composition. + const dependenciesFromInputs = + Object.values(description.inputs ?? {}) + .map(token => { + switch (getInputTokenShape(token)) { + case 'input.dependency': + return getInputTokenValue(token); + case 'input': + return token; + default: + return null; + } + }) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -758,23 +788,8 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - const baseExposes = - (base.flags - ? base.flags.expose ?? false - : true); - - const baseUpdates = - (base.flags - ? base.flags.update ?? false - : false); - - const baseComposes = - (base.flags - ? base.flags.compose ?? false - : true); - // TODO: Check description.compose ?? true instead. - const compositionNests = baseComposes; + const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); const updateDescription = {}; @@ -838,6 +853,18 @@ export function compositeFrom(description) { stepExposeDescriptions .map(expose => !!expose?.transform); + const dependenciesFromSteps = + unique( + stepExposeDescriptions + .flatMap(expose => expose?.dependencies ?? []) + .map(dependency => + (typeof dependency === 'string' + ? dependency + : getInputTokenShape(dependency) === 'input.dependency' + ? getInputTokenValue(dependency) + : null)) + .filter(Boolean)); + const anyStepsExpose = stepsExpose.includes(true); @@ -924,39 +951,12 @@ export function compositeFrom(description) { }); } - if (!baseComposes && !baseUpdates && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute`)); + if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute or update`)); } aggregate.close(); - function _filterDependencies(availableDependencies, { - dependencies, - mapDependencies, - options, - }) { - if (!dependencies && !mapDependencies && !options) { - return null; - } - - const filteredDependencies = - (dependencies - ? filterProperties(availableDependencies, dependencies) - : {}); - - if (mapDependencies) { - for (const [into, from] of Object.entries(mapDependencies)) { - filteredDependencies[into] = availableDependencies[from] ?? null; - } - } - - if (options) { - filteredDependencies['#options'] = options; - } - - return filteredDependencies; - } - function _assignDependencies(continuationAssignment, {mapContinuation}) { if (!mapContinuation) { return continuationAssignment; @@ -998,7 +998,7 @@ export function compositeFrom(description) { return continuationSymbol; }; - if (baseComposes) { + if (compositionNests) { const makeRaiseLike = returnWith => (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { @@ -1033,6 +1033,31 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; + // console.log('input description:', description.inputs); + const inputValues = + ('inputs' in description + ? Object.fromEntries(Object.entries(description.inputs) + .map(([name, token]) => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return [input(name), initialDependencies[tokenValue]]; + case 'input.value': + return [input(name), tokenValue]; + case 'input.updateValue': + return [input(name), initialValue]; + case 'myself': + return [input(name), myself]; + case 'input': + return [input(name), initialDependencies[token]]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + })) + : {}); + // console.log('input values:', inputValues); + if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { @@ -1087,7 +1112,12 @@ export function compositeFrom(description) { let continuationStorage; - const filteredDependencies = _filterDependencies(availableDependencies, expose); + const filteredDependencies = + filterProperties({ + ...availableDependencies, + ...inputMetadata, + ...inputValues, + }, expose.dependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, @@ -1106,9 +1136,17 @@ export function compositeFrom(description) { const naturalEvaluate = () => { const [name, ...args] = getExpectedEvaluation(); - let continuation; - ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](...args, continuation); + + if (isBase && !compositionNests) { + return expose[name](...args); + } else { + let continuation; + + ({continuation, continuationStorage} = + _prepareContinuation(callingTransformForThisStep)); + + return expose[name](continuation, ...args); + } } switch (step.cache) { @@ -1166,7 +1204,7 @@ export function compositeFrom(description) { if (result !== continuationSymbol) { debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); - if (baseComposes) { + if (compositionNests) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } @@ -1183,7 +1221,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (baseComposes) { + if (composiitonNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1273,26 +1311,35 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (baseExposes) { + if (anyStepsExpose) { const expose = constructedDescriptor.expose = {}; - expose.dependencies = Array.from(exposeDependencies); - - 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; - if (base.cacheComposition) expose.cache = base.cacheComposition; - } else if (baseUpdates) { - expose.transform = transformFn; + + expose.dependencies = + unique([ + ...dependenciesFromInputs, + ...dependenciesFromSteps, + ]); + + if (compositionNests) { + if (anyStepsTransform) { + expose.transform = (value, continuation, dependencies) => + _computeOrTransform(value, dependencies, continuation); + } + + if (anyStepsCompute) { + expose.compute = (continuation, dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, continuation); + } + + if (base.cacheComposition) { + expose.cache = base.cacheComposition; + } + } else if (anyStepsUpdate) { + expose.transform = (value, dependencies) => + _computeOrTransform(value, dependencies, null); } else { - expose.compute = computeFn; + expose.compute = (dependencies) => + _computeOrTransform(noTransformSymbol, dependencies, null); } } -- cgit 1.3.0-6-gf8a5 From 572b5465f9ce1e992e0384aa92461ec11dbaabff Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 11:04:33 -0300 Subject: data: make composites work --- src/data/things/composite.js | 232 ++++++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 104 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index aa383db9..cbbe6f8f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -651,7 +651,10 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - const description = description_; + // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes + const description = getInputTokenValue(description_); + const tokenShape = getInputTokenShape(description_); + if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { finalInputs[name] = input.dependency(inputOptions[name]); @@ -659,9 +662,9 @@ export function templateCompositeFrom(description) { finalInputs[name] = inputOptions[name]; } } else if (description.defaultValue) { - finalInputs[name] = input.value(defaultValue); + finalInputs[name] = input.value(description.defaultValue); } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(defaultValue); + finalInputs[name] = input.dependency(description.defaultDependency); } else { finalInputs[name] = input.value(null); } @@ -673,11 +676,11 @@ export function templateCompositeFrom(description) { if ('outputs' in description) { const finalOutputs = {}; - for (const [name, defaultDependency] of Object.entries(description.outputs)) { + for (const name of expectedOutputNames) { if (name in outputOptions) { finalOutputs[name] = outputOptions[name]; } else { - finalOutputs[name] = defaultDependency; + finalOutputs[name] = name; } } @@ -740,7 +743,7 @@ export function compositeFrom(description) { if (Array.isArray(result)) { console.log(label, ...result.map(value => (typeof value === 'object' - ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity}) + ? inspect(value, {depth: 1, colors: true, compact: true, breakLength: Infinity}) : value))); } else { console.log(label, result); @@ -762,16 +765,37 @@ export function compositeFrom(description) { const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + function _mapDependenciesToOutputs(providedDependencies) { + if (!description.outputs) { + return {}; + } + + if (!providedDependencies) { + return {}; + } + + return ( + Object.fromEntries( + Object.entries(description.outputs) + .map(([continuationName, outputName]) => [ + outputName, + providedDependencies[continuationName], + ]))); + } + // These dependencies were all provided by the composition which this one is // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = Object.values(description.inputs ?? {}) .map(token => { - switch (getInputTokenShape(token)) { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { case 'input.dependency': - return getInputTokenValue(token); + return tokenValue; case 'input': + case 'input.updateValue': return token; default: return null; @@ -779,6 +803,9 @@ export function compositeFrom(description) { }) .filter(Boolean); + const anyInputsUseUpdateValue = + dependenciesFromInputs.includes(input.updateValue()); + const base = composition.at(-1); const steps = composition.slice(); @@ -857,14 +884,30 @@ export function compositeFrom(description) { unique( stepExposeDescriptions .flatMap(expose => expose?.dependencies ?? []) - .map(dependency => - (typeof dependency === 'string' - ? dependency - : getInputTokenShape(dependency) === 'input.dependency' - ? getInputTokenValue(dependency) - : null)) + .map(dependency => { + if (typeof dependency === 'string') + return (dependency.startsWith('#') ? null : dependency); + + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input.dependency': + return (tokenValue.startsWith('#') ? null : tokenValue); + case 'input.myself': + return 'this'; + default: + return null; + } + }) .filter(Boolean)); + const anyStepsUseUpdateValue = + stepExposeDescriptions + .some(expose => + (expose?.dependencies + ? expose.dependencies.includes(input.updateValue()) + : false)); + const anyStepsExpose = stepsExpose.includes(true); @@ -877,6 +920,14 @@ export function compositeFrom(description) { const anyStepsTransform = stepsTransform.includes(true); + const compositionExposes = + anyStepsExpose; + + const compositionUpdates = + anyInputsUseUpdateValue || + anyStepsUseUpdateValue || + anyStepsUpdate; + const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, @@ -916,7 +967,7 @@ export function compositeFrom(description) { } if ( - !compositionNests && !anyStepsUpdate && + !compositionNests && !compositionUpdates && stepTransforms && !stepComputes ) { return push(new TypeError( @@ -928,26 +979,6 @@ export function compositeFrom(description) { // interesting things, like combining validation functions. Object.assign(updateDescription, update); } - - /* - // 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); - } - */ }); } @@ -957,20 +988,6 @@ export function compositeFrom(description) { aggregate.close(); - function _assignDependencies(continuationAssignment, {mapContinuation}) { - if (!mapContinuation) { - return continuationAssignment; - } - - const assignDependencies = {}; - - for (const [from, into] of Object.entries(mapContinuation)) { - assignDependencies[into] = continuationAssignment[from] ?? null; - } - - return assignDependencies; - } - function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, @@ -1013,8 +1030,8 @@ export function compositeFrom(description) { return continuationSymbol; }); - continuation.raise = makeRaiseLike('raise'); - continuation.raiseAbove = makeRaiseLike('raiseAbove'); + continuation.raiseOutput = makeRaiseLike('raiseOutput'); + continuation.raiseOutputAbove = makeRaiseLike('raiseOutputAbove'); } return {continuation, continuationStorage}; @@ -1023,7 +1040,7 @@ export function compositeFrom(description) { const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; let valueSoFar = @@ -1033,7 +1050,6 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; - // console.log('input description:', description.inputs); const inputValues = ('inputs' in description ? Object.fromEntries(Object.entries(description.inputs) @@ -1046,9 +1062,12 @@ export function compositeFrom(description) { case 'input.value': return [input(name), tokenValue]; case 'input.updateValue': - return [input(name), initialValue]; - case 'myself': - return [input(name), myself]; + if (!expectingTransform) { + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + } + return [input(name), valueSoFar]; + case 'input.myself': + return [input(name), initialDependencies['this']]; case 'input': return [input(name), initialDependencies[token]]; default: @@ -1056,7 +1075,6 @@ export function compositeFrom(description) { } })) : {}); - // console.log('input values:', inputValues); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1117,36 +1135,51 @@ export function compositeFrom(description) { ...availableDependencies, ...inputMetadata, ...inputValues, - }, expose.dependencies); + ... + (callingTransformForThisStep + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies['this'], + }, expose.dependencies ?? []); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + `with dependencies:`, filteredDependencies, + ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; const getExpectedEvaluation = () => (callingTransformForThisStep ? (filteredDependencies - ? ['transform', valueSoFar, filteredDependencies] - : ['transform', valueSoFar]) + ? ['transform', valueSoFar, continuationSymbol, filteredDependencies] + : ['transform', valueSoFar, continuationSymbol]) : (filteredDependencies - ? ['compute', filteredDependencies] - : ['compute'])); + ? ['compute', continuationSymbol, filteredDependencies] + : ['compute', continuationSymbol])); const naturalEvaluate = () => { - const [name, ...args] = getExpectedEvaluation(); + const [name, ...argsLayout] = getExpectedEvaluation(); + + let args; if (isBase && !compositionNests) { - return expose[name](...args); + args = + argsLayout.filter(arg => arg !== continuationSymbol); } else { let continuation; ({continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep)); - return expose[name](continuation, ...args); + args = + argsLayout.map(arg => + (arg === continuationSymbol + ? continuation + : arg)); } + + return expose[name](...args); } switch (step.cache) { @@ -1221,7 +1254,7 @@ export function compositeFrom(description) { debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); debug(() => colors.bright(`end composition - exit (explicit)`)); - if (composiitonNests) { + if (compositionNests) { return continuationIfApplicable.exit(providedValue); } else { return providedValue; @@ -1230,36 +1263,24 @@ export function compositeFrom(description) { 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); + if (expectingTransform) { + continuationArgs.push( + (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null)); + } debug(() => { const base = `step #${i+1} - result: ` + returnedWith; const parts = []; if (callingTransformForThisStep) { - if (continuingWithValue === undefined) { - parts.push(`(no value)`); - } else { - parts.push(`value:`, providedValue); - } + parts.push('value:', providedValue); } - if (continuingWithDependencies !== null) { - parts.push(`deps:`, continuingWithDependencies); + if (providedDependencies !== null) { + parts.push(`deps:`, providedDependencies); } else { parts.push(`(no deps)`); } @@ -1272,23 +1293,26 @@ export function compositeFrom(description) { }); switch (returnedWith) { - case 'raise': + case 'raiseOutput': debug(() => (isBase - ? colors.bright(`end composition - raise (base: explicit)`) - : colors.bright(`end composition - raise`))); + ? colors.bright(`end composition - raiseOutput (base: explicit)`) + : colors.bright(`end composition - raiseOutput`))); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); - case 'raiseAbove': - debug(() => colors.bright(`end composition - raiseAbove`)); + case 'raiseOutputAbove': + debug(() => colors.bright(`end composition - raiseOutputAbove`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable.raiseOutput(...continuationArgs); case 'continuation': if (isBase) { - debug(() => colors.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raiseOutput (inferred)`)); + continuationArgs.push(_mapDependenciesToOutputs(providedDependencies)); return continuationIfApplicable(...continuationArgs); } else { - Object.assign(availableDependencies, continuingWithDependencies); + Object.assign(availableDependencies, providedDependencies); break; } } @@ -1302,8 +1326,8 @@ export function compositeFrom(description) { } constructedDescriptor.flags = { - update: anyStepsUpdate, - expose: anyStepsExpose, + update: compositionUpdates, + expose: compositionExposes, compose: compositionNests, }; @@ -1311,7 +1335,7 @@ export function compositeFrom(description) { constructedDescriptor.update = updateDescription; } - if (anyStepsExpose) { + if (compositionExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = @@ -1321,25 +1345,25 @@ export function compositeFrom(description) { ]); if (compositionNests) { - if (anyStepsTransform) { + if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, dependencies, continuation); + _computeOrTransform(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, continuation); + _computeOrTransform(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { expose.cache = base.cacheComposition; } - } else if (anyStepsUpdate) { + } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, dependencies, null); + _computeOrTransform(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, dependencies, null); + _computeOrTransform(noTransformSymbol, null, dependencies); } } -- 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/composite.js | 142 ++++++++++++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 43 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cbbe6f8f..cfa557de 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,6 +5,7 @@ import {TupleMap} from '#wiki-data'; import { isArray, + isString, isWholeNumber, oneOf, validateArrayItems, @@ -1426,17 +1427,24 @@ export function debugComposite(fn) { // compositional step, the property will be exposed as undefined instead // of null. // -export function exposeDependency({dependency}) { - return { - annotation: `exposeDependency`, - flags: {expose: true}, +export const exposeDependency = templateCompositeFrom({ + annotation: `exposeDependency`, - expose: { - mapDependencies: {dependency}, - compute: ({dependency}) => dependency, + compose: false, + + inputs: { + dependency: input.staticDependency(), + }, + + steps: () => [ + { + dependencies: [input('dependency')], + compute: ({ + [input('dependency')]: dependency + }) => dependency, }, - }; -} + ], +}); // Exposes a constant value exactly as it is; like exposeDependency, this // is typically the base of a composition serving as a particular property @@ -1488,7 +1496,7 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ dependencies: [input('from'), input('mode')], compute: (continuation, { - [input('from')]: dependency, + [input('from')]: value, [input('mode')]: mode, }) => { let availability; @@ -1591,7 +1599,7 @@ export const exitWithoutDependency = templateCompositeFrom({ { dependencies: ['#availability', input('value')], - continuation: (continuation, { + compute: (continuation, { ['#availability']: availability, [input('value')]: value, }) => @@ -1628,9 +1636,13 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ inputs: { dependency: input(), mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input('dependency'), @@ -1657,9 +1669,13 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ inputs: { mode: input(availabilityCheckModeInput), - output: input({defaultValue: {}}), + output: input.staticValue({defaultValue: {}}), }, + outputs: ({ + [input.staticValue('output')]: output, + }) => Object.keys(output), + steps: () => [ withResultOfAvailabilityCheck({ from: input.updateValue(), @@ -1820,41 +1836,81 @@ export function withPropertyFromList({ // Gets the listed properties from each of a list of objects, providing lists // of property values each into a dependency prefixed with the same name as the // list (by default). Like withPropertyFromList, this doesn't alter indices. -export function withPropertiesFromList({ - list, - properties, - prefix = - (list.startsWith('#') - ? list - : `#${list}`), -}) { - return { - annotation: `withPropertiesFromList`, - flags: {expose: true, compose: true}, +export const withPropertiesFromList = templateCompositeFrom({ + annotation: `withPropertiesFromList`, - expose: { - mapDependencies: {list}, - options: {prefix, properties}, + inputs: { + list: input({type: 'array'}), + + properties: input({ + validate: validateArrayItems(isString), + }), - compute(continuation, {list, '#options': {prefix, properties}}) { - const lists = + prefix: input({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`)) + : '#lists'), + + steps: () => [ + { + dependencies: [input('list'), input('properties')], + compute: (continuation, { + [input('list')]: list, + [input('properties')]: properties, + }) => continuation({ + ['#lists']: Object.fromEntries( - properties.map(property => [`${prefix}.${property}`, []])); + properties.map(property => [ + property, + list.map(item => item[property] ?? null), + ])), + }), + }, - for (const item of list) { - for (const property of properties) { - lists[`${prefix}.${property}`].push( - (item === null || item === undefined - ? null - : item[property] ?? null)); - } - } + { + dependencies: [ + input.staticDependency('list'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#lists', + ], - return continuation(lists); - } - } - } -} + compute: (continuation, { + [input.staticDependency('list')]: list, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#lists']: lists, + }) => + (properties + ? continuation( + Object.fromEntries( + properties.map(property => [ + (prefix + ? `${prefix}.${property}` + : list + ? `${list}.${property}` + : `#list.${property}`), + lists[property], + ]))) + : continuation({'#lists': lists})), + }, + ], +}); // Replaces items of a list, which are null or undefined, with some fallback // value, either a constant (set {value}) or from a dependency ({dependency}). -- cgit 1.3.0-6-gf8a5 From 6ca1e4b3ad691478e94f09cfe94683cb079f6bdf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:20:19 -0300 Subject: data: update withPropertiesFromObject --- src/data/things/composite.js | 103 ++++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 26 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index cfa557de..4be01a55 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1764,34 +1764,85 @@ export const withPropertyFromObject = templateCompositeFrom({ // as a dependency prefixed with the same name as the object (by default). // If the object itself is null, all provided dependencies will be null; // if it's missing only select properties, those will be provided as null. -export function withPropertiesFromObject({ - object, - properties, - prefix = - (object.startsWith('#') - ? object - : `#${object}`), -}) { - return { - annotation: `withPropertiesFromObject`, - flags: {expose: true, compose: true}, +export const withPropertiesFromObject = templateCompositeFrom({ + annotation: `withPropertiesFromObject`, - expose: { - mapDependencies: {object}, - options: {prefix, properties}, + inputs: { + object: input({ + type: 'object', + null: true, + }), - compute: (continuation, {object, '#options': {prefix, properties}}) => - continuation( - Object.fromEntries( - properties.map(property => [ - `${prefix}.${property}`, - (object === null || object === undefined - ? null - : object[property] ?? null), - ]))), + properties: input({ + validate: validateArrayItems(isString), + }), + + prefix: input.staticValue({ + type: 'string', + null: true, + }), + }, + + outputs: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + }) => + (properties + ? properties.map(property => + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`)) + : '#object'), + + steps: () => [ + { + dependencies: [input('object'), input('properties')], + compute: (continuation, { + [input('object')]: object, + [input('properties')]: properties, + }) => continuation({ + ['#entries']: + (object === null + ? properties.map(property => [property, null]) + : properties.map(property => [property, object[property]])), + }), }, - }; -} + + { + dependencies: [ + input.staticDependency('object'), + input.staticValue('properties'), + input.staticValue('prefix'), + '#entries', + ], + + compute: ({ + [input.staticDependency('object')]: object, + [input.staticValue('properties')]: properties, + [input.staticValue('prefix')]: prefix, + ['#entries']: entries, + }) => + (properties + ? continuation( + Object.fromEntries( + entries.map(([property, value]) => [ + (prefix + ? `${prefix}.${property}` + : object + ? `${object}.${property}` + : `#object.${property}`), + value ?? null, + ]))) + : continuation({ + ['#object']: + Object.fromEntries(entries), + })), + }, + ], +}); // Gets a property from each of a list of objects (in a dependency) and // provides the results. This doesn't alter any list indices, so positions @@ -1846,7 +1897,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input({ + prefix: input.staticValue({ type: 'string', null: true, }), -- cgit 1.3.0-6-gf8a5 From ee3b52cfe889eb514f5d6a5f78297875f278e206 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 14:37:20 -0300 Subject: data: update exposeConstant, fillMissingListItems --- src/data/things/composite.js | 103 ++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 54 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4be01a55..40f4fc16 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1451,17 +1451,24 @@ export const exposeDependency = templateCompositeFrom({ // descriptor. It generally follows steps which will conditionally early // exit with some other value, with the exposeConstant base serving as the // fallback default value. -export function exposeConstant({value}) { - return { - annotation: `exposeConstant`, - flags: {expose: true}, +export const exposeConstant = templateCompositeFrom({ + annotation: `exposeConstant`, - expose: { - options: {value}, - compute: ({'#options': {value}}) => value, + compose: false, + + inputs: { + value: input.staticValue(), + }, + + steps: () => [ + { + dependencies: [input('value')], + compute: ({ + [input('value')]: value, + }) => value, }, - }; -} + ], +}); // Checks the availability of a dependency and provides the result to later // steps under '#availability' (by default). This is mainly intended for use @@ -1964,55 +1971,43 @@ export const withPropertiesFromList = templateCompositeFrom({ }); // Replaces items of a list, which are null or undefined, with some fallback -// value, either a constant (set {value}) or from a dependency ({dependency}). -// By default, this replaces the passed dependency. -export function fillMissingListItems({ - list, - value, - dependency, - into = list, -}) { - if (value !== undefined && dependency !== undefined) { - throw new TypeError(`Don't provide both value and dependency`); - } +// value. By default, this replaces the passed dependency. +export const fillMissingListItems = templateCompositeFrom({ + annotation: `fillMissingListItems`, - if (value === undefined && dependency === undefined) { - throw new TypeError(`Missing value or dependency`); - } - - if (dependency) { - return { - annotation: `fillMissingListItems.fromDependency`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list, dependency}, - mapContinuation: {into}, + inputs: { + list: input({type: 'array'}), + fill: input(), + }, - compute: (continuation, {list, dependency}) => - continuation({ - into: list.map(item => item ?? dependency), - }), - }, - }; - } else { - return { - annotation: `fillMissingListItems.fromValue`, - flags: {expose: true, compose: true}, + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#list'], - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {value}, + steps: () => [ + { + dependencies: [input('list'), input('fill')], + compute: (continuation, { + [input('list')]: list, + [input('fill')]: fill, + }) => continuation({ + ['#filled']: + list.map(item => item ?? fill), + }), + }, - compute: (continuation, {list, '#options': {value}}) => - continuation({ - into: list.map(item => item ?? value), - }), - }, - }; - } -} + { + dependencies: [input.staticDependency('list'), '#filled'], + compute: (continuation, { + [input.staticDependency('list')]: list, + ['#filled']: filled, + }) => continuation({ + [list ?? '#list']: + filled, + }), + }, + ], +}); // Filters particular values out of a list. Note that this will always // completely skip over null, but can be used to filter out any other -- 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/composite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 40f4fc16..38b7bcc9 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2073,7 +2073,7 @@ export const withFlattenedList = templateCompositeFrom({ const flattenedList = sourceList.flat(); const indices = []; let lastEndIndex = 0; - for (const {length} of sourceArray) { + for (const {length} of sourceList) { indices.push(lastEndIndex); lastEndIndex += length; } @@ -2116,8 +2116,8 @@ export const withUnflattenedList = templateCompositeFrom({ steps: () => [ { - dependencies: [input('list'), input('indices')], - compute({ + dependencies: [input('list'), input('indices'), input('filter')], + compute(continuation, { [input('list')]: list, [input('indices')]: indices, [input('filter')]: filter, -- 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/composite.js | 48 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 38b7bcc9..f744f604 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -573,13 +573,22 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.close(); + const inputMetadata = getStaticInputMetadata(inputOptions); + const expectedOutputNames = (Array.isArray(description.outputs) ? description.outputs : typeof description.outputs === 'function' - ? description.outputs(getStaticInputMetadata(inputOptions)) + ? description.outputs(inputMetadata) : []); + const ownUpdateDescription = + (typeof description.update === 'object' + ? description.update + : typeof description.update === 'function' + ? description.update(inputMetadata) + : null); + const outputOptions = {}; const instantiatedTemplate = { @@ -644,8 +653,8 @@ export function templateCompositeFrom(description) { finalDescription.compose = description.compose; } - if ('update' in description) { - finalDescription.update = description.update; + if (ownUpdateDescription) { + finalDescription.update = ownUpdateDescription; } if ('inputs' in description) { @@ -820,7 +829,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); - const updateDescription = {}; // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. @@ -932,7 +940,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, expose: stepExposeDescriptions, - update: stepUpdateDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -942,7 +949,6 @@ export function compositeFrom(description) { const { step, expose, - update, stepComposes, stepComputes, stepTransforms, @@ -974,12 +980,6 @@ export function compositeFrom(description) { return push(new TypeError( `Steps which only transform can't be used in a composition that doesn't update`)); } - - if (update) { - // TODO: This is a dumb assign statement, and it could probably do more - // interesting things, like combining validation functions. - Object.assign(updateDescription, update); - } }); } @@ -1332,8 +1332,13 @@ export function compositeFrom(description) { compose: compositionNests, }; - if (constructedDescriptor.update) { - constructedDescriptor.update = updateDescription; + if (compositionUpdates) { + // TODO: This is a dumb assign statement, and it could probably do more + // interesting things, like combining validation functions. + constructedDescriptor.update = + Object.assign( + {...description.update ?? {}}, + ...stepUpdateDescriptions.filter(Boolean)); } if (compositionExposes) { @@ -1569,15 +1574,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ validate: input({type: 'function', null: true}), }, - update: { - dependencies: [input.staticValue('validate')], - compute: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - }, + update: ({ + [input.staticValue('validate')]: validate, + }) => + (validate + ? {validate} + : {}), steps: () => [ exposeDependencyOrContinue({ -- cgit 1.3.0-6-gf8a5 From cb124756780e41c6791981233da4b56c031d6142 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:52:46 -0300 Subject: data: support update description in input.updateValue() --- src/data/things/composite.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index f744f604..e6cc267a 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -369,7 +369,15 @@ export function input(nameOrDescription) { input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = () => Symbol.for('hsmusic.composite.input.updateValue'); +input.updateValue = (description = null) => + (description + ? { + symbol: input.symbol, + shape: 'input.updateValue', + value: description, + } + : Symbol.for('hsmusic.composite.input.updateValue')); + input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); @@ -814,7 +822,9 @@ export function compositeFrom(description) { .filter(Boolean); const anyInputsUseUpdateValue = - dependenciesFromInputs.includes(input.updateValue()); + dependenciesFromInputs + .filter(dependency => isInputToken(dependency)) + .some(token => getInputTokenShape(token) === 'input.updateValue'); const base = composition.at(-1); const steps = composition.slice(); @@ -871,13 +881,22 @@ export function compositeFrom(description) { : null)); // The update description for a step, if present at all, is always set - // explicitly. + // explicitly. There may be multiple per step - namely that step's own + // {update} description, and any descriptions passed as the value in an + // input.updateValue({...}) token. const stepUpdateDescriptions = steps .map((step, index) => (stepsUpdate[index] - ? step.update ?? null - : null)); + ? [ + step.update ?? null, + ...(stepExposeDescriptions[index]?.dependencies ?? []) + .filter(dependency => isInputToken(dependency)) + .filter(token => getInputTokenShape(token) === 'input.updateValue') + .map(token => getInputTokenValue(token)) + .filter(Boolean), + ] + : [])); // Indicates presence of a {compute} function on the expose description. const stepsCompute = @@ -1338,7 +1357,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, - ...stepUpdateDescriptions.filter(Boolean)); + ...stepUpdateDescriptions.flat()); } if (compositionExposes) { -- cgit 1.3.0-6-gf8a5 From 8e3e15be98d43c1aa8a4f13709106f6848a0a9e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:53:35 -0300 Subject: data: use error.cause for nested composite compute errors --- src/data/things/composite.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e6cc267a..4074aef7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1369,15 +1369,27 @@ export function compositeFrom(description) { ...dependenciesFromSteps, ]); + const _wrapper = (...args) => { + try { + return _computeOrTransform(...args); + } catch (thrownError) { + const error = new Error( + `Error computing composition` + + (annotation ? ` ${annotation}` : '')); + error.cause = thrownError; + throw error; + } + }; + if (compositionNests) { if (compositionUpdates) { expose.transform = (value, continuation, dependencies) => - _computeOrTransform(value, continuation, dependencies); + _wrapper(value, continuation, dependencies); } if (anyStepsCompute) { expose.compute = (continuation, dependencies) => - _computeOrTransform(noTransformSymbol, continuation, dependencies); + _wrapper(noTransformSymbol, continuation, dependencies); } if (base.cacheComposition) { @@ -1385,10 +1397,10 @@ export function compositeFrom(description) { } } else if (compositionUpdates) { expose.transform = (value, dependencies) => - _computeOrTransform(value, null, dependencies); + _wrapper(value, null, dependencies); } else { expose.compute = (dependencies) => - _computeOrTransform(noTransformSymbol, null, dependencies); + _wrapper(noTransformSymbol, null, dependencies); } } -- cgit 1.3.0-6-gf8a5 From 998cc860302e3fb1e7a40c055e8ac66f195b1366 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 15:54:34 -0300 Subject: data: withResultOfAvailabilityCheck: handle undefined in 'empty' --- src/data/things/composite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 4074aef7..700cc922 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1512,8 +1512,8 @@ export const exposeConstant = templateCompositeFrom({ // Customize {mode} to select one of these modes, or default to 'null': // // * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null nor an empty array. -// This will outright error for undefined. +// * 'empty': Check that the value is neither null, undefined, 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! @@ -1546,11 +1546,11 @@ export const withResultOfAvailabilityCheck = templateCompositeFrom({ switch (mode) { case 'null': - availability = value !== null && value !== undefined; + availability = value !== undefined && value !== null; break; case 'empty': - availability = !empty(value); + availability = value !== undefined && !empty(value); break; case 'falsy': -- cgit 1.3.0-6-gf8a5 From 72c526dfeee2b227400b73c3b220cf36c885b703 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 17:01:21 -0300 Subject: data: auto-prefix '#' in output names --- src/data/things/composite.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 700cc922..26df71ae 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,6 +588,10 @@ export function templateCompositeFrom(description) { ? description.outputs : typeof description.outputs === 'function' ? description.outputs(inputMetadata) + .map(name => + (name.startsWith('#') + ? name + : '#' + name)) : []); const ownUpdateDescription = @@ -797,7 +801,9 @@ export function compositeFrom(description) { Object.entries(description.outputs) .map(([continuationName, outputName]) => [ outputName, - providedDependencies[continuationName], + (continuationName in providedDependencies + ? providedDependencies[continuationName] + : providedDependencies[continuationName.replace(/^#/, '')]), ]))); } -- cgit 1.3.0-6-gf8a5 From 981a39a5f3c2a592c84f92692c204b090622aec9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 21 Sep 2023 17:01:51 -0300 Subject: data: fix input.myself() not being spotted in inputs --- src/data/things/composite.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26df71ae..7a9048c2 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -821,6 +821,8 @@ export function compositeFrom(description) { case 'input': case 'input.updateValue': return token; + case 'input.myself': + return 'this'; default: return null; } -- cgit 1.3.0-6-gf8a5 From e14ed656f5bd1577118d053317037377c1a7a818 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 22 Sep 2023 14:00:16 -0300 Subject: data: miscellaneous improvements/fixes for updating composites --- src/data/things/composite.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a9048c2..98537c95 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -869,13 +869,18 @@ export function compositeFrom(description) { ? compositionNests : true))); - // Steps don't update unless the corresponding flag is explicitly set. + // Steps update if the corresponding flag is explicitly set, if a transform + // function is provided, or if the dependencies include an input.updateValue + // token. const stepsUpdate = steps .map(step => (step.flags ? step.flags.update ?? false - : false)); + : !!step.transform || + !!step.dependencies?.some(dependency => + isInputToken(dependency) && + getInputTokenShape(dependency) === 'input.updateValue'))); // The expose description for a step is just the entire step object, when // using the shorthand syntax where {flags: {expose: true}} is left implied. @@ -901,9 +906,8 @@ export function compositeFrom(description) { ...(stepExposeDescriptions[index]?.dependencies ?? []) .filter(dependency => isInputToken(dependency)) .filter(token => getInputTokenShape(token) === 'input.updateValue') - .map(token => getInputTokenValue(token)) - .filter(Boolean), - ] + .map(token => getInputTokenValue(token)), + ].filter(Boolean) : [])); // Indicates presence of a {compute} function on the expose description. @@ -960,6 +964,7 @@ export function compositeFrom(description) { anyStepsExpose; const compositionUpdates = + 'update' in description || anyInputsUseUpdateValue || anyStepsUseUpdateValue || anyStepsUpdate; @@ -1010,8 +1015,8 @@ export function compositeFrom(description) { }); } - if (!compositionNests && !anyStepsUpdate && !anyStepsCompute) { - aggregate.push(new TypeError(`Expected at least one step to compute or update`)); + if (!compositionNests && !anyStepsCompute && !anyStepsTransform) { + aggregate.push(new TypeError(`Expected at least one step to compute or transform`)); } aggregate.close(); @@ -1341,6 +1346,7 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); + if (providedValue !== null) valueSoFar = providedValue; break; } } @@ -1395,7 +1401,7 @@ export function compositeFrom(description) { _wrapper(value, continuation, dependencies); } - if (anyStepsCompute) { + if (anyStepsCompute && !anyStepsUseUpdateValue && !anyInputsUseUpdateValue) { expose.compute = (continuation, dependencies) => _wrapper(noTransformSymbol, continuation, dependencies); } -- cgit 1.3.0-6-gf8a5 From 7f7c50e7976bebc937c302638cade5e1fd543ff4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 22 Sep 2023 14:00:43 -0300 Subject: data: improve selecting values for input tokens in dependencies --- src/data/things/composite.js | 45 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 98537c95..da2848f8 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1163,21 +1163,46 @@ export function compositeFrom(description) { let continuationStorage; + const filterableDependencies = { + ...availableDependencies, + ...inputMetadata, + ...inputValues, + ... + (expectingTransform + ? {[input.updateValue()]: valueSoFar} + : {}), + [input.myself()]: initialDependencies?.['this'] ?? null, + }; + + const selectDependencies = + (expose.dependencies ?? []).map(dependency => { + if (!isInputToken(dependency)) return dependency; + const tokenShape = getInputTokenShape(dependency); + const tokenValue = getInputTokenValue(dependency); + switch (tokenShape) { + case 'input': + case 'input.staticDependency': + case 'input.staticValue': + return dependency; + case 'input.myself': + return input.myself(); + case 'input.dependency': + return tokenValue; + case 'input.updateValue': + return input.updateValue(); + default: + throw new Error(`Unexpected token ${tokenShape} as dependency`); + } + }) + const filteredDependencies = - filterProperties({ - ...availableDependencies, - ...inputMetadata, - ...inputValues, - ... - (callingTransformForThisStep - ? {[input.updateValue()]: valueSoFar} - : {}), - [input.myself()]: initialDependencies['this'], - }, expose.dependencies ?? []); + filterProperties(filterableDependencies, selectDependencies); debug(() => [ `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies, + `selecting:`, selectDependencies, + `from available:`, filterableDependencies, ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]); let result; -- cgit 1.3.0-6-gf8a5 From 8bcae16b391762f6b533654ec06c3bf0c8770d35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 20:24:08 -0300 Subject: data, test: WIP tests for compositeFrom --- src/data/things/composite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index da2848f8..c0f0ab0b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -752,6 +752,9 @@ export function templateCompositeFrom(description) { templateCompositeFrom.symbol = Symbol(); +export const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); +export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); + export function compositeFrom(description) { const {annotation} = description; @@ -1070,9 +1073,6 @@ export function compositeFrom(description) { return {continuation, continuationStorage}; } - const continuationSymbol = Symbol.for('compositeFrom: continuation symbol'); - const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol'); - function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) { const expectingTransform = initialValue !== noTransformSymbol; -- cgit 1.3.0-6-gf8a5 From f3d98f5ea63db7f7b2155e7efb0812f025c5bcf3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 20:35:57 -0300 Subject: data, test: collate update description from composition inputs --- src/data/things/composite.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c0f0ab0b..34e550a1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -837,6 +837,16 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + // Update descriptions passed as the value in an input.updateValue() token, + // as provided as inputs for this composition. + const inputUpdateDescriptions = + Object.values(description.inputs ?? {}) + .map(token => + (getInputTokenShape(token) === 'input.updateValue' + ? getInputTokenValue(token) + : null)) + .filter(Boolean); + const base = composition.at(-1); const steps = composition.slice(); @@ -1396,6 +1406,7 @@ export function compositeFrom(description) { constructedDescriptor.update = Object.assign( {...description.update ?? {}}, + ...inputUpdateDescriptions, ...stepUpdateDescriptions.flat()); } -- cgit 1.3.0-6-gf8a5 From 84b09a42c7baf248115f596217c07871e374d1af Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:10:38 -0300 Subject: data: fix updating valueSoFar on non-transform calls --- src/data/things/composite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 34e550a1..fdb80cf3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1381,7 +1381,9 @@ export function compositeFrom(description) { return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, providedDependencies); - if (providedValue !== null) valueSoFar = providedValue; + if (callingTransformForThisStep && providedValue !== null) { + valueSoFar = providedValue; + } break; } } -- cgit 1.3.0-6-gf8a5 From 3b458e5c403054bda58733e238ab666596cc9f70 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:12:54 -0300 Subject: data: refactor/tidy input token construction --- src/data/things/composite.js | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index fdb80cf3..293952b7 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -355,35 +355,30 @@ import { const globalCompositeCache = {}; -export function input(nameOrDescription) { - if (typeof nameOrDescription === 'string') { - return Symbol.for(`hsmusic.composite.input:${nameOrDescription}`); - } else { - return { - symbol: Symbol.for('hsmusic.composite.input'), - shape: 'input', - value: nameOrDescription, - }; - } -} +const _valueIntoToken = shape => + (value = null) => + (value === null + ? Symbol.for(`hsmusic.composite.${shape}`) + : typeof value === 'string' + ? Symbol.for(`hsmusic.composite.${shape}:${value}`) + : { + symbol: Symbol.for(`hsmusic.composite.input`), + shape, + value, + }); +export const input = _valueIntoToken('input'); input.symbol = Symbol.for('hsmusic.composite.input'); -input.updateValue = (description = null) => - (description - ? { - symbol: input.symbol, - shape: 'input.updateValue', - value: description, - } - : Symbol.for('hsmusic.composite.input.updateValue')); +input.value = _valueIntoToken('input.value'); +input.dependency = _valueIntoToken('input.dependency'); input.myself = () => Symbol.for(`hsmusic.composite.input.myself`); -input.value = value => ({symbol: input.symbol, shape: 'input.value', value}); -input.dependency = name => Symbol.for(`hsmusic.composite.input.dependency:${name}`); -input.staticDependency = name => Symbol.for(`hsmusic.composite.input.staticDependency:${name}`); -input.staticValue = name => Symbol.for(`hsmusic.composite.input.staticValue:${name}`); +input.updateValue = _valueIntoToken('input.updateValue'); + +input.staticDependency = _valueIntoToken('input.staticDependency'); +input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { if (typeof token === 'object') { -- 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/composite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 293952b7..791b8360 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,10 +4,10 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + is, isArray, isString, isWholeNumber, - oneOf, validateArrayItems, } from '#validators'; @@ -1567,7 +1567,7 @@ export const exposeConstant = templateCompositeFrom({ // const availabilityCheckModeInput = { - validate: oneOf('null', 'empty', 'falsy'), + validate: is('null', 'empty', 'falsy'), defaultValue: 'null', }; -- cgit 1.3.0-6-gf8a5 From e304bebf19340b825df10a17315b534f5dca0219 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 23 Sep 2023 22:15:06 -0300 Subject: data: WIP input validation Static only, as of this commit. --- src/data/things/composite.js | 55 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 791b8360..27b345cd 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -548,7 +548,12 @@ export function templateCompositeFrom(description) { }); const wrongTypeInputNames = []; - const wrongInputCallInputNames = []; + + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; + + const validateFailedInputNames = []; + const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { if (misplacedInputNames.includes(name)) { @@ -559,6 +564,37 @@ export function templateCompositeFrom(description) { wrongTypeInputNames.push(name); continue; } + + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); + + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + } + + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + } + + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } + } + } } if (!empty(misplacedInputNames)) { @@ -569,6 +605,23 @@ export function templateCompositeFrom(description) { inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } + if (!empty(expectedStaticDependencyInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } + + if (!empty(expectedStaticValueInputNames)) { + inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } + + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + inputOptionsAggregate.push(error); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 219596b6d52443d1090c94e50244cf79d548a167 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 25 Sep 2023 08:48:19 -0300 Subject: data, test: exposeConstant, withResultOfAvailabilityCheck --- src/data/things/composite.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 27b345cd..1148687c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -721,7 +721,6 @@ export function templateCompositeFrom(description) { const finalInputs = {}; for (const [name, description_] of Object.entries(description.inputs)) { - // TODO: Validate inputOptions[name] against staticValue, staticDependency shapes const description = getInputTokenValue(description_); const tokenShape = getInputTokenShape(description_); -- 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/composite.js | 254 ++++++++++++++++++++++++++++--------------- 1 file changed, 169 insertions(+), 85 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 1148687c..0f943ec3 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -5,7 +5,6 @@ import {TupleMap} from '#wiki-data'; import { is, - isArray, isString, isWholeNumber, validateArrayItems, @@ -18,6 +17,7 @@ import { openAggregate, stitchArrays, unique, + withAggregate, } from '#sugar'; // Composes multiple compositional "steps" and a "base" to form a property @@ -443,13 +443,53 @@ function getStaticInputMetadata(inputOptions) { return metadata; } -export function templateCompositeFrom(description) { - const compositeName = +function getCompositionName(description) { + return ( (description.annotation ? description.annotation - : `unnamed composite`); + : `unnamed composite`)); +} + +function validateInputValue(value, description) { + const tokenValue = getInputTokenValue(description); + + const {acceptsNull, defaultValue, type, validate} = tokenValue || {}; - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositeName}`}); + if (value === null || value === undefined) { + if (acceptsNull || defaultValue === null) { + return true; + } else { + throw new TypeError( + (type + ? `Expected ${type}, got ${value}` + : `Expected value, got ${value}`)); + } + } + + if (type) { + // Note: null is already handled earlier in this function, so it won't + // cause any trouble here. + const typeofValue = + (typeof value === 'object' + ? Array.isArray(value) ? 'array' : 'object' + : typeof value); + + if (typeofValue !== type) { + throw new TypeError(`Expected ${type}, got ${typeofValue}`); + } + } + + if (validate) { + validate(value); + } + + return true; +} + +export function templateCompositeFrom(description) { + const compositionName = getCompositionName(description); + + const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); if ('steps' in description) { if (Array.isArray(description.steps)) { @@ -469,7 +509,7 @@ export function templateCompositeFrom(description) { break validateInputs; } - descriptionAggregate.nest({message: `Errors in input descriptions for ${compositeName}`}, ({push}) => { + descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { const missingCallsToInput = []; const wrongCallsToInput = []; @@ -515,7 +555,7 @@ export function templateCompositeFrom(description) { throw new Error(`${value}: Expected "#" at start`); } }), - {message: `Errors in output descriptions for ${compositeName}`}); + {message: `Errors in output descriptions for ${compositionName}`}); } } @@ -527,7 +567,7 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositeName}`}); + const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); const providedInputNames = Object.keys(inputOptions); @@ -543,7 +583,6 @@ export function templateCompositeFrom(description) { if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; - if (inputDescription.null === true) return false; return true; }); @@ -655,7 +694,7 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositeName}`}); + const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); const misplacedOutputNames = []; const wrongTypeOutputNames = []; @@ -718,28 +757,27 @@ export function templateCompositeFrom(description) { } if ('inputs' in description) { - const finalInputs = {}; - - for (const [name, description_] of Object.entries(description.inputs)) { - const description = getInputTokenValue(description_); - const tokenShape = getInputTokenShape(description_); + const inputMapping = {}; + for (const [name, token] of Object.entries(description.inputs)) { + const tokenValue = getInputTokenValue(token); if (name in inputOptions) { if (typeof inputOptions[name] === 'string') { - finalInputs[name] = input.dependency(inputOptions[name]); + inputMapping[name] = input.dependency(inputOptions[name]); } else { - finalInputs[name] = inputOptions[name]; + inputMapping[name] = inputOptions[name]; } - } else if (description.defaultValue) { - finalInputs[name] = input.value(description.defaultValue); - } else if (description.defaultDependency) { - finalInputs[name] = input.dependency(description.defaultDependency); + } else if (tokenValue.defaultValue) { + inputMapping[name] = input.value(tokenValue.defaultValue); + } else if (tokenValue.defaultDependency) { + inputMapping[name] = input.dependency(tokenValue.defaultDependency); } else { - finalInputs[name] = input.value(null); + inputMapping[name] = input.value(null); } } - finalDescription.inputs = finalInputs; + finalDescription.inputMapping = inputMapping; + finalDescription.inputDescriptions = description.inputs; } if ('outputs' in description) { @@ -768,7 +806,7 @@ export function templateCompositeFrom(description) { const finalDescription = {...ownDescription}; - const aggregate = openAggregate({message: `Errors resolving ${compositeName}`}); + const aggregate = openAggregate({message: `Errors resolving ${compositionName}`}); const steps = ownDescription.steps(); @@ -804,6 +842,7 @@ export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol' export function compositeFrom(description) { const {annotation} = description; + const compositionName = getCompositionName(description); const debug = fn => { if (compositeFrom.debug === true) { @@ -835,7 +874,7 @@ export function compositeFrom(description) { ? compositeFrom(step.toResolvedComposition()) : step)); - const inputMetadata = getStaticInputMetadata(description.inputs ?? {}); + const inputMetadata = getStaticInputMetadata(description.inputMapping ?? {}); function _mapDependenciesToOutputs(providedDependencies) { if (!description.outputs) { @@ -861,7 +900,7 @@ export function compositeFrom(description) { // nested inside, so input('name')-shaped tokens are going to be evaluated // in the context of the containing composition. const dependenciesFromInputs = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => { const tokenShape = getInputTokenShape(token); const tokenValue = getInputTokenValue(token); @@ -884,10 +923,41 @@ export function compositeFrom(description) { .filter(dependency => isInputToken(dependency)) .some(token => getInputTokenShape(token) === 'input.updateValue'); + const inputNames = + Object.keys(description.inputMapping ?? {}); + + const inputSymbols = + inputNames.map(name => input(name)); + + const inputsMayBeDynamicValue = + stitchArrays({ + mappingToken: Object.values(description.inputMapping ?? {}), + descriptionToken: Object.values(description.inputDescriptions ?? {}), + }).map(({mappingToken, descriptionToken}) => { + if (getInputTokenShape(descriptionToken) === 'input.staticValue') return false; + if (getInputTokenShape(mappingToken) === 'input.value') return false; + return true; + }); + + const inputDescriptions = + Object.values(description.inputDescriptions ?? {}); + + /* + const inputsAcceptNull = + Object.values(description.inputDescriptions ?? {}) + .map(token => { + const tokenValue = getInputTokenValue(token); + if (!tokenValue) return false; + if ('acceptsNull' in tokenValue) return tokenValue.acceptsNull; + if ('defaultValue' in tokenValue) return tokenValue.defaultValue === null; + return false; + }); + */ + // Update descriptions passed as the value in an input.updateValue() token, // as provided as inputs for this composition. const inputUpdateDescriptions = - Object.values(description.inputs ?? {}) + Object.values(description.inputMapping ?? {}) .map(token => (getInputTokenShape(token) === 'input.updateValue' ? getInputTokenValue(token) @@ -903,7 +973,6 @@ export function compositeFrom(description) { (annotation ? ` (${annotation})` : ''), }); - // TODO: Check description.compose ?? true instead. const compositionNests = description.compose ?? true; const exposeDependencies = new Set(); @@ -1141,30 +1210,44 @@ export function compositeFrom(description) { const availableDependencies = {...initialDependencies}; const inputValues = - ('inputs' in description - ? Object.fromEntries(Object.entries(description.inputs) - .map(([name, token]) => { - const tokenShape = getInputTokenShape(token); - const tokenValue = getInputTokenValue(token); - switch (tokenShape) { - case 'input.dependency': - return [input(name), initialDependencies[tokenValue]]; - case 'input.value': - return [input(name), tokenValue]; - case 'input.updateValue': - if (!expectingTransform) { - throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); - } - return [input(name), valueSoFar]; - case 'input.myself': - return [input(name), initialDependencies['this']]; - case 'input': - return [input(name), initialDependencies[token]]; - default: - throw new TypeError(`Unexpected input shape ${tokenShape}`); - } - })) - : {}); + Object.values(description.inputMapping ?? {}) + .map(token => { + const tokenShape = getInputTokenShape(token); + const tokenValue = getInputTokenValue(token); + switch (tokenShape) { + case 'input.dependency': + return initialDependencies[tokenValue]; + case 'input.value': + return tokenValue; + case 'input.updateValue': + if (!expectingTransform) + throw new Error(`Unexpected input.updateValue() accessed on non-transform call`); + return valueSoFar; + case 'input.myself': + return initialDependencies['this']; + case 'input': + return initialDependencies[token]; + default: + throw new TypeError(`Unexpected input shape ${tokenShape}`); + } + }); + + withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + for (const {dynamic, name, value, description} of stitchArrays({ + dynamic: inputsMayBeDynamicValue, + name: inputNames, + value: inputValues, + description: inputDescriptions, + })) { + if (!dynamic) continue; + try { + validateInputValue(value, description); + } catch (error) { + error.message = `${name}: ${error.message}`; + throw error; + } + } + }); if (expectingTransform) { debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); @@ -1220,10 +1303,15 @@ export function compositeFrom(description) { let continuationStorage; + const inputDictionary = + Object.fromEntries( + stitchArrays({symbol: inputSymbols, value: inputValues}) + .map(({symbol, value}) => [symbol, value])); + const filterableDependencies = { ...availableDependencies, ...inputMetadata, - ...inputValues, + ...inputDictionary, ... (expectingTransform ? {[input.updateValue()]: valueSoFar} @@ -1568,7 +1656,7 @@ export const exposeDependency = templateCompositeFrom({ compose: false, inputs: { - dependency: input.staticDependency(), + dependency: input.staticDependency({acceptsNull: true}), }, steps: () => [ @@ -1618,17 +1706,17 @@ export const exposeConstant = templateCompositeFrom({ // for values like zero and the empty string! // -const availabilityCheckModeInput = { +const inputAvailabilityCheckMode = () => input({ validate: is('null', 'empty', 'falsy'), defaultValue: 'null', -}; +}); export const withResultOfAvailabilityCheck = templateCompositeFrom({ annotation: `withResultOfAvailabilityCheck`, inputs: { - from: input(), - mode: input(availabilityCheckModeInput), + from: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, outputs: ['#availability'], @@ -1669,8 +1757,8 @@ export const exposeDependencyOrContinue = templateCompositeFrom({ annotation: `exposeDependencyOrContinue`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), }, steps: () => [ @@ -1700,8 +1788,12 @@ export const exposeUpdateValueOrContinue = templateCompositeFrom({ annotation: `exposeUpdateValueOrContinue`, inputs: { - mode: input(availabilityCheckModeInput), - validate: input({type: 'function', null: true}), + mode: inputAvailabilityCheckMode(), + + validate: input({ + type: 'function', + defaultValue: null, + }), }, update: ({ @@ -1725,9 +1817,9 @@ export const exitWithoutDependency = templateCompositeFrom({ annotation: `exitWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), - value: input({null: true}), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), + value: input({defaultValue: null}), }, steps: () => [ @@ -1755,7 +1847,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ annotation: `exitWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), value: input({defaultValue: null}), }, @@ -1763,6 +1855,7 @@ export const exitWithoutUpdateValue = templateCompositeFrom({ exitWithoutDependency({ dependency: input.updateValue(), mode: input('mode'), + value: input('value'), }), ], }); @@ -1773,8 +1866,8 @@ export const raiseOutputWithoutDependency = templateCompositeFrom({ annotation: `raiseOutputWithoutDependency`, inputs: { - dependency: input(), - mode: input(availabilityCheckModeInput), + dependency: input({acceptsNull: true}), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1807,7 +1900,7 @@ export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ annotation: `raiseOutputWithoutUpdateValue`, inputs: { - mode: input(availabilityCheckModeInput), + mode: inputAvailabilityCheckMode(), output: input.staticValue({defaultValue: {}}), }, @@ -1841,7 +1934,7 @@ export const withPropertyFromObject = templateCompositeFrom({ annotation: `withPropertyFromObject`, inputs: { - object: input({type: 'object', null: true}), + object: input({type: 'object', acceptsNull: true}), property: input({type: 'string'}), }, @@ -1907,19 +2000,13 @@ export const withPropertiesFromObject = templateCompositeFrom({ annotation: `withPropertiesFromObject`, inputs: { - object: input({ - type: 'object', - null: true, - }), + object: input({type: 'object', acceptsNull: true}), properties: input({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2036,10 +2123,7 @@ export const withPropertiesFromList = templateCompositeFrom({ validate: validateArrayItems(isString), }), - prefix: input.staticValue({ - type: 'string', - null: true, - }), + prefix: input.staticValue({type: 'string', defaultValue: null}), }, outputs: ({ @@ -2109,7 +2193,7 @@ export const fillMissingListItems = templateCompositeFrom({ inputs: { list: input({type: 'array'}), - fill: input(), + fill: input({acceptsNull: true}), }, outputs: ({ @@ -2150,8 +2234,8 @@ export const excludeFromList = templateCompositeFrom({ inputs: { list: input(), - item: input({null: true}), - items: input({validate: isArray, null: true}), + item: input({defaultValue: null}), + items: input({type: 'array', defaultValue: null}), }, outputs: ({ -- cgit 1.3.0-6-gf8a5 From 747df818115b4aefd2433990f2997fe4c80bc501 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 11:35:11 -0300 Subject: data: refactor most openAggregate calls -> withAggregate --- src/data/things/composite.js | 327 ++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 174 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 0f943ec3..26be4a67 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -489,77 +489,75 @@ function validateInputValue(value, description) { export function templateCompositeFrom(description) { const compositionName = getCompositionName(description); - const descriptionAggregate = openAggregate({message: `Errors in description for ${compositionName}`}); - - if ('steps' in description) { - if (Array.isArray(description.steps)) { - descriptionAggregate.push(new TypeError(`Wrap steps array in a function`)); - } else if (typeof description.steps !== 'function') { - descriptionAggregate.push(new TypeError(`Expected steps to be a function (returning an array)`)); + withAggregate({message: `Errors in description for ${compositionName}`}, ({map, nest, push}) => { + if ('steps' in description) { + if (Array.isArray(description.steps)) { + push(new TypeError(`Wrap steps array in a function`)); + } else if (typeof description.steps !== 'function') { + push(new TypeError(`Expected steps to be a function (returning an array)`)); + } } - } - validateInputs: - if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - descriptionAggregate.push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - descriptionAggregate.push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); - break validateInputs; - } + validateInputs: + if ('inputs' in description) { + if (Array.isArray(description.inputs)) { + push(new Error(`Expected inputs to be object, got array`)); + break validateInputs; + } else if (typeof description.inputs !== 'object') { + push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + break validateInputs; + } - descriptionAggregate.nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { - const missingCallsToInput = []; - const wrongCallsToInput = []; + nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => { + const missingCallsToInput = []; + const wrongCallsToInput = []; - for (const [name, value] of Object.entries(description.inputs)) { - if (!isInputToken(value)) { - missingCallsToInput.push(name); - continue; + for (const [name, value] of Object.entries(description.inputs)) { + if (!isInputToken(value)) { + missingCallsToInput.push(name); + continue; + } + + if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { + wrongCallsToInput.push(name); + } } - if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) { - wrongCallsToInput.push(name); + for (const name of missingCallsToInput) { + push(new Error(`${name}: Missing call to input()`)); } - } - for (const name of missingCallsToInput) { - push(new Error(`${name}: Missing call to input()`)); - } + for (const name of wrongCallsToInput) { + const shape = getInputTokenShape(description.inputs[name]); + push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + } + }); + } - for (const name of wrongCallsToInput) { - const shape = getInputTokenShape(description.inputs[name]); - push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`)); + validateOutputs: + if ('outputs' in description) { + if ( + !Array.isArray(description.outputs) && + typeof description.outputs !== 'function' + ) { + push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + break validateOutputs; } - }); - } - validateOutputs: - if ('outputs' in description) { - if ( - !Array.isArray(description.outputs) && - typeof description.outputs !== 'function' - ) { - descriptionAggregate.push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); - break validateOutputs; - } - - if (Array.isArray(description.outputs)) { - descriptionAggregate.map( - description.outputs, - decorateErrorWithIndex(value => { - if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) - } else if (!value.startsWith('#')) { - throw new Error(`${value}: Expected "#" at start`); - } - }), - {message: `Errors in output descriptions for ${compositionName}`}); + if (Array.isArray(description.outputs)) { + map( + description.outputs, + decorateErrorWithIndex(value => { + if (typeof value !== 'string') { + throw new Error(`${value}: Expected string, got ${typeof value}`) + } else if (!value.startsWith('#')) { + throw new Error(`${value}: Expected "#" at start`); + } + }), + {message: `Errors in output descriptions for ${compositionName}`}); + } } - } - - descriptionAggregate.close(); + }); const expectedInputNames = (description.inputs @@ -567,106 +565,104 @@ export function templateCompositeFrom(description) { : []); const instantiate = (inputOptions = {}) => { - const inputOptionsAggregate = openAggregate({message: `Errors in input options passed to ${compositionName}`}); - - const providedInputNames = Object.keys(inputOptions); - - const misplacedInputNames = - providedInputNames - .filter(name => !expectedInputNames.includes(name)); - - const missingInputNames = - expectedInputNames - .filter(name => !providedInputNames.includes(name)) - .filter(name => { - const inputDescription = description.inputs[name].value; - if (!inputDescription) return true; - if ('defaultValue' in inputDescription) return false; - if ('defaultDependency' in inputDescription) return false; - return true; - }); + withAggregate({message: `Errors in input options passed to ${compositionName}`}, ({push}) => { + const providedInputNames = Object.keys(inputOptions); + + const misplacedInputNames = + providedInputNames + .filter(name => !expectedInputNames.includes(name)); + + const missingInputNames = + expectedInputNames + .filter(name => !providedInputNames.includes(name)) + .filter(name => { + const inputDescription = description.inputs[name].value; + if (!inputDescription) return true; + if ('defaultValue' in inputDescription) return false; + if ('defaultDependency' in inputDescription) return false; + return true; + }); - const wrongTypeInputNames = []; + const wrongTypeInputNames = []; - const expectedStaticValueInputNames = []; - const expectedStaticDependencyInputNames = []; + const expectedStaticValueInputNames = []; + const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; - const validateFailedErrors = []; + const validateFailedInputNames = []; + const validateFailedErrors = []; - for (const [name, value] of Object.entries(inputOptions)) { - if (misplacedInputNames.includes(name)) { - continue; - } + for (const [name, value] of Object.entries(inputOptions)) { + if (misplacedInputNames.includes(name)) { + continue; + } - if (typeof value !== 'string' && !isInputToken(value)) { - wrongTypeInputNames.push(name); - continue; - } + if (typeof value !== 'string' && !isInputToken(value)) { + wrongTypeInputNames.push(name); + continue; + } - const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); + const descriptionShape = getInputTokenShape(description.inputs[name]); + const descriptionValue = getInputTokenValue(description.inputs[name]); - const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); - const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); + const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); + const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; + if (descriptionShape === 'input.staticValue') { + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } } - } - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; + if (descriptionShape === 'input.staticDependency') { + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } } - } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); + if (descriptionValue && 'validate' in descriptionValue) { + if (tokenShape === 'input.value') { + try { + descriptionValue.validate(tokenValue); + } catch (error) { + validateFailedInputNames.push(name); + validateFailedErrors.push(error); + } } } } - } - - if (!empty(misplacedInputNames)) { - inputOptionsAggregate.push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); - } - if (!empty(missingInputNames)) { - inputOptionsAggregate.push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); - } + if (!empty(misplacedInputNames)) { + push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`)); + } - if (!empty(expectedStaticDependencyInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); - } + if (!empty(missingInputNames)) { + push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); + } - if (!empty(expectedStaticValueInputNames)) { - inputOptionsAggregate.push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); - } + if (!empty(expectedStaticDependencyInputNames)) { + push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - inputOptionsAggregate.push(error); - } + if (!empty(expectedStaticValueInputNames)) { + push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + } - for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - inputOptionsAggregate.push(new Error(`${name}: Expected string or input() call, got ${type}`)); - } + for (const {name, validationError} of stitchArrays({ + name: validateFailedInputNames, + validationError: validateFailedErrors, + })) { + const error = new Error(`${name}: Validation failed for static value`); + error.cause = validationError; + push(error); + } - inputOptionsAggregate.close(); + for (const name of wrongTypeInputNames) { + const type = typeof inputOptions[name]; + push(new Error(`${name}: Expected string or input() call, got ${type}`)); + } + }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -694,48 +690,31 @@ export function templateCompositeFrom(description) { symbol: templateCompositeFrom.symbol, outputs(providedOptions) { - const outputOptionsAggregate = openAggregate({message: `Errors in output options passed to ${compositionName}`}); - - const misplacedOutputNames = []; - const wrongTypeOutputNames = []; - // const notPrivateOutputNames = []; + withAggregate({message: `Errors in output options passed to ${compositionName}`}, ({push}) => { + const misplacedOutputNames = []; + const wrongTypeOutputNames = []; + + for (const [name, value] of Object.entries(providedOptions)) { + if (!expectedOutputNames.includes(name)) { + misplacedOutputNames.push(name); + continue; + } - for (const [name, value] of Object.entries(providedOptions)) { - if (!expectedOutputNames.includes(name)) { - misplacedOutputNames.push(name); - continue; + if (typeof value !== 'string') { + wrongTypeOutputNames.push(name); + continue; + } } - if (typeof value !== 'string') { - wrongTypeOutputNames.push(name); - continue; + if (!empty(misplacedOutputNames)) { + push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); } - /* - if (!value.startsWith('#')) { - notPrivateOutputNames.push(name); - continue; + for (const name of wrongTypeOutputNames) { + const type = typeof providedOptions[name]; + push(new Error(`${name}: Expected string, got ${type}`)); } - */ - } - - if (!empty(misplacedOutputNames)) { - outputOptionsAggregate.push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`)); - } - - for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected string, got ${type}`)); - } - - /* - for (const name of notPrivateOutputNames) { - const into = providedOptions[name]; - outputOptionsAggregate.push(new Error(`${name}: Expected "#" at start, got ${into}`)); - } - */ - - outputOptionsAggregate.close(); + }); Object.assign(outputOptions, providedOptions); return instantiatedTemplate; -- cgit 1.3.0-6-gf8a5 From 1e09cfe3fcaa3f6e020e50ce49ea77c254b04dfd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 11:51:16 -0300 Subject: data: reuse validateInputValue for static inputs --- src/data/things/composite.js | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 26be4a67..e58b6524 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -588,7 +588,6 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; - const validateFailedInputNames = []; const validateFailedErrors = []; for (const [name, value] of Object.entries(inputOptions)) { @@ -602,7 +601,6 @@ export function templateCompositeFrom(description) { } const descriptionShape = getInputTokenShape(description.inputs[name]); - const descriptionValue = getInputTokenValue(description.inputs[name]); const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); @@ -621,14 +619,12 @@ export function templateCompositeFrom(description) { } } - if (descriptionValue && 'validate' in descriptionValue) { - if (tokenShape === 'input.value') { - try { - descriptionValue.validate(tokenValue); - } catch (error) { - validateFailedInputNames.push(name); - validateFailedErrors.push(error); - } + if (tokenShape === 'input.value') { + try { + validateInputValue(tokenValue, description.inputs[name]); + } catch (error) { + error.message = `${name}: ${error.message}`; + validateFailedErrors.push(error); } } } @@ -649,19 +645,14 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } - for (const {name, validationError} of stitchArrays({ - name: validateFailedInputNames, - validationError: validateFailedErrors, - })) { - const error = new Error(`${name}: Validation failed for static value`); - error.cause = validationError; - push(error); - } - for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); } + + for (const error of validateFailedErrors) { + push(error); + } }); const inputMetadata = getStaticInputMetadata(inputOptions); @@ -1211,7 +1202,7 @@ export function compositeFrom(description) { } }); - withAggregate({message: `Errors in dynamic input values provided to ${compositionName}`}, ({push}) => { + withAggregate({message: `Errors in input values provided to ${compositionName}`}, ({push}) => { for (const {dynamic, name, value, description} of stitchArrays({ dynamic: inputsMayBeDynamicValue, name: inputNames, @@ -1223,7 +1214,7 @@ export function compositeFrom(description) { validateInputValue(value, description); } catch (error) { error.message = `${name}: ${error.message}`; - throw error; + push(error); } } }); -- cgit 1.3.0-6-gf8a5 From d719eff73be9b18a3c83b984e68469c3be91457c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 13:19:52 -0300 Subject: data: compositeFrom: validate static token shapes for normal input --- src/data/things/composite.js | 49 +++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index e58b6524..de6827c6 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -576,7 +576,7 @@ export function templateCompositeFrom(description) { expectedInputNames .filter(name => !providedInputNames.includes(name)) .filter(name => { - const inputDescription = description.inputs[name].value; + const inputDescription = getInputTokenValue(description.inputs[name]); if (!inputDescription) return true; if ('defaultValue' in inputDescription) return false; if ('defaultDependency' in inputDescription) return false; @@ -587,6 +587,7 @@ export function templateCompositeFrom(description) { const expectedStaticValueInputNames = []; const expectedStaticDependencyInputNames = []; + const expectedValueProvidingTokenInputNames = []; const validateFailedErrors = []; @@ -605,18 +606,33 @@ export function templateCompositeFrom(description) { const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null); const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null); - if (descriptionShape === 'input.staticValue') { - if (tokenShape !== 'input.value') { - expectedStaticValueInputNames.push(name); - continue; - } - } + switch (descriptionShape) { + case'input.staticValue': + if (tokenShape !== 'input.value') { + expectedStaticValueInputNames.push(name); + continue; + } + break; - if (descriptionShape === 'input.staticDependency') { - if (typeof value !== 'string' && tokenShape !== 'input.dependency') { - expectedStaticDependencyInputNames.push(name); - continue; - } + case 'input.staticDependency': + if (typeof value !== 'string' && tokenShape !== 'input.dependency') { + expectedStaticDependencyInputNames.push(name); + continue; + } + break; + + case 'input': + if (typeof value !== 'string' && ![ + 'input', + 'input.value', + 'input.dependency', + 'input.myself', + 'input.updateValue', + ].includes(tokenShape)) { + expectedValueProvidingTokenInputNames.push(name); + continue; + } + break; } if (tokenShape === 'input.value') { @@ -645,6 +661,15 @@ export function templateCompositeFrom(description) { push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); } + for (const name of expectedValueProvidingTokenInputNames) { + const shapeOrType = + (isInputToken(inputOptions[name]) + ? getInputTokenShape(inputOptions[name]) + : typeof inputOptions[name]); + + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + } + for (const name of wrongTypeInputNames) { const type = typeof inputOptions[name]; push(new Error(`${name}: Expected string or input() call, got ${type}`)); -- cgit 1.3.0-6-gf8a5 From 518647f8b80ffda6d502b1a75656da7f2ae4b9d3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:00:18 -0300 Subject: data: templateCompositeFrom: improve error message consistency --- src/data/things/composite.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index de6827c6..33f49e9b 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -16,6 +16,7 @@ import { filterProperties, openAggregate, stitchArrays, + typeAppearance, unique, withAggregate, } from '#sugar'; @@ -381,7 +382,9 @@ input.staticDependency = _valueIntoToken('input.staticDependency'); input.staticValue = _valueIntoToken('input.staticValue'); function isInputToken(token) { - if (typeof token === 'object') { + if (token === null) { + return false; + } else if (typeof token === 'object') { return token.symbol === Symbol.for('hsmusic.composite.input'); } else if (typeof token === 'symbol') { return token.description.startsWith('hsmusic.composite.input'); @@ -653,26 +656,29 @@ export function templateCompositeFrom(description) { push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`)); } - if (!empty(expectedStaticDependencyInputNames)) { - push(new Error(`Expected static dependencies: ${expectedStaticDependencyInputNames.join(', ')}`)); + const inputAppearance = name => + (isInputToken(inputOptions[name]) + ? `${getInputTokenShape(inputOptions[name])}() call` + : `dependency name`); + + for (const name of expectedStaticDependencyInputNames) { + const appearance = inputAppearance(name); + push(new Error(`${name}: Expected dependency name, got ${appearance}`)); } - if (!empty(expectedStaticValueInputNames)) { - push(new Error(`Expected static values: ${expectedStaticValueInputNames.join(', ')}`)); + for (const name of expectedStaticValueInputNames) { + const appearance = inputAppearance(name) + push(new Error(`${name}: Expected input.value() call, got ${appearance}`)); } for (const name of expectedValueProvidingTokenInputNames) { - const shapeOrType = - (isInputToken(inputOptions[name]) - ? getInputTokenShape(inputOptions[name]) - : typeof inputOptions[name]); - - push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${shapeOrType}`)); + const appearance = getInputTokenShape(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${appearance}`)); } for (const name of wrongTypeInputNames) { - const type = typeof inputOptions[name]; - push(new Error(`${name}: Expected string or input() call, got ${type}`)); + const type = typeAppearance(inputOptions[name]); + push(new Error(`${name}: Expected dependency name or input() call, got ${type}`)); } for (const error of validateFailedErrors) { -- cgit 1.3.0-6-gf8a5 From 411c053dc4b314b2bc0d58d3899fd796ad0054c2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 14:11:02 -0300 Subject: data, util: use typeAppearance in more places --- src/data/things/composite.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 33f49e9b..b6009525 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -395,7 +395,7 @@ function isInputToken(token) { function getInputTokenShape(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -407,7 +407,7 @@ function getInputTokenShape(token) { function getInputTokenValue(token) { if (!isInputToken(token)) { - throw new TypeError(`Expected an input token, got ${token}`); + throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`); } if (typeof token === 'object') { @@ -464,8 +464,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${value}` - : `Expected value, got ${value}`)); + ? `Expected ${type}, got ${typeAppearance(value)}` + : `Expected value, got ${typeAppearance(value)}`)); } } @@ -478,7 +478,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeofValue}`); + throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); } } @@ -503,11 +503,11 @@ export function templateCompositeFrom(description) { validateInputs: if ('inputs' in description) { - if (Array.isArray(description.inputs)) { - push(new Error(`Expected inputs to be object, got array`)); - break validateInputs; - } else if (typeof description.inputs !== 'object') { - push(new Error(`Expected inputs to be object, got ${typeof description.inputs}`)); + if ( + Array.isArray(description.inputs) || + typeof description.inputs !== 'object' + ) { + push(new Error(`Expected inputs to be object, got ${typeAppearance(description.inputs)}`)); break validateInputs; } @@ -543,7 +543,7 @@ export function templateCompositeFrom(description) { !Array.isArray(description.outputs) && typeof description.outputs !== 'function' ) { - push(new Error(`Expected outputs to be array or function, got ${typeof description.outputs}`)); + push(new Error(`Expected outputs to be array or function, got ${typeAppearance(description.outputs)}`)); break validateOutputs; } @@ -552,7 +552,7 @@ export function templateCompositeFrom(description) { description.outputs, decorateErrorWithIndex(value => { if (typeof value !== 'string') { - throw new Error(`${value}: Expected string, got ${typeof value}`) + throw new Error(`${value}: Expected string, got ${typeAppearance(value)}`) } else if (!value.startsWith('#')) { throw new Error(`${value}: Expected "#" at start`); } @@ -733,8 +733,8 @@ export function templateCompositeFrom(description) { } for (const name of wrongTypeOutputNames) { - const type = typeof providedOptions[name]; - push(new Error(`${name}: Expected string, got ${type}`)); + const appearance = typeAppearance(providedOptions[name]); + push(new Error(`${name}: Expected string, got ${appearance}`)); } }); @@ -865,7 +865,7 @@ export function compositeFrom(description) { if (!Array.isArray(description.steps)) { throw new TypeError( - `Expected steps to be array, got ${typeof description.steps}` + + `Expected steps to be array, got ${typeAppearance(description.steps)}` + (annotation ? ` (${annotation})` : '')); } -- 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/composite.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index b6009525..eb93bd7c 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -976,8 +976,6 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; - const exposeDependencies = new Set(); - // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. const stepsExpose = @@ -1101,7 +1099,6 @@ export function compositeFrom(description) { const stepEntries = stitchArrays({ step: steps, - expose: stepExposeDescriptions, stepComposes: stepsCompose, stepComputes: stepsCompute, stepTransforms: stepsTransform, @@ -1110,7 +1107,6 @@ export function compositeFrom(description) { for (let i = 0; i < stepEntries.length; i++) { const { step, - expose, stepComposes, stepComputes, stepTransforms, @@ -2046,7 +2042,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ '#entries', ], - compute: ({ + compute: (continuation, { [input.staticDependency('object')]: object, [input.staticValue('properties')]: properties, [input.staticValue('prefix')]: prefix, -- cgit 1.3.0-6-gf8a5 From ea02f6453f697d1e9fc6cfef2cdcf454c3f4286e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 29 Sep 2023 10:09:20 -0300 Subject: data: fix & tidy dynamic outputs in utilities --- src/data/things/composite.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index eb93bd7c..7a3a8319 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1938,15 +1938,12 @@ export const withPropertyFromObject = templateCompositeFrom({ outputs: ({ [input.staticDependency('object')]: object, [input.staticValue('property')]: property, - }) => { - return [ - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - ]; - }, + }) => + (object && property + ? (object.startsWith('#') + ? [`${object}.${property}`] + : [`#${object}.${property}`]) + : ['#value']), steps: () => [ { @@ -2018,7 +2015,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ : object ? `${object}.${property}` : `#object.${property}`)) - : '#object'), + : ['#object']), steps: () => [ { @@ -2135,7 +2132,7 @@ export const withPropertiesFromList = templateCompositeFrom({ : list ? `${list}.${property}` : `#list.${property}`)) - : '#lists'), + : ['#lists']), steps: () => [ { -- cgit 1.3.0-6-gf8a5 From e4dc2be4c12a5578bfb5d5945a592907aed1cb4f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 29 Sep 2023 10:36:59 -0300 Subject: data, test: type validation message adjustments --- src/data/things/composite.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7a3a8319..c03f8833 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -4,6 +4,7 @@ import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; import { + a, is, isString, isWholeNumber, @@ -464,8 +465,8 @@ function validateInputValue(value, description) { } else { throw new TypeError( (type - ? `Expected ${type}, got ${typeAppearance(value)}` - : `Expected value, got ${typeAppearance(value)}`)); + ? `Expected ${a(type)}, got ${typeAppearance(value)}` + : `Expected a value, got ${typeAppearance(value)}`)); } } @@ -478,7 +479,7 @@ function validateInputValue(value, description) { : typeof value); if (typeofValue !== type) { - throw new TypeError(`Expected ${type}, got ${typeAppearance(value)}`); + throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`); } } @@ -1997,6 +1998,7 @@ export const withPropertiesFromObject = templateCompositeFrom({ object: input({type: 'object', acceptsNull: true}), properties: input({ + type: 'array', validate: validateArrayItems(isString), }), -- 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/composite.js | 727 +------------------------------------------ 1 file changed, 1 insertion(+), 726 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c03f8833..7e068dce 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -2,14 +2,7 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import {TupleMap} from '#wiki-data'; - -import { - a, - is, - isString, - isWholeNumber, - validateArrayItems, -} from '#validators'; +import {a} from '#validators'; import { decorateErrorWithIndex, @@ -1639,721 +1632,3 @@ export function debugComposite(fn) { compositeFrom.debug = false; return value; } - -// 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. -// -// 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. -// -export const exposeDependency = templateCompositeFrom({ - annotation: `exposeDependency`, - - compose: false, - - inputs: { - dependency: input.staticDependency({acceptsNull: true}), - }, - - steps: () => [ - { - dependencies: [input('dependency')], - compute: ({ - [input('dependency')]: dependency - }) => dependency, - }, - ], -}); - -// 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. -export const exposeConstant = templateCompositeFrom({ - annotation: `exposeConstant`, - - compose: false, - - inputs: { - value: input.staticValue(), - }, - - steps: () => [ - { - dependencies: [input('value')], - compute: ({ - [input('value')]: value, - }) => value, - }, - ], -}); - -// Checks the availability of a dependency 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 default to 'null': -// -// * 'null': Check that the value isn't null (and not undefined either). -// * 'empty': Check that the value is neither null, undefined, 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! -// - -const inputAvailabilityCheckMode = () => input({ - validate: is('null', 'empty', 'falsy'), - defaultValue: 'null', -}); - -export const withResultOfAvailabilityCheck = templateCompositeFrom({ - annotation: `withResultOfAvailabilityCheck`, - - inputs: { - from: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - outputs: ['#availability'], - - steps: () => [ - { - dependencies: [input('from'), input('mode')], - - compute: (continuation, { - [input('from')]: value, - [input('mode')]: mode, - }) => { - let availability; - - switch (mode) { - case 'null': - availability = value !== undefined && value !== null; - break; - - case 'empty': - availability = value !== undefined && !empty(value); - break; - - case 'falsy': - availability = !!value && (!Array.isArray(value) || !empty(value)); - break; - } - - return continuation({'#availability': availability}); - }, - }, - ], -}); - -// Exposes a dependency as it is, or continues if it's unavailable. -// See withResultOfAvailabilityCheck for {mode} options! -export const exposeDependencyOrContinue = templateCompositeFrom({ - annotation: `exposeDependencyOrContinue`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('dependency')], - compute: (continuation, { - ['#availability']: availability, - [input('dependency')]: dependency, - }) => - (availability - ? continuation.exit(dependency) - : continuation()), - }, - ], -}); - -// Exposes the update value of an {update: true} property as it is, -// or continues if it's unavailable. See withResultOfAvailabilityCheck -// for {mode} options! Also provide {validate} here to conveniently -// set a custom validation check for this property's update value. -export const exposeUpdateValueOrContinue = templateCompositeFrom({ - annotation: `exposeUpdateValueOrContinue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - - validate: input({ - type: 'function', - defaultValue: null, - }), - }, - - update: ({ - [input.staticValue('validate')]: validate, - }) => - (validate - ? {validate} - : {}), - - steps: () => [ - exposeDependencyOrContinue({ - dependency: input.updateValue(), - mode: input('mode'), - }), - ], -}); - -// Early exits if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutDependency = templateCompositeFrom({ - annotation: `exitWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('value')], - compute: (continuation, { - ['#availability']: availability, - [input('value')]: value, - }) => - (availability - ? continuation() - : continuation.exit(value)), - }, - ], -}); - -// Early exits if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const exitWithoutUpdateValue = templateCompositeFrom({ - annotation: `exitWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - value: input({defaultValue: null}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue(), - mode: input('mode'), - value: input('value'), - }), - ], -}); - -// Raises if a dependency isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutDependency = templateCompositeFrom({ - annotation: `raiseOutputWithoutDependency`, - - inputs: { - dependency: input({acceptsNull: true}), - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input('dependency'), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Raises if this property's update value isn't available. -// See withResultOfAvailabilityCheck for {mode} options! -export const raiseOutputWithoutUpdateValue = templateCompositeFrom({ - annotation: `raiseOutputWithoutUpdateValue`, - - inputs: { - mode: inputAvailabilityCheckMode(), - output: input.staticValue({defaultValue: {}}), - }, - - outputs: ({ - [input.staticValue('output')]: output, - }) => Object.keys(output), - - steps: () => [ - withResultOfAvailabilityCheck({ - from: input.updateValue(), - mode: input('mode'), - }), - - { - dependencies: ['#availability', input('output')], - compute: (continuation, { - ['#availability']: availability, - [input('output')]: output, - }) => - (availability - ? continuation() - : continuation.raiseOutputAbove(output)), - }, - ], -}); - -// Gets a property of some object (in a dependency) and provides that value. -// If the object itself is null, or the object doesn't have the listed property, -// the provided dependency will also be null. -export const withPropertyFromObject = templateCompositeFrom({ - annotation: `withPropertyFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - property: input({type: 'string'}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), - - steps: () => [ - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('property'), - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), - }, - - { - dependencies: [ - '#output', - input('object'), - input('property'), - ], - - compute: (continuation, { - ['#output']: output, - [input('object')]: object, - [input('property')]: property, - }) => continuation({ - [output]: - (object === null - ? null - : object[property] ?? null), - }), - }, - ], -}); - -// Gets the listed properties from some object, providing each property's value -// as a dependency prefixed with the same name as the object (by default). -// If the object itself is null, all provided dependencies will be null; -// if it's missing only select properties, those will be provided as null. -export const withPropertiesFromObject = templateCompositeFrom({ - annotation: `withPropertiesFromObject`, - - inputs: { - object: input({type: 'object', acceptsNull: true}), - - properties: input({ - type: 'array', - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`)) - : ['#object']), - - steps: () => [ - { - dependencies: [input('object'), input('properties')], - compute: (continuation, { - [input('object')]: object, - [input('properties')]: properties, - }) => continuation({ - ['#entries']: - (object === null - ? properties.map(property => [property, null]) - : properties.map(property => [property, object[property]])), - }), - }, - - { - dependencies: [ - input.staticDependency('object'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#entries', - ], - - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#entries']: entries, - }) => - (properties - ? continuation( - Object.fromEntries( - entries.map(([property, value]) => [ - (prefix - ? `${prefix}.${property}` - : object - ? `${object}.${property}` - : `#object.${property}`), - value ?? null, - ]))) - : continuation({ - ['#object']: - Object.fromEntries(entries), - })), - }, - ], -}); - -// Gets a property from each of a list of objects (in a dependency) and -// provides the results. This doesn't alter any list indices, so positions -// which were null in the original list are kept null here. Objects which don't -// have the specified property are retained in-place as null. -export function withPropertyFromList({ - list, - property, - into = null, -}) { - into ??= - (list.startsWith('#') - ? `${list}.${property}` - : `#${list}.${property}`); - - return { - annotation: `withPropertyFromList`, - flags: {expose: true, compose: true}, - - expose: { - mapDependencies: {list}, - mapContinuation: {into}, - options: {property}, - - compute(continuation, {list, '#options': {property}}) { - if (list === undefined || empty(list)) { - return continuation({into: []}); - } - - return continuation({ - into: - list.map(item => - (item === null || item === undefined - ? null - : item[property] ?? null)), - }); - }, - }, - }; -} - -// Gets the listed properties from each of a list of objects, providing lists -// of property values each into a dependency prefixed with the same name as the -// list (by default). Like withPropertyFromList, this doesn't alter indices. -export const withPropertiesFromList = templateCompositeFrom({ - annotation: `withPropertiesFromList`, - - inputs: { - list: input({type: 'array'}), - - properties: input({ - validate: validateArrayItems(isString), - }), - - prefix: input.staticValue({type: 'string', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - }) => - (properties - ? properties.map(property => - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`)) - : ['#lists']), - - steps: () => [ - { - dependencies: [input('list'), input('properties')], - compute: (continuation, { - [input('list')]: list, - [input('properties')]: properties, - }) => continuation({ - ['#lists']: - Object.fromEntries( - properties.map(property => [ - property, - list.map(item => item[property] ?? null), - ])), - }), - }, - - { - dependencies: [ - input.staticDependency('list'), - input.staticValue('properties'), - input.staticValue('prefix'), - '#lists', - ], - - compute: (continuation, { - [input.staticDependency('list')]: list, - [input.staticValue('properties')]: properties, - [input.staticValue('prefix')]: prefix, - ['#lists']: lists, - }) => - (properties - ? continuation( - Object.fromEntries( - properties.map(property => [ - (prefix - ? `${prefix}.${property}` - : list - ? `${list}.${property}` - : `#list.${property}`), - lists[property], - ]))) - : continuation({'#lists': lists})), - }, - ], -}); - -// Replaces items of a list, which are null or undefined, with some fallback -// value. By default, this replaces the passed dependency. -export const fillMissingListItems = templateCompositeFrom({ - annotation: `fillMissingListItems`, - - inputs: { - list: input({type: 'array'}), - fill: input({acceptsNull: true}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [input('list'), input('fill')], - compute: (continuation, { - [input('list')]: list, - [input('fill')]: fill, - }) => continuation({ - ['#filled']: - list.map(item => item ?? fill), - }), - }, - - { - dependencies: [input.staticDependency('list'), '#filled'], - compute: (continuation, { - [input.staticDependency('list')]: list, - ['#filled']: filled, - }) => continuation({ - [list ?? '#list']: - filled, - }), - }, - ], -}); - -// Filters particular values out of a list. Note that this will always -// completely skip over null, but can be used to filter out any other -// primitive or object value. -export const excludeFromList = templateCompositeFrom({ - annotation: `excludeFromList`, - - inputs: { - list: input(), - - item: input({defaultValue: null}), - items: input({type: 'array', defaultValue: null}), - }, - - outputs: ({ - [input.staticDependency('list')]: list, - }) => [list ?? '#list'], - - steps: () => [ - { - dependencies: [ - input.staticDependency('list'), - input('list'), - input('item'), - input('items'), - ], - - compute: (continuation, { - [input.staticDependency('list')]: listName, - [input('list')]: listContents, - [input('item')]: excludeItem, - [input('items')]: excludeItems, - }) => continuation({ - [listName ?? '#list']: - listContents.filter(item => { - if (excludeItem !== null && item === excludeItem) return false; - if (!empty(excludeItems) && excludeItems.includes(item)) return false; - return true; - }), - }), - }, - ], -}); - -// Flattens an array with one level of nested arrays, providing as dependencies -// both the flattened array as well as the original starting indices of each -// successive source array. -export const withFlattenedList = templateCompositeFrom({ - annotation: `withFlattenedList`, - - inputs: { - list: input({type: 'array'}), - }, - - outputs: ['#flattenedList', '#flattenedIndices'], - - steps: () => [ - { - dependencies: [input('list')], - compute(continuation, { - [input('list')]: sourceList, - }) { - const flattenedList = sourceList.flat(); - const indices = []; - let lastEndIndex = 0; - for (const {length} of sourceList) { - indices.push(lastEndIndex); - lastEndIndex += length; - } - - return continuation({ - ['#flattenedList']: flattenedList, - ['#flattenedIndices']: indices, - }); - }, - }, - ], -}); - -// After mapping the contents of a flattened array in-place (being careful to -// retain the original indices by replacing unmatched results with null instead -// of filtering them out), this function allows for recombining them. It will -// filter out null and undefined items by default (pass {filter: false} to -// disable this). -export const withUnflattenedList = templateCompositeFrom({ - annotation: `withUnflattenedList`, - - inputs: { - list: input({ - type: 'array', - defaultDependency: '#flattenedList', - }), - - indices: input({ - validate: validateArrayItems(isWholeNumber), - defaultDependency: '#flattenedIndices', - }), - - filter: input({ - type: 'boolean', - defaultValue: true, - }), - }, - - outputs: ['#unflattenedList'], - - steps: () => [ - { - dependencies: [input('list'), input('indices'), input('filter')], - compute(continuation, { - [input('list')]: list, - [input('indices')]: indices, - [input('filter')]: filter, - }) { - const unflattenedList = []; - - for (let i = 0; i < indices.length; i++) { - const startIndex = indices[i]; - const endIndex = - (i === indices.length - 1 - ? list.length - : indices[i + 1]); - - const values = list.slice(startIndex, endIndex); - unflattenedList.push( - (filter - ? values.filter(value => value !== null && value !== undefined) - : values)); - } - - return continuation({ - ['#unflattenedList']: unflattenedList, - }); - }, - }, - ], -}); -- cgit 1.3.0-6-gf8a5 From d2174a01dda63ba233cbcdf48bb70ed50127d54d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:30:39 -0300 Subject: data: obliterate composite.js explainer Poor (read: largely outdated) code documentation is worse than no code documentation. The various infrastructural systems specially designed for hsmusic should get more dedicated reference material, but that can't well be written before the systems are tested and used for longer. The compositional data processing style has just about settled, but it's still very young (compared to, say, the overarching data- to-page flow, content functions, or the HTML and content template systems). --- src/data/things/composite.js | 333 ------------------------------------------- 1 file changed, 333 deletions(-) (limited to 'src/data/things/composite.js') diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7e068dce..51525bc1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -15,339 +15,6 @@ import { withAggregate, } from '#sugar'; -// 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: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: 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: -// -// Track.coverArtists = -// compositeFrom([ -// doSomethingWhichMightEarlyExit(), -// -// withResolvedContribs({ -// from: 'coverArtistContribsByRef', -// into: '#coverArtistContribs', -// }), -// -// { -// flags: {expose: true}, -// expose: { -// dependencies: ['#coverArtistContribs'], -// compute: ({'#coverArtistContribs': coverArtistContribs}) => -// 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: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: 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: -// -// withResolvedContribs = ({from, to}) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {from}, -// mapContinuation: {into}, -// compute({artistData, from: contribsByRef}, continuation) { -// if (!artistData) return null; -// return continuation({ -// into: (..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 compositeFrom() 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! -// - const globalCompositeCache = {}; const _valueIntoToken = shape => -- cgit 1.3.0-6-gf8a5