in with the new
This commit is contained in:
43
src/core/plugins/err/actions.js
Normal file
43
src/core/plugins/err/actions.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import serializeError from "serialize-error"
|
||||
|
||||
export const NEW_THROWN_ERR = "err_new_thrown_err"
|
||||
export const NEW_THROWN_ERR_BATCH = "err_new_thrown_err_batch"
|
||||
export const NEW_SPEC_ERR = "err_new_spec_err"
|
||||
export const NEW_AUTH_ERR = "err_new_auth_err"
|
||||
export const CLEAR = "err_clear"
|
||||
|
||||
export function newThrownErr(err, action) {
|
||||
return {
|
||||
type: NEW_THROWN_ERR,
|
||||
payload: { action, error: serializeError(err) }
|
||||
}
|
||||
}
|
||||
|
||||
export function newThrownErrBatch(errors) {
|
||||
return {
|
||||
type: NEW_THROWN_ERR_BATCH,
|
||||
payload: errors
|
||||
}
|
||||
}
|
||||
|
||||
export function newSpecErr(err, action) {
|
||||
return {
|
||||
type: NEW_SPEC_ERR,
|
||||
payload: err
|
||||
}
|
||||
}
|
||||
|
||||
export function newAuthErr(err, action) {
|
||||
return {
|
||||
type: NEW_AUTH_ERR,
|
||||
payload: err
|
||||
}
|
||||
}
|
||||
|
||||
export function clear(filter = {}) {
|
||||
// filter looks like: {type: 'spec'}, {source: 'parser'}
|
||||
return {
|
||||
type: CLEAR,
|
||||
payload: filter
|
||||
}
|
||||
}
|
||||
31
src/core/plugins/err/error-transformers/README.md
Normal file
31
src/core/plugins/err/error-transformers/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Error transformers
|
||||
|
||||
Error transformers provide a standard interface for making generated error messages more useful to end users.
|
||||
|
||||
### Inputs & outputs
|
||||
|
||||
Each transformer's `transform` function is given an Immutable List of Immutable Maps as its first argument.
|
||||
|
||||
It is expected that each `transform` function returns a List of similarly-formed Maps.
|
||||
|
||||
These errors originate from the Redux error actions that add errors to state. Errors are transformed before being passed into the reducer.
|
||||
|
||||
It's important that all the keys present in each error (specifically, `line`, `level`, `message`, `source`, and `type`) are present when the transformer is finished.
|
||||
|
||||
##### Deleting an error
|
||||
|
||||
If you want to delete an error completely, you can overwrite it with `null`. The null value will be filtered out of the transformed error array before the errors are returned.
|
||||
å
|
||||
|
||||
### Example transformer
|
||||
|
||||
This transformer will increase all your line numbers by 10.
|
||||
|
||||
```
|
||||
export function transform(errors) {
|
||||
return errors.map(err => {
|
||||
err.line += 10
|
||||
return err
|
||||
})
|
||||
}
|
||||
```
|
||||
57
src/core/plugins/err/error-transformers/hook.js
Normal file
57
src/core/plugins/err/error-transformers/hook.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import concat from "lodash/concat"
|
||||
import reduce from "lodash/reduce"
|
||||
let request = require.context("./transformers/", true, /\.js$/)
|
||||
let errorTransformers = []
|
||||
|
||||
request.keys().forEach( function( key ){
|
||||
if( key === "./hook.js" ) {
|
||||
return
|
||||
}
|
||||
|
||||
if( !key.match(/js$/) ) {
|
||||
return
|
||||
}
|
||||
|
||||
if( key.slice(2).indexOf("/") > -1) {
|
||||
// skip files in subdirs
|
||||
return
|
||||
}
|
||||
|
||||
errorTransformers.push({
|
||||
name: toTitleCase(key).replace(".js", "").replace("./", ""),
|
||||
transform: request(key).transform
|
||||
})
|
||||
})
|
||||
|
||||
export default function transformErrors (errors, system) {
|
||||
let inputs = {
|
||||
jsSpec: system.specSelectors.specJson().toJS()
|
||||
}
|
||||
|
||||
let transformedErrors = reduce(errorTransformers, (result, transformer) => {
|
||||
try {
|
||||
let newlyTransformedErrors = transformer.transform(result, inputs)
|
||||
return newlyTransformedErrors.filter(err => !!err) // filter removed errors
|
||||
} catch(e) {
|
||||
console.error("Transformer error:", e)
|
||||
return result
|
||||
}
|
||||
}, errors)
|
||||
|
||||
return transformedErrors
|
||||
.filter(err => !!err) // filter removed errors
|
||||
.map(err => {
|
||||
if(!err.get("line") && err.get("path")) {
|
||||
// TODO: re-resolve line number if we've transformed it away
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function toTitleCase(str) {
|
||||
return str
|
||||
.split("-")
|
||||
.map(substr => substr[0].toUpperCase() + substr.slice(1))
|
||||
.join("")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export function transform(errors) {
|
||||
// JSONSchema refers to the current object being validated
|
||||
// as 'instance'. This isn't helpful to users, so we remove it.
|
||||
return errors
|
||||
.map(err => {
|
||||
let seekStr = "is not of a type(s)"
|
||||
let i = err.get("message").indexOf(seekStr)
|
||||
if(i > -1) {
|
||||
let types = err.get("message").slice(i + seekStr.length).split(",")
|
||||
return err.set("message", err.get("message").slice(0, i) + makeNewMessage(types))
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function makeNewMessage(types) {
|
||||
return types.reduce((p, c, i, arr) => {
|
||||
if(i === arr.length - 1 && arr.length > 1) {
|
||||
return p + "or " + c
|
||||
} else if(arr[i+1] && arr.length > 2) {
|
||||
return p + c + ", "
|
||||
} else if(arr[i+1]) {
|
||||
return p + c + " "
|
||||
} else {
|
||||
return p + c
|
||||
}
|
||||
}, "should be a")
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import get from "lodash/get"
|
||||
import last from "lodash/get"
|
||||
import { fromJS, List } from "immutable"
|
||||
|
||||
export function transform(errors, { jsSpec }) {
|
||||
// LOOK HERE THIS TRANSFORMER IS CURRENTLY DISABLED 😃
|
||||
// TODO: finish implementing, fix flattening problem
|
||||
/* eslint-disable no-unreachable */
|
||||
return errors
|
||||
|
||||
|
||||
// JSONSchema gives us very little to go on
|
||||
let searchStr = "is not exactly one from <#/definitions/parameter>,<#/definitions/jsonReference>"
|
||||
return errors
|
||||
.map(err => {
|
||||
let message = err.get("message")
|
||||
let isParameterOneOfError = message.indexOf(searchStr) > -1
|
||||
if(isParameterOneOfError) {
|
||||
// try to find what's wrong
|
||||
return createTailoredParameterError(err, jsSpec)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
})
|
||||
.flatten(true) // shallow Immutable flatten
|
||||
}
|
||||
|
||||
const VALID_IN_VALUES = ["path", "query", "header", "body", "formData"]
|
||||
const VALID_COLLECTIONFORMAT_VALUES = ["csv", "ssv", "tsv", "pipes", "multi"]
|
||||
|
||||
function createTailoredParameterError(err, jsSpec) {
|
||||
let newErrs = []
|
||||
let parameter = get(jsSpec, err.get("path"))
|
||||
|
||||
// find addressable cases
|
||||
if(parameter.in && VALID_IN_VALUES.indexOf(parameter.in) === -1) {
|
||||
let message = `Wrong value for the "in" keyword. Expected one of: ${VALID_IN_VALUES.join(", ")}.`
|
||||
newErrs.push({
|
||||
message,
|
||||
path: err.get("path") + ".in",
|
||||
type: "spec",
|
||||
source: "schema",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
|
||||
if(parameter.collectionFormat && VALID_COLLECTIONFORMAT_VALUES.indexOf(parameter.collectionFormat) === -1) {
|
||||
let message = `Wrong value for the "collectionFormat" keyword. Expected one of: ${VALID_COLLECTIONFORMAT_VALUES.join(", ")}.`
|
||||
newErrs.push({
|
||||
message,
|
||||
path: err.get("path") + ".collectionFormat",
|
||||
type: "spec",
|
||||
source: "schema",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
|
||||
return newErrs.length ? fromJS(newErrs) : err // fall back to making no changes
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function transform(errors) {
|
||||
return errors
|
||||
.map(err => {
|
||||
return err.set("message", removeSubstring(err.get("message"), "instance."))
|
||||
})
|
||||
}
|
||||
|
||||
function removeSubstring(str, substr) {
|
||||
return str.replace(new RegExp(substr, "g"), "")
|
||||
}
|
||||
15
src/core/plugins/err/index.js
Normal file
15
src/core/plugins/err/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import makeReducers from "./reducers"
|
||||
import * as actions from "./actions"
|
||||
import * as selectors from "./selectors"
|
||||
|
||||
export default function(system) {
|
||||
return {
|
||||
statePlugins: {
|
||||
err: {
|
||||
reducers: makeReducers(system),
|
||||
actions,
|
||||
selectors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/core/plugins/err/reducers.js
Normal file
68
src/core/plugins/err/reducers.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
NEW_THROWN_ERR,
|
||||
NEW_THROWN_ERR_BATCH,
|
||||
NEW_SPEC_ERR,
|
||||
NEW_AUTH_ERR,
|
||||
CLEAR
|
||||
} from "./actions"
|
||||
|
||||
import reject from "lodash/reject"
|
||||
|
||||
import Im, { fromJS, List } from "immutable"
|
||||
|
||||
import transformErrors from "./error-transformers/hook"
|
||||
|
||||
let DEFAULT_ERROR_STRUCTURE = {
|
||||
// defaults
|
||||
line: 0,
|
||||
level: "error",
|
||||
message: "Unknown error"
|
||||
}
|
||||
|
||||
export default function(system) {
|
||||
return {
|
||||
[NEW_THROWN_ERR]: (state, { payload }) => {
|
||||
let error = Object.assign(DEFAULT_ERROR_STRUCTURE, payload, {type: "thrown"})
|
||||
return state
|
||||
.update("errors", errors => (errors || List()).push( fromJS( error )) )
|
||||
.update("errors", errors => transformErrors(errors, system.getSystem()))
|
||||
},
|
||||
|
||||
[NEW_THROWN_ERR_BATCH]: (state, { payload }) => {
|
||||
payload = payload.map(err => {
|
||||
return fromJS(Object.assign(DEFAULT_ERROR_STRUCTURE, err, { type: "thrown" }))
|
||||
})
|
||||
return state
|
||||
.update("errors", errors => (errors || List()).concat( fromJS( payload )) )
|
||||
.update("errors", errors => transformErrors(errors, system.getSystem()))
|
||||
},
|
||||
|
||||
[NEW_SPEC_ERR]: (state, { payload }) => {
|
||||
let error = fromJS(payload)
|
||||
error = error.set("type", "spec")
|
||||
return state
|
||||
.update("errors", errors => (errors || List()).push( fromJS(error)).sortBy(err => err.get("line")) )
|
||||
.update("errors", errors => transformErrors(errors, system.getSystem()))
|
||||
},
|
||||
|
||||
[NEW_AUTH_ERR]: (state, { payload }) => {
|
||||
let error = fromJS(Object.assign({}, payload))
|
||||
|
||||
error = error.set("type", "auth")
|
||||
return state
|
||||
.update("errors", errors => (errors || List()).push( fromJS(error)) )
|
||||
.update("errors", errors => transformErrors(errors, system.getSystem()))
|
||||
},
|
||||
|
||||
[CLEAR]: (state, { payload }) => {
|
||||
if(!payload) {
|
||||
return
|
||||
}
|
||||
// TODO: Rework, to use immutable only, no need for lodash
|
||||
let newErrors = Im.fromJS(reject((state.get("errors") || List()).toJS(), payload))
|
||||
return state.merge({
|
||||
errors: newErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/core/plugins/err/selectors.js
Normal file
15
src/core/plugins/err/selectors.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { List } from "immutable"
|
||||
import { createSelector } from "reselect"
|
||||
|
||||
const state = state => state
|
||||
|
||||
export const allErrors = createSelector(
|
||||
state,
|
||||
err => err.get("errors", List())
|
||||
)
|
||||
|
||||
export const lastError = createSelector(
|
||||
allErrors,
|
||||
all => all.last()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user