diff options
Diffstat (limited to 'src/data/things')
-rw-r--r-- | src/data/things/thing.js | 402 |
1 files changed, 230 insertions, 172 deletions
diff --git a/src/data/things/thing.js b/src/data/things/thing.js index cd62288e..782946ce 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -743,7 +743,7 @@ export default class Thing extends CacheableObject { } const base = composition.at(-1); - const steps = composition.slice(0, -1); + const steps = composition.slice(); const aggregate = openAggregate({ message: @@ -751,78 +751,118 @@ export default class Thing extends CacheableObject { (annotation ? ` (${annotation})` : ''), }); - if (base.flags.compose && base.flags.compute) { - push(new TypeError(`Base which composes can't also update yet`)); - } + const baseExposes = + (base.flags + ? base.flags.expose + : true); - const exposeSteps = []; - const exposeDependencies = new Set(); + const baseUpdates = + (base.flags + ? base.flags.update + : false); - if (base.expose?.dependencies) { - for (const dependency of base.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } - } + const baseComposes = + (base.flags + ? base.flags.compose + : true); - if (base.expose?.mapDependencies) { - for (const dependency of Object.values(base.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (!baseExposes) { + aggregate.push(new TypeError(`All steps, including base, must expose`)); } + const exposeDependencies = new Set(); + + let anyStepsCompute = false; + let anyStepsTransform = false; + for (let i = 0; i < steps.length; i++) { const step = steps[i]; + const isBase = i === steps.length - 1; const message = - (step.annotation - ? `Errors in step #${i + 1} (${step.annotation})` - : `Errors in step #${i + 1}`); + `Errors in step #${i + 1}` + + (isBase ? ` (base)` : ``) + + (step.annotation ? ` (${step.annotation})` : ``); aggregate.nest({message}, ({push}) => { - if (!step.flags.compose) { - push(new TypeError(`Steps (all but bottom item) must be {compose: true}`)); - } + if (step.flags) { + let flagsErrored = false; - if (step.flags.update) { - push(new Error(`Steps which update aren't supported yet`)); - } - - if (step.flags.expose) expose: { - if (!step.expose.transform && !step.expose.compute) { - push(new TypeError(`Steps which expose must provide at least one of transform or compute`)); - break expose; + if (!step.flags.compose && !isBase) { + push(new TypeError(`All steps but base must compose`)); + flagsErrored = true; } - if ( - step.expose.transform && - !step.expose.compute && - !base.flags.update && - !base.flags.compose - ) { - push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); - break expose; + if (!step.flags.expose) { + push(new TypeError(`All steps must expose`)); + flagsErrored = true; } - if (step.expose.dependencies) { - for (const dependency of step.expose.dependencies) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + if (flagsErrored) { + return; } + } - if (step.expose.mapDependencies) { - for (const dependency of Object.values(step.expose.mapDependencies)) { - if (typeof dependency === 'string' && dependency.startsWith('#')) continue; - exposeDependencies.add(dependency); - } + const expose = + (step.flags + ? step.expose + : step); + + const stepComputes = !!expose.compute; + const stepTransforms = !!expose.transform; + + if (!stepComputes && !stepTransforms) { + push(new TypeError(`Steps must provide compute or transform (or both)`)); + return; + } + + if ( + stepTransforms && !stepComputes && + !baseUpdates && !baseComposes + ) { + push(new TypeError(`Steps which only transform can't be composed with a non-updating base`)); + return; + } + + if (stepComputes) { + anyStepsCompute = true; + } + + if (stepTransforms) { + anyStepsTransform = true; + } + + // Unmapped dependencies are exposed on the final composition only if + // they're "public", i.e. pointing to update values of other properties + // on the CacheableObject. + for (const dependency of expose.dependencies ?? []) { + if (typeof dependency === 'string' && dependency.startsWith('#')) { + continue; } - exposeSteps.push(step); + exposeDependencies.add(dependency); + } + + // Mapped dependencies are always exposed on the final composition. + // These are explicitly for reading values which are named outside of + // the current compositional step. + for (const dependency of Object.values(expose.mapDependencies ?? {})) { + exposeDependencies.add(dependency); } }); } + if (!baseComposes) { + if (baseUpdates) { + if (!anyStepsTransform) { + push(new TypeError(`Expected at least one step to transform`)); + } + } else { + if (!anyStepsCompute) { + push(new TypeError(`Expected at least one step to compute`)); + } + } + } + aggregate.close(); const constructedDescriptor = {}; @@ -832,64 +872,68 @@ export default class Thing extends CacheableObject { } constructedDescriptor.flags = { - update: !!base.flags.update, - expose: !!base.flags.expose, - compose: !!base.flags.compose, + update: baseUpdates, + expose: baseExposes, + compose: baseComposes, }; - if (base.flags.update) { + if (baseUpdates) { constructedDescriptor.update = base.update; } - if (base.flags.expose) { + if (baseExposes) { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); const continuationSymbol = Symbol('continuation symbol'); const noTransformSymbol = Symbol('no-transform symbol'); - function _filterDependencies(dependencies, step) { + function _filterDependencies(availableDependencies, { + dependencies, + mapDependencies, + options, + }) { const filteredDependencies = - (step.expose.dependencies - ? filterProperties(dependencies, step.expose.dependencies) + (dependencies + ? filterProperties(availableDependencies, dependencies) : {}); - if (step.expose.mapDependencies) { - for (const [to, from] of Object.entries(step.expose.mapDependencies)) { - filteredDependencies[to] = dependencies[from] ?? null; + if (mapDependencies) { + for (const [to, from] of Object.entries(mapDependencies)) { + filteredDependencies[to] = availableDependencies[from] ?? null; } } - if (step.expose.options) { - filteredDependencies['#options'] = step.expose.options; + if (options) { + filteredDependencies['#options'] = options; } return filteredDependencies; } - function _assignDependencies(continuationAssignment, step) { - if (!step.expose.mapContinuation) { + function _assignDependencies(continuationAssignment, {mapContinuation}) { + if (!mapContinuation) { return continuationAssignment; } const assignDependencies = {}; - for (const [from, to] of Object.entries(step.expose.mapContinuation)) { + for (const [from, to] of Object.entries(mapContinuation)) { assignDependencies[to] = continuationAssignment[from] ?? null; } return assignDependencies; } - function _prepareContinuation(transform) { + function _prepareContinuation(callingTransformForThisStep) { const continuationStorage = { returnedWith: null, - providedDependencies: null, - providedValue: null, + providedDependencies: undefined, + providedValue: undefined, }; const continuation = - (transform + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { continuationStorage.returnedWith = 'continuation'; continuationStorage.providedDependencies = providedDependencies; @@ -908,150 +952,166 @@ export default class Thing extends CacheableObject { return continuationSymbol; }; - if (base.flags.compose) { - continuation.raise = - (transform + if (baseComposes) { + const makeRaiseLike = returnWith => + (callingTransformForThisStep ? (providedValue, providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; continuationStorage.providedValue = providedValue; return continuationSymbol; } : (providedDependencies = null) => { - continuationStorage.returnedWith = 'raise'; + continuationStorage.returnedWith = returnWith; continuationStorage.providedDependencies = providedDependencies; return continuationSymbol; }); + + continuation.raise = makeRaiseLike('raise'); + continuation.raiseAbove = makeRaiseLike('raiseAbove'); } return {continuation, continuationStorage}; } - function _computeOrTransform(value, initialDependencies, continuationIfApplicable) { - const dependencies = {...initialDependencies}; + function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) { + const expectingTransform = initialValue !== noTransformSymbol; - let valueSoFar = value; // Set only for {update: true} compositions - let exportDependencies = null; // Set only for {compose: true} compositions + let valueSoFar = + (expectingTransform + ? initialValue + : undefined); - debug(() => color.bright(`begin composition`)); + const availableDependencies = {...initialDependencies}; - stepLoop: for (let i = 0; i < exposeSteps.length; i++) { - const step = exposeSteps[i]; - debug(() => [`step #${i+1}:`, step]); + if (expectingTransform) { + debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + } else { + debug(() => color.bright(`begin composition - not transforming`)); + } - const transform = - valueSoFar !== noTransformSymbol && - step.expose.transform; + stepLoop: for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const isBase = i === steps.length - 1; - const filteredDependencies = _filterDependencies(dependencies, step); - const {continuation, continuationStorage} = _prepareContinuation(transform); + debug(() => [ + `step #${i+1}` + + (isBase + ? ` (base):` + : ` of ${steps.length}:`), + step]); + + const expose = + (step.flags + ? step.expose + : step); + + const callingTransformForThisStep = + expectingTransform && expose.transform; + + const filteredDependencies = _filterDependencies(availableDependencies, expose); + const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep); debug(() => [ - `step #${i+1} - ${transform ? 'transform' : 'compute'}`, + `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`, `with dependencies:`, filteredDependencies]); const result = - (transform + (callingTransformForThisStep ? step.expose.transform(valueSoFar, filteredDependencies, continuation) : step.expose.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { - if (base.flags.compose) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]); + + if (baseComposes) { + throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => [`step #${i+1} - early-exit (inferred) ->`, result]); - debug(() => color.bright(`end composition`)); + debug(() => color.bright(`end composition - exit (inferred)`)); + return result; } - switch (continuationStorage.returnedWith) { - case 'exit': - debug(() => [`step #${i+1} - result: early-exit (explicit) ->`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; - - case 'raise': - debug(() => `step #${i+1} - result: raise`); - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step) ?? {}; - if (transform) valueSoFar = continuationStorage.providedValue; - break stepLoop; + const {returnedWith} = continuationStorage; - case 'continuation': - if (transform) { - valueSoFar = continuationStorage.providedValue; - } + if (returnedWith === 'exit') { + const {providedValue} = continuationStorage; - if (continuationStorage.providedDependencies) { - const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); - Object.assign(dependencies, assignDependencies); - debug(() => `step #${i+1} - result: continuation`); - debug(() => [`assign dependencies:`, assignDependencies]); - } else { - debug(() => `step #${i+1} - result: continuation (no provided dependencies)`); - } + debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); + debug(() => color.bright(`end composition - exit (explicit)`)); - break; + if (baseComposes) { + return continuationIfApplicable.exit(providedValue); + } else { + return providedValue; + } } - } - if (exportDependencies) { - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - - debug(() => `completed all steps, reached base`); + const {providedValue, providedDependencies} = continuationStorage; - const filteredDependencies = _filterDependencies(dependencies, base); + const continuingWithValue = + (expectingTransform + ? (callingTransformForThisStep + ? providedValue ?? null + : valueSoFar ?? null) + : undefined); - const transform = - valueSoFar !== noTransformSymbol && - base.expose.transform; + const continuingWithDependencies = + (providedDependencies + ? _assignDependencies(providedDependencies, expose) + : null); - debug(() => [ - `base - ${transform ? 'transform' : 'compute'}`, - `with dependencies:`, filteredDependencies]); + const continuationArgs = []; + if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue); + if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies); - if (base.flags.compose) { - const {continuation, continuationStorage} = _prepareContinuation(transform); - - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies, continuation) - : base.expose.compute(filteredDependencies, continuation)); + debug(() => { + const base = `step #${i+1} - result: ` + returnedWith; + const parts = []; - if (result !== continuationSymbol) { - throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); - } + if (callingTransformForThisStep) { + if (continuingWithValue === undefined) { + parts.push(`(no value)`); + } else { + parts.push(`value:`, providedValue); + } + } - switch (continuationStorage.returnedWith) { - case 'continuation': - throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + if (continuingWithDependencies !== null) { + parts.push(`deps:`, continuingWithDependencies); + } else { + parts.push(`(no deps)`); + } - case 'exit': - debug(() => `base - result: early-exit (explicit)`); - debug(() => [`early-exit:`, continuationStorage.providedValue]); - debug(() => color.bright(`end composition`)); - return continuationStorage.providedValue; + if (empty(parts)) { + return base; + } else { + return [base + ' ->', ...parts]; + } + }); + switch (returnedWith) { case 'raise': - exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base); - debug(() => `base - result: raise`); - debug(() => [`raise dependencies:`, exportDependencies]); - debug(() => color.bright(`end composition`)); - return continuationIfApplicable(exportDependencies); - } - } else { - const result = - (transform - ? base.expose.transform(valueSoFar, filteredDependencies) - : base.expose.compute(filteredDependencies)); + debug(() => + (isBase + ? color.bright(`end composition - raise (base: explicit)`) + : color.bright(`end composition - raise`))); + return continuationIfApplicable(...continuationArgs); - debug(() => [`base - non-compose (final) result:`, result]); - debug(() => color.bright(`end composition`)); + case 'raiseAbove': + debug(() => color.bright(`end composition - raiseAbove`)); + return continuationIfApplicable.raise(...continuationArgs); - return result; + case 'continuation': + if (isBase) { + debug(() => color.bright(`end composition - raise (inferred)`)); + return continuationIfApplicable(...continuationArgs); + } else { + Object.assign(availableDependencies, continuingWithDependencies); + break; + } + } } } @@ -1063,12 +1123,10 @@ export default class Thing extends CacheableObject { (initialDependencies, continuationIfApplicable) => _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable); - if (base.flags.compose) { - if (exposeSteps.some(step => step.expose.transform)) { - expose.transform = transformFn; - } - expose.compute = computeFn; - } else if (base.flags.update) { + if (baseComposes) { + if (anyStepsTransform) expose.transform = transformFn; + if (anyStepsCompute) expose.compute = computeFn; + } else if (baseUpdates) { expose.transform = transformFn; } else { expose.compute = computeFn; @@ -1229,7 +1287,7 @@ export default class Thing extends CacheableObject { switch (mode) { case 'null': return value !== null; case 'empty': return !empty(value); - case 'falsy': return !empty(value) && !!value; + case 'falsy': return !!value && (!Array.isArray(value) || !empty(value)); default: return false; } }; |