« get me outta code hell

aggregate shenanigans left uncommitted - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
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).