« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/composite.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/composite.js')
-rw-r--r--src/data/composite.js228
1 files changed, 162 insertions, 66 deletions
diff --git a/src/data/composite.js b/src/data/composite.js
index b70dbcaf..d3848806 100644
--- a/src/data/composite.js
+++ b/src/data/composite.js
@@ -17,7 +17,7 @@ const _valueIntoToken = shape =>
    : typeof value === 'string'
       ? Symbol.for(`hsmusic.composite.${shape}:${value}`)
       : {
-          symbol: Symbol.for(`hsmusic.composite.input`),
+          symbol: Symbol.for(`hsmusic.composite.${shape.split('.')[0]}`),
           shape,
           value,
         });
@@ -36,6 +36,10 @@ input.updateValue = _valueIntoToken('input.updateValue');
 input.staticDependency = _valueIntoToken('input.staticDependency');
 input.staticValue = _valueIntoToken('input.staticValue');
 
+// Only valid in positional inputs. This is replaced with
+// equivalent input.value() token in prepared inputs.
+export const V = _valueIntoToken('V');
+
 function isInputToken(token) {
   if (token === null) {
     return false;
@@ -48,27 +52,39 @@ function isInputToken(token) {
   }
 }
 
+function isConciseInputToken(token) {
+  if (token === null) {
+    return false;
+  } else if (typeof token === 'object') {
+    return token.symbol === Symbol.for('hsmusic.composite.V');
+  } else if (typeof token === 'symbol') {
+    return token.description.startsWith('hsmusic.composite.V');
+  } else {
+    return false;
+  }
+}
+
 function getInputTokenShape(token) {
-  if (!isInputToken(token)) {
+  if (!isInputToken(token) && !isConciseInputToken(token)) {
     throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`);
   }
 
   if (typeof token === 'object') {
     return token.shape;
   } else {
-    return token.description.match(/hsmusic\.composite\.(input.*?)(:|$)/)[1];
+    return token.description.match(/hsmusic\.composite\.(input.*?|V)(:|$)/)[1];
   }
 }
 
 function getInputTokenValue(token) {
-  if (!isInputToken(token)) {
+  if (!isInputToken(token) && !isConciseInputToken(token)) {
     throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`);
   }
 
   if (typeof token === 'object') {
     return token.value;
   } else {
-    return token.description.match(/hsmusic\.composite\.input.*?:(.*)/)?.[1] ?? null;
+    return token.description.match(/hsmusic\.composite\.(?:input.*?|V):(.*)/)?.[1] ?? null;
   }
 }
 
@@ -214,17 +230,90 @@ export function templateCompositeFrom(description) {
       ? Object.keys(description.inputs)
       : []);
 
-  const instantiate = (inputOptions = {}) => {
+  const positionalInputNames = expectedInputNames;
+
+  const instantiate = (...args) => {
+    const preparedInputs = {};
+
     withAggregate({message: `Errors in input options passed to ${compositionName}`}, ({push}) => {
-      const providedInputNames = Object.keys(inputOptions);
+      const [positionalInputs, namedInputs] =
+        (typeof args.at(-1) === 'object' &&
+         !isInputToken(args.at(-1)) &&
+         !isConciseInputToken(args.at(-1))
+          ? [args.slice(0, -1), args.at(-1)]
+          : [args, {}]);
+
+      const expresslyProvidedInputNames = Object.keys(namedInputs);
+      const positionallyProvidedInputNames = [];
+
+      const apparentInputRoutes = {};
+
+      const wrongTypeInputPositions = [];
+      const namedAndPositionalConflictInputPositions = [];
+
+      const maximumPositionalInputs = positionalInputNames.length;
+      const lastPossiblePositionalIndex = maximumPositionalInputs - 1;
+
+      for (const [index, value] of positionalInputs.entries()) {
+        if (!isInputToken(value) && !isConciseInputToken(value)) {
+          if (typeof value === 'object' && value !== null) {
+            wrongTypeInputPositions.push(index);
+            continue;
+          } else if (typeof value !== 'string') {
+            wrongTypeInputPositions.push(index);
+            continue;
+          }
+        }
+
+        if (index > lastPossiblePositionalIndex) {
+          continue;
+        }
+
+        const correspondingName = expectedInputNames[index];
+        if (expresslyProvidedInputNames.includes(correspondingName)) {
+          namedAndPositionalConflictInputPositions.push(index);
+          continue;
+        }
+
+        preparedInputs[correspondingName] =
+          (isConciseInputToken(value)
+            ? input.value(getInputTokenValue(value))
+            : value);
+
+        apparentInputRoutes[correspondingName] = `${correspondingName} (i = ${index})`;
+        positionallyProvidedInputNames.push(correspondingName);
+      }
 
       const misplacedInputNames =
-        providedInputNames
+        expresslyProvidedInputNames
           .filter(name => !expectedInputNames.includes(name));
 
+      const wrongTypeInputNames = [];
+
+      for (const [name, value] of Object.entries(namedInputs)) {
+        if (misplacedInputNames.includes(name)) {
+          continue;
+        }
+
+        // Concise input tokens, V(...), end up here too.
+        if (typeof value !== 'string' && !isInputToken(value)) {
+          wrongTypeInputNames.push(name);
+          continue;
+        }
+
+        preparedInputs[name] = value;
+        apparentInputRoutes[name] = name;
+      }
+
+      const totalProvidedInputNames =
+        unique([
+          ...expresslyProvidedInputNames,
+          ...positionallyProvidedInputNames,
+        ]);
+
       const missingInputNames =
         expectedInputNames
-          .filter(name => !providedInputNames.includes(name))
+          .filter(name => !totalProvidedInputNames.includes(name))
           .filter(name => {
             const inputDescription = getInputTokenValue(description.inputs[name]);
             if (!inputDescription) return true;
@@ -233,57 +322,39 @@ export function templateCompositeFrom(description) {
             return true;
           });
 
-      const wrongTypeInputNames = [];
-
       const expectedStaticValueInputNames = [];
       const expectedStaticDependencyInputNames = [];
       const expectedValueProvidingTokenInputNames = [];
-
       const validateFailedErrors = [];
 
-      for (const [name, value] of Object.entries(inputOptions)) {
-        if (misplacedInputNames.includes(name)) {
-          continue;
-        }
-
-        if (typeof value !== 'string' && !isInputToken(value)) {
-          wrongTypeInputNames.push(name);
-          continue;
-        }
-
+      for (const [name, value] of Object.entries(preparedInputs)) {
         const descriptionShape = getInputTokenShape(description.inputs[name]);
 
         const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null);
         const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null);
 
-        switch (descriptionShape) {
-          case'input.staticValue':
-            if (tokenShape !== 'input.value') {
-              expectedStaticValueInputNames.push(name);
-              continue;
-            }
-            break;
-
-          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.thisProperty',
-              'input.updateValue',
-            ].includes(tokenShape)) {
-              expectedValueProvidingTokenInputNames.push(name);
-              continue;
-            }
-            break;
+        if (descriptionShape === 'input.staticValue') {
+          if (tokenShape !== 'input.value') {
+            expectedStaticValueInputNames.push(name);
+            continue;
+          }
+        } else if (descriptionShape === 'input.staticDependency') {
+          if (typeof value !== 'string' && tokenShape !== 'input.dependency') {
+            expectedStaticDependencyInputNames.push(name);
+            continue;
+          }
+        } else {
+          if (typeof value !== 'string' && ![
+            'input',
+            'input.value',
+            'input.dependency',
+            'input.myself',
+            'input.thisProperty',
+            'input.updateValue',
+          ].includes(tokenShape)) {
+            expectedValueProvidingTokenInputNames.push(name);
+            continue;
+          }
         }
 
         if (tokenShape === 'input.value') {
@@ -296,6 +367,11 @@ export function templateCompositeFrom(description) {
         }
       }
 
+      const inputAppearance = name =>
+        (isInputToken(preparedInputs[name])
+          ? `${getInputTokenShape(preparedInputs[name])}() call`
+          : `dependency name`);
+
       if (!empty(misplacedInputNames)) {
         push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`));
       }
@@ -304,29 +380,49 @@ export function templateCompositeFrom(description) {
         push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`));
       }
 
-      const inputAppearance = name =>
-        (isInputToken(inputOptions[name])
-          ? `${getInputTokenShape(inputOptions[name])}() call`
-          : `dependency name`);
+      if (positionalInputs.length > maximumPositionalInputs) {
+        push(new Error(`Too many positional inputs provided (${positionalInputs.length} > ${maximumPositionalInputs}`));
+      }
+
+      for (const index of namedAndPositionalConflictInputPositions) {
+        const conflictingName = positionalInputNames[index];
+        push(new Error(`${name}: Provided as both named and positional (i = ${index}) input`));
+      }
 
       for (const name of expectedStaticDependencyInputNames) {
-        const appearance = inputAppearance(name);
-        push(new Error(`${name}: Expected dependency name, got ${appearance}`));
+        const appearance = inputAppearance(preparedInputs[name]);
+        const route = apparentInputRoutes[name];
+        push(new Error(`${route}: Expected dependency name, got ${appearance}`));
       }
 
       for (const name of expectedStaticValueInputNames) {
-        const appearance = inputAppearance(name)
-        push(new Error(`${name}: Expected input.value() call, got ${appearance}`));
+        const appearance = inputAppearance(preparedInputs[name]);
+        const route = apparentInputRoutes[name];
+        push(new Error(`${route}: Expected input.value() call, got ${appearance}`));
       }
 
       for (const name of expectedValueProvidingTokenInputNames) {
-        const appearance = getInputTokenShape(inputOptions[name]);
-        push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${appearance}`));
+        const appearance = getInputTokenShape(preparedInputs[name]);
+        const route = apparentInputRoutes[name];
+        push(new Error(`${route}: Expected dependency name or value-providing input() call, got ${appearance}`));
       }
 
       for (const name of wrongTypeInputNames) {
-        const type = typeAppearance(inputOptions[name]);
-        push(new Error(`${name}: Expected dependency name or input() call, got ${type}`));
+        if (isConciseInputToken(namedInputs[name])) {
+          push(new Error(`${name}: Use input.value() instead of V() for named inputs`));
+        } else {
+          const type = typeAppearance(namedInputs[name]);
+          push(new Error(`${name}: Expected dependency name or input() call, got ${type}`));
+        }
+      }
+
+      for (const index of wrongTypeInputPositions) {
+        const type = typeAppearance(positionalInputs[index]);
+        if (type === 'object') {
+          push(new Error(`i = ${index}: Got object - all named dependencies must be passed together, in last argument`));
+        } else {
+          push(new Error(`i = ${index}: Expected dependency name or input() call, got ${type}`));
+        }
       }
 
       for (const error of validateFailedErrors) {
@@ -338,13 +434,13 @@ export function templateCompositeFrom(description) {
     if ('inputs' in description) {
       for (const [name, token] of Object.entries(description.inputs)) {
         const tokenValue = getInputTokenValue(token);
-        if (name in inputOptions) {
-          if (typeof inputOptions[name] === 'string') {
-            inputMapping[name] = input.dependency(inputOptions[name]);
+        if (name in preparedInputs) {
+          if (typeof preparedInputs[name] === 'string') {
+            inputMapping[name] = input.dependency(preparedInputs[name]);
           } else {
             // This is always an input token, since only a string or
             // an input token is a valid input option (asserted above).
-            inputMapping[name] = inputOptions[name];
+            inputMapping[name] = preparedInputs[name];
           }
         } else if (tokenValue.defaultValue) {
           inputMapping[name] = input.value(tokenValue.defaultValue);