summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2021-06-27 13:11:26 -0300
committer(quasar) nebula <towerofnix@gmail.com>2021-06-27 13:11:34 -0300
commit73bbd2853e7a6ecc143d9d71dcca522e84375e30 (patch)
treeb74cef0a95d96ad627bd2780fd2198129d4aa046
parent0d31ffdd127d4a199b8944ddb81f9013c45cd83c (diff)
aggregate shenanigans left uncommitted
-rw-r--r--src/thing/album.js21
-rw-r--r--src/thing/structures.js13
-rw-r--r--src/util/sugar.js85
3 files changed, 108 insertions, 11 deletions
diff --git a/src/thing/album.js b/src/thing/album.js
index be37489..e99cfc3 100644
--- a/src/thing/album.js
+++ b/src/thing/album.js
@@ -1,6 +1,7 @@
import Thing from './thing.js';
import {
+ validateDirectory,
validateReference
} from './structures.js';
@@ -10,22 +11,33 @@ import {
} from '../util/sugar.js';
export default class Album extends Thing {
+ #directory = null;
#tracks = [];
static updateError = {
+ directory: Thing.extendPropertyError('directory'),
tracks: Thing.extendPropertyError('tracks')
};
update(source) {
- withAggregate(({ wrap, call, map }) => {
- if (source.tracks) {
- this.#tracks = map(source.tracks, t => validateReference('track')(t) && t, {
- errorClass: this.constructor.updateError.tracks
+ const err = this.constructor.updateError;
+
+ withAggregate(({ nest, filter, throws }) => {
+
+ if (source.directory) {
+ nest(throws(err.directory), ({ call }) => {
+ if (call(validateDirectory, source.directory)) {
+ this.#directory = source.directory;
+ }
});
}
+
+ if (source.tracks)
+ this.#tracks = filter(source.tracks, validateReference('track'), throws(err.tracks));
});
}
+ get directory() { return this.#directory; }
get tracks() { return this.#tracks; }
}
@@ -35,6 +47,7 @@ console.log('tracks (before):', album.tracks);
try {
album.update({
+ directory: 'oh yes',
tracks: [
'lol',
123,
diff --git a/src/thing/structures.js b/src/thing/structures.js
index e1bf06c..89c9bd3 100644
--- a/src/thing/structures.js
+++ b/src/thing/structures.js
@@ -1,5 +1,18 @@
// Generic structure utilities common across various Thing types.
+export function validateDirectory(directory) {
+ if (typeof directory !== 'string')
+ throw new TypeError(`Expected a string, got ${directory}`);
+
+ if (directory.length === 0)
+ throw new TypeError(`Expected directory to be non-zero length`);
+
+ if (directory.match(/[^a-zA-Z0-9\-]/))
+ throw new TypeError(`Expected only letters, numbers, and dash, got "${directory}"`);
+
+ return true;
+}
+
export function validateReference(type = '') {
return ref => {
if (typeof ref !== 'string')
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 54b2df0..38c8047 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -104,15 +104,18 @@ export function openAggregate({
// Anything passed here should probably extend from that! May be used for
// letting callers programatically distinguish between multiple aggregate
// errors.
- errorClass = AggregateError,
+ //
+ // This should be provided using the aggregateThrows utility function.
+ [openAggregate.errorClassSymbol]: errorClass = AggregateError,
// Optional human-readable message to describe the aggregate error, if
// constructed.
message = '',
- // Value to return when a provided function throws an error. (This is
- // primarily useful when wrapping a function and then providing it to
- // another utility, e.g. array.map().)
+ // Value to return when a provided function throws an error. If this is a
+ // function, it will be called with the arguments given to the function.
+ // (This is primarily useful when wrapping a function and then providing it
+ // to another utility, e.g. array.map().)
returnOnFail = null
} = {}) {
const errors = [];
@@ -124,7 +127,9 @@ export function openAggregate({
return fn(...args);
} catch (error) {
errors.push(error);
- return returnOnFail;
+ return (typeof returnOnFail === 'function'
+ ? returnOnFail(...args)
+ : returnOnFail);
}
};
@@ -132,6 +137,10 @@ export function openAggregate({
return aggregate.wrap(fn)(...args);
};
+ aggregate.nest = (...args) => {
+ return aggregate.call(() => withAggregate(...args));
+ };
+
aggregate.map = (...args) => {
const parent = aggregate;
const { result, aggregate: child } = mapAggregate(...args);
@@ -139,6 +148,15 @@ export function openAggregate({
return result;
};
+ aggregate.filter = (...args) => {
+ const parent = aggregate;
+ const { result, aggregate: child } = filterAggregate(...args);
+ parent.call(child.close);
+ return result;
+ };
+
+ aggregate.throws = aggregateThrows;
+
aggregate.close = () => {
if (errors.length) {
throw Reflect.construct(errorClass, [errors, message]);
@@ -148,9 +166,19 @@ export function openAggregate({
return aggregate;
}
+openAggregate.errorClassSymbol = Symbol('error class');
+
+// Utility function for providing {errorClass} parameter to aggregate functions.
+export function aggregateThrows(errorClass) {
+ return {[openAggregate.errorClassSymbol]: errorClass};
+}
+
// Performs an ordinary array map with the given function, collating into a
// results array (with errored inputs filtered out) and an error aggregate.
//
+// Optionally, override returnOnFail to disable filtering and map errored inputs
+// to a particular output.
+//
// Note the aggregate property is the result of openAggregate(), still unclosed;
// use aggregate.close() to throw the error. (This aggregate may be passed to a
// parent aggregate: `parent.call(aggregate.close)`!)
@@ -158,8 +186,8 @@ export function mapAggregate(array, fn, aggregateOpts) {
const failureSymbol = Symbol();
const aggregate = openAggregate({
- ...aggregateOpts,
- returnOnFail: failureSymbol
+ returnOnFail: failureSymbol,
+ ...aggregateOpts
});
const result = array.map(aggregate.wrap(fn))
@@ -168,6 +196,49 @@ export function mapAggregate(array, fn, aggregateOpts) {
return {result, aggregate};
}
+// Performs an ordinary array filter with the given function, collating into a
+// results array (with errored inputs filtered out) and an error aggregate.
+//
+// Optionally, override returnOnFail to disable filtering errors and map errored
+// inputs to a particular output.
+//
+// As with mapAggregate, the returned aggregate property is not yet closed.
+export function filterAggregate(array, fn, aggregateOpts) {
+ const failureSymbol = Symbol();
+
+ const aggregate = openAggregate({
+ returnOnFail: failureSymbol,
+ ...aggregateOpts
+ });
+
+ const result = array.map(aggregate.wrap((x, ...rest) => ({
+ input: x,
+ output: fn(x, ...rest)
+ })))
+ .filter(value => {
+ // Filter out results which match the failureSymbol, i.e. errored
+ // inputs.
+ if (value === failureSymbol) return false;
+
+ // Always keep results which match the overridden returnOnFail
+ // value, if provided.
+ if (value === aggregateOpts.returnOnFail) return true;
+
+ // Otherwise, filter according to the returned value of the wrapped
+ // function.
+ return value.output;
+ })
+ .map(value => {
+ // Then turn the results back into their corresponding input, or, if
+ // provided, the overridden returnOnFail value.
+ return (value === aggregateOpts.returnOnFail
+ ? value
+ : value.input);
+ });
+
+ return {result, aggregate};
+}
+
// Totally sugar function for opening an aggregate, running the provided
// function with it, then closing the function and returning the result (if
// there's no throw).