Skip to content

Commit

Permalink
Improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed May 29, 2022
1 parent a7df45a commit b649272
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 62 deletions.
210 changes: 167 additions & 43 deletions src/config/normalize/lib/call.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
/* eslint-disable max-lines */
// TODO: fix max-lines linting
import { inspect } from 'util'

import { wrapError } from '../../../error/wrap.js'

// Call `keyword.test()`
export const callTest = async function (test, input, info) {
return await callFunc({ func: test, input, info, hasInput: true, test })
export const callTest = async function ({ test, input, info, keyword }) {
return await callFunc({
func: test,
input,
info,
hasInput: true,
keyword,
errorType: 'keyword',
bugType: 'keyword',
})
}

// Call `keyword.normalize()`
export const callNormalize = async function (normalize, definition, info) {
export const callNormalize = async function ({
normalize,
definition,
info,
keyword,
exampleDefinition,
}) {
const func = () => normalize(definition)
return await callFunc({ func, info, hasInput: false })
return await callFunc({
func,
info,
keyword,
definition,
exampleDefinition,
hasInput: false,
errorType: 'definition',
bugType: 'keyword',
})
}

// Call definition function
Expand All @@ -20,8 +45,20 @@ export const callDefinition = async function ({
info,
hasInput,
test,
keyword,
exampleDefinition,
}) {
return await callFunc({ func: definition, input, info, hasInput, test })
return await callFunc({
func: definition,
input,
info,
hasInput,
test,
keyword,
exampleDefinition,
errorType: 'input',
bugType: 'definition',
})
}

// Call `keyword.main()`
Expand All @@ -32,9 +69,20 @@ export const callMain = async function ({
info,
hasInput,
test,
keyword,
}) {
const func = main.bind(undefined, definition)
return await callFunc({ func, input, info, hasInput, test })
return await callFunc({
func,
input,
info,
hasInput,
test,
keyword,
definition,
errorType: 'input',
bugType: 'keyword',
})
}

// Call a function from `test()`, `main()` or a definition.
Expand All @@ -49,45 +97,33 @@ const callFunc = async function ({
info: { example, prefix, ...info },
hasInput,
test,
keyword,
definition,
exampleDefinition,
errorType,
bugType,
}) {
try {
return await (hasInput ? func(input, info) : func(info))
return hasInput ? await func(input, info) : await func(info)
} catch (error) {
throw handleError({
const isValidation = isValidateError(error)
const type = isValidation ? errorType : bugType
throw ERROR_HANDLERS[type]({
error,
input,
example,
prefix,
originalName,
hasInput,
test,
keyword,
definition,
exampleDefinition,
isValidation,
})
}
}

const handleError = function ({
error,
input,
example,
prefix,
originalName,
hasInput,
test,
}) {
const isValidation = isValidateError(error)
const errorA = addPrefix({ error, prefix, originalName, isValidation })

if (!isValidation) {
return errorA
}

// eslint-disable-next-line fp/no-mutation
errorA.validation = true
const errorB = addCurrentValue(errorA, input, hasInput)
const errorC = addExampleValue({ error: errorB, example, hasInput, test })
return errorC
}

// Consumers can distinguish users errors from system bugs by checking
// the `error.validation` boolean property.
// User errors are distinguished by having error message starting with "must".
Expand All @@ -102,27 +138,114 @@ const isValidateError = function (error) {
return error instanceof Error && error.message.startsWith('must')
}

const addPrefix = function ({ error, prefix, originalName, isValidation }) {
const prefixA = getPrefix(prefix, originalName)
const prefixB = isValidation ? prefixA : `${prefixA}: `
return wrapError(error, prefixB)
const handleInputError = function ({
error,
input,
example,
prefix,
originalName,
hasInput,
test,
}) {
error.validation = true
const errorA = addInputPrefix(error, prefix, originalName)
const errorB = addCurrentInput(errorA, input, hasInput)
const errorC = addExampleInput({ error: errorB, example, hasInput, test })
return errorC
}

const handleDefinitionError = function ({
error,
prefix,
originalName,
keyword,
definition,
exampleDefinition,
isValidation,
}) {
const errorA = addDefinitionPrefix({
error,
prefix,
originalName,
keyword,
isValidation,
})
const errorB = addCurrentDefinition(errorA, definition)
const errorC = addExampleDefinition(errorB, exampleDefinition)
return errorC
}

const handleKeywordError = function ({
error,
input,
prefix,
originalName,
hasInput,
keyword,
definition,
}) {
const errorA = addKeywordPrefix({ error, prefix, originalName, keyword })
const errorB = addCurrentDefinition(errorA, definition)
const errorC = addCurrentInput(errorB, input, hasInput)
return errorC
}

const ERROR_HANDLERS = {
input: handleInputError,
definition: handleDefinitionError,
keyword: handleKeywordError,
}

// The `prefix` is the name of the type of property to show in error
// message and warnings such as "Option".
export const getPrefix = function (prefix, originalName) {
return `${prefix} "${originalName}"`
const addInputPrefix = function (error, prefix, originalName) {
return wrapError(error, `${prefix} "${originalName}"`)
}

const addDefinitionPrefix = function ({
error,
prefix,
originalName,
keyword,
isValidation,
}) {
const suffix = isValidation ? 'definition' : 'must have a valid definition:\n'
return wrapError(
error,
`${prefix} "${originalName}"'s keyword "${keyword}" ${suffix}`,
)
}

const addKeywordPrefix = function ({ error, prefix, originalName, keyword }) {
return wrapError(
error,
`${prefix} "${originalName}"'s keyword "${keyword}" bug:`,
)
}

// Add the current definition as error suffix
const addCurrentDefinition = function (error, definition) {
return definition === undefined
? error
: wrapErrorValue(error, 'Current definition', definition)
}

// Add the example definition as error suffix
const addExampleDefinition = function (error, exampleDefinition) {
return exampleDefinition === undefined
? error
: wrapErrorValue(error, 'Example definition', exampleDefinition)
}

// Add the current input value as error suffix
const addCurrentValue = function (error, input, hasInput) {
return hasInput ? wrapErrorValue(error, 'Current value', input) : error
// Add the current input as error suffix
const addCurrentInput = function (error, input, hasInput) {
return hasInput ? wrapErrorValue(error, 'Current input', input) : error
}

// Add the example as error suffix
const addExampleValue = function ({ error, example, hasInput, test }) {
// Add the example input as error suffix
const addExampleInput = function ({ error, example, hasInput, test }) {
return example !== undefined && (hasInput || test !== undefined)
? wrapErrorValue(error, 'Example value', example)
? wrapErrorValue(error, 'Example input', example)
: error
}

Expand All @@ -135,3 +258,4 @@ const serializeValue = function (value) {
const separator = valueStr.includes('\n') ? '\n' : ' '
return `${separator}${valueStr}`
}
/* eslint-enable max-lines */
30 changes: 24 additions & 6 deletions src/config/normalize/lib/keywords/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { callDefinition, callNormalize } from '../call.js'
// `aliases` can be defined to either:
// - Augment the behavior of another keyword
// - Rename a keyword
export const getDefinition = function (rule, name, aliases) {
const definition = rule[name]
export const getDefinition = function (rule, keyword, aliases) {
const definition = rule[keyword]

if (definition !== undefined || aliases === undefined) {
return definition
Expand All @@ -22,19 +22,37 @@ export const callDefinitionFunc = async function ({
info,
hasInput,
test,
keyword,
exampleDefinition,
}) {
return typeof definition === 'function'
? await callDefinition({ definition, input, info, hasInput, test })
? await callDefinition({
definition,
input,
info,
hasInput,
test,
keyword,
exampleDefinition,
})
: definition
}

// Apply `keyword.normalize(definition)`
export const normalizeDefinition = async function (
export const normalizeDefinition = async function ({
definition,
normalize,
info,
) {
keyword,
exampleDefinition,
}) {
return normalize === undefined
? definition
: await callNormalize(normalize, definition, info)
: await callNormalize({
normalize,
definition,
info,
keyword,
exampleDefinition,
})
}
8 changes: 5 additions & 3 deletions src/config/normalize/lib/keywords/list/cwd.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { stat } from 'fs/promises'
import { resolve } from 'path'
import { cwd as getCwd } from 'process'

const normalize = async function (definition) {
await validateCwd(definition)
Expand All @@ -8,13 +9,13 @@ const normalize = async function (definition) {

const validateCwd = async function (definition) {
if (typeof definition !== 'string') {
throw new TypeError('It must be a string.')
throw new TypeError('must be a string.')
}

const cwdStat = await getStat(definition)

if (!cwdStat.isDirectory()) {
throw new Error('It must be a directory.')
throw new Error('must be a directory.')
}
}

Expand All @@ -23,7 +24,7 @@ const getStat = async function (definition) {
return await stat(definition)
} catch (error) {
throw error.code === 'ENOENT'
? new Error('It must be an existing file')
? new Error('must be an existing file.')
: error
}
}
Expand All @@ -37,6 +38,7 @@ const main = function (definition) {
export default {
name: 'cwd',
undefinedInput: true,
exampleDefinition: getCwd(),
normalize,
main,
}
Loading

0 comments on commit b649272

Please sign in to comment.