diff --git a/package.json b/package.json index 6090d4c..3952944 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "create-error": "^0.3.1", "default-value": "^1.0.0", "flatten": "^1.0.3", - "is-arguments": "^1.0.4" + "indent-string": "^4.0.0", + "is-arguments": "^1.0.4", + "supports-color": "^7.1.0" }, "devDependencies": { "@joepie91/eslint-config": "^1.1.0", diff --git a/src/aggregrate-errors.js b/src/aggregrate-errors.js index f24caf6..ff963a7 100644 --- a/src/aggregrate-errors.js +++ b/src/aggregrate-errors.js @@ -1,5 +1,7 @@ "use strict"; +const indentString = require("indent-string"); +const supportsColor = require("supports-color"); const matchVirtualProperty = require("@validatem/match-virtual-property"); const AggregrateValidationError = require("./aggregrate-validation-error"); @@ -7,38 +9,69 @@ const AggregrateValidationError = require("./aggregrate-validation-error"); // TODO: Omit the "At (root)" for path-less errors, to avoid confusion when singular values are being compared? // TODO: Move out the path generating logic into a separate module, to better support custom error formatting code -module.exports = function aggregrateAndThrowErrors(errors) { - let rephrasedErrors = errors.map((error) => { - let stringifiedPathSegments = error.path.map((segment) => { +function joinPathSegments(segments) { + return (segments.length > 0) + ? segments.join(" -> ") + : "(root)"; +} + +// NOTE: We do some manual ANSI escape code stuff here for now, because using `chalk` would significantly inflate the bundle size of the core. +// TODO: Find a better solution for this. +let openHighlight, openDim, closeColor; + +if (supportsColor.stderr) { + openHighlight = `\u001b[36m`; // cyan + openDim = `\u001b[90m`; // gray + closeColor = `\u001b[39m`; +} else { + openHighlight = ""; + openDim = ""; + closeColor = ""; +} + +function renderErrorList(errors, basePath = []) { + let rephrasedErrors = errors.map((error, i) => { + let pathSegments = error.path.map((segment) => { if (segment == null) { throw new Error(`Unexpected empty path segment encountered; this is a bug, please report it!`); - } else if (typeof segment === "string") { - return segment; - } else if (typeof segment === "number") { - return String(segment); + } else if (typeof segment === "string" || typeof segment === "number") { + return openHighlight + String(segment) + closeColor; } else if (matchVirtualProperty(segment)) { - return `(${segment.name})`; + return openDim + `(${segment.name})` + closeColor; } else { throw new Error(`Unexpected path segment encountered: ${segment}; this is a bug, please report it!`); } - }); + }); - /* TODO: Make immutable */ - let path = (stringifiedPathSegments.length > 0) - ? stringifiedPathSegments.join(" -> ") - : "(root)"; + let lineCharacter = (i < errors.length - 1) + ? "├─" + : "└─"; - error.message = `At ${path}: ${error.message}`; - return error; + let mainLine = (basePath.length > 0) + // ? `... -> ${joinPathSegments(pathSegments)}: ${error.message}` + ? ` ${lineCharacter} ${joinPathSegments(pathSegments)}: ${error.message}` + : ` - At ${joinPathSegments(pathSegments)}: ${error.message}`; + + if (error.subErrors != null && error.subErrors.length > 0) { + let renderedSubErrors = renderErrorList(error.subErrors, error.path); + + return mainLine + "\n" + indentString(renderedSubErrors, 2); + } else { + return mainLine; + } }); - let detailLines = rephrasedErrors.map((error) => { - return ` - ${error.message}`; + return rephrasedErrors.map((error) => { + return `${error}`; }).join("\n"); +} + +module.exports = function aggregrateAndThrowErrors(errors) { + let detailLines = renderErrorList(errors); if (errors.length > 0) { return new AggregrateValidationError(`One or more validation errors occurred:\n${detailLines}`, { - errors: rephrasedErrors + errors: errors }); } }; diff --git a/yarn.lock b/yarn.lock index 209a255..4e3cbe0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -697,6 +697,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"