« 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/validators.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/validators.js')
-rw-r--r--src/data/validators.js404
1 files changed, 212 insertions, 192 deletions
diff --git a/src/data/validators.js b/src/data/validators.js
index 0d325ae..714dc3a 100644
--- a/src/data/validators.js
+++ b/src/data/validators.js
@@ -1,367 +1,387 @@
-import { withAggregate } from '../util/sugar.js';
+import { withAggregate } from "../util/sugar.js";
 
-import { color, ENABLE_COLOR, decorateTime } from '../util/cli.js';
+import { color, ENABLE_COLOR, decorateTime } from "../util/cli.js";
 
-import { inspect as nodeInspect } from 'util';
+import { inspect as nodeInspect } from "util";
 
 function inspect(value) {
-    return nodeInspect(value, {colors: ENABLE_COLOR});
+  return nodeInspect(value, { colors: ENABLE_COLOR });
 }
 
 // Basic types (primitives)
 
 function a(noun) {
-    return (/[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`);
+  return /[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`;
 }
 
 function isType(value, type) {
-    if (typeof value !== type)
-        throw new TypeError(`Expected ${a(type)}, got ${typeof value}`);
+  if (typeof value !== type)
+    throw new TypeError(`Expected ${a(type)}, got ${typeof value}`);
 
-    return true;
+  return true;
 }
 
 export function isBoolean(value) {
-    return isType(value, 'boolean');
+  return isType(value, "boolean");
 }
 
 export function isNumber(value) {
-    return isType(value, 'number');
+  return isType(value, "number");
 }
 
 export function isPositive(number) {
-    isNumber(number);
+  isNumber(number);
 
-    if (number <= 0)
-        throw new TypeError(`Expected positive number`);
+  if (number <= 0) throw new TypeError(`Expected positive number`);
 
-    return true;
+  return true;
 }
 
 export function isNegative(number) {
-    isNumber(number);
+  isNumber(number);
 
-    if (number >= 0)
-        throw new TypeError(`Expected negative number`);
+  if (number >= 0) throw new TypeError(`Expected negative number`);
 
-    return true;
+  return true;
 }
 
 export function isPositiveOrZero(number) {
-    isNumber(number);
+  isNumber(number);
 
-    if (number < 0)
-        throw new TypeError(`Expected positive number or zero`);
+  if (number < 0) throw new TypeError(`Expected positive number or zero`);
 
-    return true;
+  return true;
 }
 
 export function isNegativeOrZero(number) {
-    isNumber(number);
+  isNumber(number);
 
-    if (number > 0)
-        throw new TypeError(`Expected negative number or zero`);
+  if (number > 0) throw new TypeError(`Expected negative number or zero`);
 
-    return true;
+  return true;
 }
 
 export function isInteger(number) {
-    isNumber(number);
+  isNumber(number);
 
-    if (number % 1 !== 0)
-        throw new TypeError(`Expected integer`);
+  if (number % 1 !== 0) throw new TypeError(`Expected integer`);
 
-    return true;
+  return true;
 }
 
 export function isCountingNumber(number) {
-    isInteger(number);
-    isPositive(number);
+  isInteger(number);
+  isPositive(number);
 
-    return true;
+  return true;
 }
 
 export function isWholeNumber(number) {
-    isInteger(number);
-    isPositiveOrZero(number);
+  isInteger(number);
+  isPositiveOrZero(number);
 
-    return true;
+  return true;
 }
 
 export function isString(value) {
-    return isType(value, 'string');
+  return isType(value, "string");
 }
 
 export function isStringNonEmpty(value) {
-    isString(value);
+  isString(value);
 
-    if (value.trim().length === 0)
-        throw new TypeError(`Expected non-empty string`);
+  if (value.trim().length === 0)
+    throw new TypeError(`Expected non-empty string`);
 
-    return true;
+  return true;
 }
 
 // Complex types (non-primitives)
 
 export function isInstance(value, constructor) {
-    isObject(value);
+  isObject(value);
 
-    if (!(value instanceof constructor))
-        throw new TypeError(`Expected ${constructor.name}, got ${value.constructor.name}`);
+  if (!(value instanceof constructor))
+    throw new TypeError(
+      `Expected ${constructor.name}, got ${value.constructor.name}`
+    );
 
-    return true;
+  return true;
 }
 
 export function isDate(value) {
-    return isInstance(value, Date);
+  return isInstance(value, Date);
 }
 
 export function isObject(value) {
-    isType(value, 'object');
+  isType(value, "object");
 
-    // Note: Please remember that null is always a valid value for properties
-    // held by a CacheableObject. This assertion is exclusively for use in other
-    // contexts.
-    if (value === null)
-        throw new TypeError(`Expected an object, got null`);
+  // Note: Please remember that null is always a valid value for properties
+  // held by a CacheableObject. This assertion is exclusively for use in other
+  // contexts.
+  if (value === null) throw new TypeError(`Expected an object, got null`);
 
-    return true;
+  return true;
 }
 
 export function isArray(value) {
-    if (typeof value !== 'object' || value === null || !Array.isArray(value))
-        throw new TypeError(`Expected an array, got ${value}`);
+  if (typeof value !== "object" || value === null || !Array.isArray(value))
+    throw new TypeError(`Expected an array, got ${value}`);
 
-    return true;
+  return true;
 }
 
 function validateArrayItemsHelper(itemValidator) {
-    return (item, index) => {
-        try {
-            const value = itemValidator(item);
-
-            if (value !== true) {
-                throw new Error(`Expected validator to return true`);
-            }
-        } catch (error) {
-            error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`;
-            throw error;
-        }
-    };
+  return (item, index) => {
+    try {
+      const value = itemValidator(item);
+
+      if (value !== true) {
+        throw new Error(`Expected validator to return true`);
+      }
+    } catch (error) {
+      error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${
+        error.message
+      }`;
+      throw error;
+    }
+  };
 }
 
 export function validateArrayItems(itemValidator) {
-    const fn = validateArrayItemsHelper(itemValidator);
+  const fn = validateArrayItemsHelper(itemValidator);
 
-    return array => {
-        isArray(array);
+  return (array) => {
+    isArray(array);
 
-        withAggregate({message: 'Errors validating array items'}, ({ wrap }) => {
-            array.forEach(wrap(fn));
-        });
+    withAggregate({ message: "Errors validating array items" }, ({ wrap }) => {
+      array.forEach(wrap(fn));
+    });
 
-        return true;
-    };
+    return true;
+  };
 }
 
 export function validateInstanceOf(constructor) {
-    return object => isInstance(object, constructor);
+  return (object) => isInstance(object, constructor);
 }
 
 // Wiki data (primitives & non-primitives)
 
 export function isColor(color) {
-    isStringNonEmpty(color);
+  isStringNonEmpty(color);
 
-    if (color.startsWith('#')) {
-        if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
-            throw new TypeError(`Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`);
+  if (color.startsWith("#")) {
+    if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
+      throw new TypeError(
+        `Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`
+      );
 
-        if (/[^0-9a-fA-F]/.test(color.slice(1)))
-            throw new TypeError(`Expected hexadecimal digits`);
+    if (/[^0-9a-fA-F]/.test(color.slice(1)))
+      throw new TypeError(`Expected hexadecimal digits`);
 
-        return true;
-    }
+    return true;
+  }
 
-    throw new TypeError(`Unknown color format`);
+  throw new TypeError(`Unknown color format`);
 }
 
 export function isCommentary(commentary) {
-    return isString(commentary);
+  return isString(commentary);
 }
 
-const isArtistRef = validateReference('artist');
+const isArtistRef = validateReference("artist");
 
 export function validateProperties(spec) {
-    const specEntries = Object.entries(spec);
-    const specKeys = Object.keys(spec);
-
-    return object => {
-        isObject(object);
-
-        if (Array.isArray(object))
-            throw new TypeError(`Expected an object, got array`);
-
-        withAggregate({message: `Errors validating object properties`}, ({ call }) => {
-            for (const [ specKey, specValidator ] of specEntries) {
-                call(() => {
-                    const value = object[specKey];
-                    try {
-                        specValidator(value);
-                    } catch (error) {
-                        error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`;
-                        throw error;
-                    }
-                });
-            }
+  const specEntries = Object.entries(spec);
+  const specKeys = Object.keys(spec);
 
-            const unknownKeys = Object.keys(object).filter(key => !specKeys.includes(key));
-            if (unknownKeys.length > 0) {
-                call(() => {
-                    throw new Error(`Unknown keys present (${unknownKeys.length}): [${unknownKeys.join(', ')}]`);
-                });
+  return (object) => {
+    isObject(object);
+
+    if (Array.isArray(object))
+      throw new TypeError(`Expected an object, got array`);
+
+    withAggregate(
+      { message: `Errors validating object properties` },
+      ({ call }) => {
+        for (const [specKey, specValidator] of specEntries) {
+          call(() => {
+            const value = object[specKey];
+            try {
+              specValidator(value);
+            } catch (error) {
+              error.message = `(key: ${color.green(specKey)}, value: ${inspect(
+                value
+              )}) ${error.message}`;
+              throw error;
             }
-        });
+          });
+        }
 
-        return true;
-    };
-}
+        const unknownKeys = Object.keys(object).filter(
+          (key) => !specKeys.includes(key)
+        );
+        if (unknownKeys.length > 0) {
+          call(() => {
+            throw new Error(
+              `Unknown keys present (${
+                unknownKeys.length
+              }): [${unknownKeys.join(", ")}]`
+            );
+          });
+        }
+      }
+    );
 
+    return true;
+  };
+}
 
 export const isContribution = validateProperties({
-    who: isArtistRef,
-    what: value => value === undefined || value === null || isStringNonEmpty(value),
+  who: isArtistRef,
+  what: (value) =>
+    value === undefined || value === null || isStringNonEmpty(value),
 });
 
 export const isContributionList = validateArrayItems(isContribution);
 
 export const isAdditionalFile = validateProperties({
-    title: isString,
-    description: value => (value === undefined || value === null || isString(value)),
-    files: validateArrayItems(isString)
+  title: isString,
+  description: (value) =>
+    value === undefined || value === null || isString(value),
+  files: validateArrayItems(isString),
 });
 
 export const isAdditionalFileList = validateArrayItems(isAdditionalFile);
 
 export function isDimensions(dimensions) {
-    isArray(dimensions);
+  isArray(dimensions);
 
-    if (dimensions.length !== 2)
-        throw new TypeError(`Expected 2 item array`);
+  if (dimensions.length !== 2) throw new TypeError(`Expected 2 item array`);
 
-    isPositive(dimensions[0]);
-    isInteger(dimensions[0]);
-    isPositive(dimensions[1]);
-    isInteger(dimensions[1]);
+  isPositive(dimensions[0]);
+  isInteger(dimensions[0]);
+  isPositive(dimensions[1]);
+  isInteger(dimensions[1]);
 
-    return true;
+  return true;
 }
 
 export function isDirectory(directory) {
-    isStringNonEmpty(directory);
+  isStringNonEmpty(directory);
 
-    if (directory.match(/[^a-zA-Z0-9_\-]/))
-        throw new TypeError(`Expected only letters, numbers, dash, and underscore, got "${directory}"`);
+  if (directory.match(/[^a-zA-Z0-9_\-]/))
+    throw new TypeError(
+      `Expected only letters, numbers, dash, and underscore, got "${directory}"`
+    );
 
-    return true;
+  return true;
 }
 
 export function isDuration(duration) {
-    isNumber(duration);
-    isPositiveOrZero(duration);
+  isNumber(duration);
+  isPositiveOrZero(duration);
 
-    return true;
+  return true;
 }
 
 export function isFileExtension(string) {
-    isStringNonEmpty(string);
+  isStringNonEmpty(string);
 
-    if (string[0] === '.')
-        throw new TypeError(`Expected no dot (.) at the start of file extension`);
+  if (string[0] === ".")
+    throw new TypeError(`Expected no dot (.) at the start of file extension`);
 
-    if (string.match(/[^a-zA-Z0-9_]/))
-        throw new TypeError(`Expected only alphanumeric and underscore`);
+  if (string.match(/[^a-zA-Z0-9_]/))
+    throw new TypeError(`Expected only alphanumeric and underscore`);
 
-    return true;
+  return true;
 }
 
 export function isLanguageCode(string) {
-    // TODO: This is a stub function because really we don't need a detailed
-    // is-language-code parser right now.
+  // TODO: This is a stub function because really we don't need a detailed
+  // is-language-code parser right now.
 
-    isString(string);
+  isString(string);
 
-    return true;
+  return true;
 }
 
 export function isName(name) {
-    return isString(name);
+  return isString(name);
 }
 
 export function isURL(string) {
-    isStringNonEmpty(string);
+  isStringNonEmpty(string);
 
-    new URL(string);
+  new URL(string);
 
-    return true;
+  return true;
 }
 
-export function validateReference(type = 'track') {
-    return ref => {
-        isStringNonEmpty(ref);
+export function validateReference(type = "track") {
+  return (ref) => {
+    isStringNonEmpty(ref);
 
-        const match = ref.trim().match(/^(?:(?<typePart>\S+):(?=\S))?(?<directoryPart>.+)(?<!:)$/);
+    const match = ref
+      .trim()
+      .match(/^(?:(?<typePart>\S+):(?=\S))?(?<directoryPart>.+)(?<!:)$/);
 
-        if (!match)
-            throw new TypeError(`Malformed reference`);
+    if (!match) throw new TypeError(`Malformed reference`);
 
-        const { groups: { typePart, directoryPart } } = match;
+    const {
+      groups: { typePart, directoryPart },
+    } = match;
 
-        if (typePart && typePart !== type)
-            throw new TypeError(`Expected ref to begin with "${type}:", got "${typePart}:"`);
+    if (typePart && typePart !== type)
+      throw new TypeError(
+        `Expected ref to begin with "${type}:", got "${typePart}:"`
+      );
 
-        if (typePart)
-            isDirectory(directoryPart);
+    if (typePart) isDirectory(directoryPart);
 
-        isName(ref);
+    isName(ref);
 
-        return true;
-    };
+    return true;
+  };
 }
 
-export function validateReferenceList(type = '') {
-    return validateArrayItems(validateReference(type));
+export function validateReferenceList(type = "") {
+  return validateArrayItems(validateReference(type));
 }
 
 // Compositional utilities
 
 export function oneOf(...checks) {
-    return value => {
-        const errorMeta = [];
+  return (value) => {
+    const errorMeta = [];
 
-        for (let i = 0, check; check = checks[i]; i++) {
-            try {
-                const result = check(value);
-
-                if (result !== true) {
-                    throw new Error(`Check returned false`);
-                }
+    for (let i = 0, check; (check = checks[i]); i++) {
+      try {
+        const result = check(value);
 
-                return true;
-            } catch (error) {
-                errorMeta.push([check, i, error]);
-            }
+        if (result !== true) {
+          throw new Error(`Check returned false`);
         }
 
-        // Don't process error messages until every check has failed.
-        const errors = [];
-        for (const [ check, i, error ] of errorMeta) {
-            error.message = (check.name
-                ? `(#${i} "${check.name}") ${error.message}`
-                : `(#${i}) ${error.message}`);
-            error.check = check;
-            errors.push(error);
-        }
-        throw new AggregateError(errors, `Expected one of ${checks.length} possible checks, but none were true`);
-    };
+        return true;
+      } catch (error) {
+        errorMeta.push([check, i, error]);
+      }
+    }
+
+    // Don't process error messages until every check has failed.
+    const errors = [];
+    for (const [check, i, error] of errorMeta) {
+      error.message = check.name
+        ? `(#${i} "${check.name}") ${error.message}`
+        : `(#${i}) ${error.message}`;
+      error.check = check;
+      errors.push(error);
+    }
+    throw new AggregateError(
+      errors,
+      `Expected one of ${checks.length} possible checks, but none were true`
+    );
+  };
 }