« get me outta code hell

composite, data: positional inputs, V(...) - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2025-11-27 19:51:01 -0400
committer(quasar) nebula <qznebula@protonmail.com>2025-11-27 19:51:01 -0400
commit861810685da73fa9fd6d4a1cb965155c61c99d11 (patch)
tree9c541dae0dee21023f83fe894bc2bfe7f135beb0
parent0bfedac2a7478c81fc2ba76e85489086c6fdab17 (diff)
composite, data: positional inputs, V(...)
basic backwards-compatible implementation,
and a small amount of demo usage
-rw-r--r--src/data/composite.js228
-rw-r--r--src/data/composite/control-flow/exitWithoutDependency.js2
-rw-r--r--src/data/composite/control-flow/exitWithoutUpdateValue.js2
-rw-r--r--src/data/things/track.js51
4 files changed, 175 insertions, 108 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);
diff --git a/src/data/composite/control-flow/exitWithoutDependency.js b/src/data/composite/control-flow/exitWithoutDependency.js
index c660a7ef..598f2ec2 100644
--- a/src/data/composite/control-flow/exitWithoutDependency.js
+++ b/src/data/composite/control-flow/exitWithoutDependency.js
@@ -11,8 +11,8 @@ export default templateCompositeFrom({
 
   inputs: {
     dependency: input({acceptsNull: true}),
-    mode: inputAvailabilityCheckMode(),
     value: input({defaultValue: null}),
+    mode: inputAvailabilityCheckMode(),
   },
 
   steps: () => [
diff --git a/src/data/composite/control-flow/exitWithoutUpdateValue.js b/src/data/composite/control-flow/exitWithoutUpdateValue.js
index 1cce233f..3d9fbf76 100644
--- a/src/data/composite/control-flow/exitWithoutUpdateValue.js
+++ b/src/data/composite/control-flow/exitWithoutUpdateValue.js
@@ -10,8 +10,8 @@ export default templateCompositeFrom({
   annotation: `exitWithoutUpdateValue`,
 
   inputs: {
-    mode: inputAvailabilityCheckMode(),
     value: input({defaultValue: null}),
+    mode: inputAvailabilityCheckMode(),
 
     validate: input({
       type: 'function',
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 39a1804f..030778a3 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -2,7 +2,7 @@ import {inspect} from 'node:util';
 
 import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
-import {input} from '#composite';
+import {input, V} from '#composite';
 import {onlyItem} from '#sugar';
 import {sortByDate} from '#sort';
 import Thing from '#thing';
@@ -141,14 +141,8 @@ export class Track extends Thing {
         validate: input.value(isBoolean),
       }),
 
-      withPropertyFromObject({
-        object: 'trackSection',
-        property: input.value('suffixTrackDirectories'),
-      }),
-
-      exposeDependency({
-        dependency: '#trackSection.suffixTrackDirectories',
-      }),
+      withPropertyFromObject('trackSection', V('suffixTrackDirectories')),
+      exposeDependency('#trackSection.suffixTrackDirectories'),
     ],
 
     // Controls how find.track works - it'll never be matched by
@@ -160,10 +154,7 @@ export class Track extends Thing {
         validate: input.value(isBoolean),
       }),
 
-      withPropertyFromObject({
-        object: 'album',
-        property: input.value('alwaysReferenceTracksByDirectory'),
-      }),
+      withPropertyFromObject('album', V('alwaysReferenceTracksByDirectory')),
 
       // Falsy mode means this exposes true if the album's property is true,
       // but continues if the property is false (which is also the default).
@@ -172,20 +163,10 @@ export class Track extends Thing {
         mode: input.value('falsy'),
       }),
 
-      exitWithoutDependency({
-        dependency: '_mainRelease',
-        value: input.value(false),
-      }),
+      exitWithoutDependency('_mainRelease', V(false)),
+      exitWithoutDependency('mainReleaseTrack', V(false)),
 
-      exitWithoutDependency({
-        dependency: 'mainReleaseTrack',
-        value: input.value(false),
-      }),
-
-      withPropertyFromObject({
-        object: 'mainReleaseTrack',
-        property: input.value('name'),
-      }),
+      withPropertyFromObject('mainReleaseTrack', V('name')),
 
       {
         dependencies: ['name', '#mainReleaseTrack.name'],
@@ -248,9 +229,7 @@ export class Track extends Thing {
         '#resolvedReference': '#sameNameSingle',
       }),
 
-      exposeDependencyOrContinue({
-        dependency: '#sameNameSingle',
-      }),
+      exposeDependencyOrContinue('#sameNameSingle'),
 
       {
         dependencies: [
@@ -321,18 +300,10 @@ export class Track extends Thing {
         validate: input.value(isContentString),
       }),
 
-      exposeDependencyOrContinue({
-        dependency: '_artistText',
-      }),
-
-      withPropertyFromObject({
-        object: 'album',
-        property: input.value('trackArtistText'),
-      }),
+      exposeDependencyOrContinue('_artistText'),
 
-      exposeDependency({
-        dependency: '#album.trackArtistText',
-      }),
+      withPropertyFromObject('album', V('trackArtistText')),
+      exposeDependency('#album.trackArtistText'),
     ],
 
     artistContribs: [