« 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/things/validators.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/validators.js')
-rw-r--r--src/data/things/validators.js114
1 files changed, 87 insertions, 27 deletions
diff --git a/src/data/things/validators.js b/src/data/things/validators.js
index f60c363c..55eedbcf 100644
--- a/src/data/things/validators.js
+++ b/src/data/things/validators.js
@@ -1,7 +1,12 @@
 import {inspect as nodeInspect} from 'node:util';
 
+// Heresy.
+import printable_characters from 'printable-characters';
+const {strlen} = printable_characters;
+
 import {colors, ENABLE_COLOR} from '#cli';
-import {empty, typeAppearance, withAggregate} from '#sugar';
+import {cut, empty, typeAppearance, withAggregate} from '#sugar';
+import {commentaryRegex} from '#wiki-data';
 
 function inspect(value) {
   return nodeInspect(value, {colors: ENABLE_COLOR});
@@ -96,7 +101,10 @@ export function isStringNonEmpty(value) {
 }
 
 export function optional(validator) {
-  return value => value === null || value === undefined || validator(value);
+  return value =>
+    value === null ||
+    value === undefined ||
+    validator(value);
 }
 
 // Complex types (non-primitives)
@@ -166,29 +174,42 @@ export function is(...values) {
 }
 
 function validateArrayItemsHelper(itemValidator) {
-  return (item, index) => {
+  return (item, index, array) => {
     try {
-      const value = itemValidator(item);
+      const value = itemValidator(item, index, array);
 
       if (value !== true) {
         throw new Error(`Expected validator to return true`);
       }
     } catch (error) {
-      error.message = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)}) ${error.message}`;
+      const annotation = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)})`;
+
+      error.message =
+        (error.message.includes('\n') || strlen(annotation) > 20
+          ? annotation + '\n' +
+            error.message
+              .split('\n')
+              .map(line => `  ${line}`)
+              .join('\n')
+          : `${annotation} ${error}`);
+
       error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index;
+
       throw error;
     }
   };
 }
 
 export function validateArrayItems(itemValidator) {
-  const fn = validateArrayItemsHelper(itemValidator);
+  const helper = validateArrayItemsHelper(itemValidator);
 
   return (array) => {
     isArray(array);
 
-    withAggregate({message: 'Errors validating array items'}, ({wrap}) => {
-      array.forEach(wrap(fn));
+    withAggregate({message: 'Errors validating array items'}, ({call}) => {
+      for (let index = 0; index < array.length; index++) {
+        call(helper, array[index], index, array);
+      }
     });
 
     return true;
@@ -200,12 +221,12 @@ export function strictArrayOf(itemValidator) {
 }
 
 export function sparseArrayOf(itemValidator) {
-  return validateArrayItems(item => {
+  return validateArrayItems((item, index, array) => {
     if (item === false || item === null) {
       return true;
     }
 
-    return itemValidator(item);
+    return itemValidator(item, index, array);
   });
 }
 
@@ -231,18 +252,56 @@ export function isColor(color) {
   throw new TypeError(`Unknown color format`);
 }
 
-export function isCommentary(commentary) {
-  isString(commentary);
+export function isCommentary(commentaryText) {
+  isString(commentaryText);
 
-  const [firstLine] = commentary.match(/.*/);
-  if (!firstLine.replace(/<\/b>/g, '').includes(':</i>')) {
-    throw new TypeError(`Missing commentary citation: "${
-      firstLine.length > 40
-        ? firstLine.slice(0, 40) + '...'
-        : firstLine
-    }"`);
+  const rawMatches =
+    Array.from(commentaryText.matchAll(commentaryRegex));
+
+  if (empty(rawMatches)) {
+    throw new TypeError(`Expected at least one commentary heading`);
   }
 
+  const niceMatches =
+    rawMatches.map(match => ({
+      position: match.index,
+      length: match[0].length,
+    }));
+
+  validateArrayItems(({position, length}, index) => {
+    if (index === 0 && position > 0) {
+      throw new TypeError(`Expected first commentary heading to be at top`);
+    }
+
+    const ownInput = commentaryText.slice(position, position + length);
+    const restOfInput = commentaryText.slice(position + length);
+    const nextLineBreak = restOfInput.indexOf('\n');
+    const upToNextLineBreak = restOfInput.slice(0, nextLineBreak);
+
+    if (/\S/.test(upToNextLineBreak)) {
+      throw new TypeError(
+        `Expected commentary heading to occupy entire line, got extra text:\n` +
+        `${colors.green(`"${cut(ownInput, 40)}"`)} (<- heading)\n` +
+        `(extra on same line ->) ${colors.red(`"${cut(upToNextLineBreak, 30)}"`)}\n` +
+        `(Check for missing "|-" in YAML, or a misshapen annotation)`);
+    }
+
+    const nextHeading =
+      (index === niceMatches.length - 1
+        ? commentaryText.length
+        : niceMatches[index + 1].position);
+
+    const upToNextHeading =
+      commentaryText.slice(position + length, nextHeading);
+
+    if (!/\S/.test(upToNextHeading)) {
+      throw new TypeError(
+        `Expected commentary entry to have body text, only got a heading`);
+    }
+
+    return true;
+  })(niceMatches);
+
   return true;
 }
 
@@ -285,20 +344,14 @@ export function validateProperties(spec) {
 
 export const isContribution = validateProperties({
   who: isArtistRef,
-  what: (value) =>
-    value === undefined ||
-    value === null ||
-    isStringNonEmpty(value),
+  what: optional(isStringNonEmpty),
 });
 
 export const isContributionList = validateArrayItems(isContribution);
 
 export const isAdditionalFile = validateProperties({
   title: isString,
-  description: (value) =>
-    value === undefined ||
-    value === null ||
-    isString(value),
+  description: optional(isStringNonEmpty),
   files: validateArrayItems(isString),
 });
 
@@ -376,6 +429,13 @@ export function isURL(string) {
   return true;
 }
 
+export const isAdditionalName = validateProperties({
+  name: isName,
+  annotation: optional(isStringNonEmpty),
+});
+
+export const isAdditionalNameList = validateArrayItems(isAdditionalName);
+
 export function validateReference(type = 'track') {
   return (ref) => {
     isStringNonEmpty(ref);