Added Swagger

This commit is contained in:
2020-06-10 08:25:21 +02:00
parent 5c6f37eaf7
commit af76cbca87
257 changed files with 48861 additions and 12 deletions
@@ -0,0 +1,44 @@
A guide to custom patterns used in the JSON Schema validation documents in this folder...
### Pivot key existential switch
Applies a schema to an object based on whether a key exists in an object.
> "If key A exists on the object, apply schema X. Else, apply schema Y."
```yaml
switch:
- if:
required: [a]
then:
description: schema X; within `then` can be any JSON Schema content
- then:
description: schema Y; within `then` can be any JSON Schema content
```
### Pivot key value switch
Applies a schema to an object based on the value of a specific, always-required key (the "pivot key").
> "If key A is foo, apply schema X. Else, if key A is bar, apply schema Y. Else, tell the user that key A must be foo or bar."
- The pivot key must be `required` in each `if` block, otherwise the switch may generate a false positive for the entire object when the key isn't provided at all.
- The default case (the last one, with `then` but no `if`) must always require the pivot key's presence and report all possible values back as an enum, otherwise a misleading error message may be shown to the user.
```yaml
switch:
- if:
required: [a]
properties: { a: { enum: [foo] } }
then:
description: schema X; within `then` can be any JSON Schema content
- if:
required: [a]
properties: { a: { enum: [bar] } }
then:
description: schema Y; within `then` can be any JSON Schema content
- then:
description: fallback schema; ensures the user is told the pivot key is needed and should have one of the enumerated values
required: [a]
properties: { a: { enum: [foo, bar] } }
```
@@ -0,0 +1,176 @@
// JSON-Schema ( draf04 ) validator
import JsonSchemaWebWorker from "./validator.worker.js"
import YAML from "js-yaml"
import PromiseWorker from "promise-worker"
import debounce from "lodash/debounce"
import swagger2SchemaYaml from "./swagger2-schema.yaml"
import oas3SchemaYaml from "./oas3-schema.yaml"
const swagger2Schema = YAML.safeLoad(swagger2SchemaYaml)
const oas3Schema = YAML.safeLoad(oas3SchemaYaml)
// Lazily created promise worker
let _promiseWorker
const promiseWorker = () => {
if (!_promiseWorker)
_promiseWorker = new PromiseWorker(new JsonSchemaWebWorker())
return _promiseWorker
}
export const addSchema = (schema, schemaPath = []) => () => {
promiseWorker().postMessage({
type: "add-schema",
payload: {
schemaPath,
schema
}
})
}
// Figure out what schema we need to use ( we're making provision to be able to do sub-schema validation later on)
// ...for now we just pick which base schema to use (eg: openapi-2-0, openapi-3.0, etc)
export const getSchemaBasePath = () => ({ specSelectors }) => {
// Eg: [openapi-3.0] or [openapi-2-0]
// later on... ["openapi-2.0", "paths", "get"]
const isOAS3 = specSelectors.isOAS3 ? specSelectors.isOAS3() : false
const isSwagger2 = specSelectors.isSwagger2
? specSelectors.isSwagger2()
: false
const isAmbiguousVersion = isOAS3 && isSwagger2
// Refuse to handle ambiguity
if (isAmbiguousVersion) return []
if (isSwagger2) return ["openapi-2.0"]
if (isOAS3) return ["openapi-3.0"]
}
export const setup = () => ({ jsonSchemaValidatorActions }) => {
// Add schemas , once off
jsonSchemaValidatorActions.addSchema(swagger2Schema, ["openapi-2.0"])
jsonSchemaValidatorActions.addSchema(oas3Schema, ["openapi-3.0"])
}
export const validate = ({ spec, path = [], ...rest }) => system => {
// stagger clearing errors, in case there is another debounced validation
// run happening, which can occur when the user's typing cadence matches
// the latency of validation
// TODO: instead of using a timeout, be aware of any pending validation
// promises, and use them to schedule error clearing.
setTimeout(() => {
system.errActions.clear({
source: system.jsonSchemaValidatorSelectors.errSource()
})
}, 50)
system.jsonSchemaValidatorActions.validateDebounced({ spec, path, ...rest })
}
// Create a debounced validate, that is lazy
let _debValidate
export const validateDebounced = (...args) => system => {
// Lazily create one...
if (!_debValidate) {
_debValidate = debounce((...args) => {
system.jsonSchemaValidatorActions.validateImmediate(...args)
}, 200)
}
return _debValidate(...args)
}
export const validateImmediate = ({ spec, path = [] }) => system => {
// schemaPath refers to type of schema, and later might refer to sub-schema
const baseSchemaPath = system.jsonSchemaValidatorSelectors.getSchemaBasePath()
// No base path? Then we're unable to do anything...
if (!baseSchemaPath.length)
throw new Error("Ambiguous schema path, unable to run validation")
return system.jsonSchemaValidatorActions.validateWithBaseSchema({
spec,
path: [...baseSchemaPath, ...path]
})
}
export const validateWithBaseSchema = ({ spec, path = [] }) => system => {
const errSource = system.jsonSchemaValidatorSelectors.errSource()
return promiseWorker()
.postMessage({
type: "validate",
payload: {
jsSpec: spec,
specStr: system.specSelectors.specStr(),
schemaPath: path,
source: errSource
}
})
.then(
({ results, path }) => {
system.jsonSchemaValidatorActions.handleResults(null, {
results,
path
})
},
err => {
system.jsonSchemaValidatorActions.handleResults(err, {})
}
)
}
export const handleResults = (err, { results }) => system => {
if (err) {
// Something bad happened with validation.
throw err
}
system.errActions.clear({
source: system.jsonSchemaValidatorSelectors.errSource()
})
if (!Array.isArray(results)) {
results = [results]
}
// Filter out anything funky
results = results.filter(val => typeof val === "object" && val !== null)
if (results.length) {
system.errActions.newSpecErrBatch(results)
}
}
export default function() {
return {
afterLoad: system => system.jsonSchemaValidatorActions.setup(),
statePlugins: {
jsonSchemaValidator: {
actions: {
addSchema,
validate,
handleResults,
validateDebounced,
validateImmediate,
validateWithBaseSchema,
setup
},
selectors: {
getSchemaBasePath,
errSource() {
// Used to identify the errors generated by this plugin
return "structural"
}
}
},
spec: {
wrapActions: {
validateSpec: (ori, system) => (...args) => {
ori(...args)
const [spec, path] = args
system.jsonSchemaValidatorActions.validate({ spec, path })
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
import "src/polyfills"
import registerPromiseWorker from "promise-worker/register"
import Validator from "./validator"
const validator = new Validator()
registerPromiseWorker(({ type, payload }) => {
if (type == "add-schema") {
const { schema, schemaPath } = payload
validator.addSchema(schema, schemaPath)
return
}
if (type == "validate") {
const { jsSpec, specStr, schemaPath, source } = payload
let validationResults = validator.validate({
jsSpec,
specStr,
schemaPath,
source
})
return { results: validationResults }
}
})
@@ -0,0 +1,118 @@
// Error condenser!
//
// 1. group all errors by path
// 2. score them by message frequency
// 3. select the most frequent messages (ties retain all equally-frequent messages)
// 4. concatenate the params of each occurrence of the most frequent message
// 5. create one condensed error for the path
// 6. return all condensed errors as an array
export function condenseErrors(errors) {
if (!Array.isArray(errors)) {
return []
}
const tree = {}
function countFor(dataPath, message) {
return tree[dataPath][message].length
}
errors.forEach(err => {
const { dataPath, message } = err
if (tree[dataPath] && tree[dataPath][message]) {
tree[dataPath][message].push(err)
} else if (tree[dataPath]) {
tree[dataPath][message] = [err]
} else {
tree[dataPath] = {
[message]: [err]
}
}
})
const dataPaths = Object.keys(tree)
return dataPaths.reduce((res, path) => {
const messages = Object.keys(tree[path])
const mostFrequentMessageNames = messages.reduce(
(obj, msg) => {
const count = countFor(path, msg)
if (count > obj.max) {
return {
messages: [msg],
max: count
}
} else if (count === obj.max) {
obj.messages.push(msg)
return obj
} else {
return obj
}
},
{ max: 0, messages: [] }
).messages
const mostFrequentMessages = mostFrequentMessageNames.map(
name => tree[path][name]
)
const condensedErrors = mostFrequentMessages.map(messages => {
return messages.reduce((prev, err) => {
const obj = Object.assign({}, prev, {
params: mergeParams(prev.params, err.params)
})
if (!prev.params && !err.params) {
delete obj.params
}
return obj
})
})
return res.concat(condensedErrors)
}, [])
}
// Helpers
function mergeParams(objA = {}, objB = {}) {
if (!objA && !objB) {
return undefined
}
const res = {}
for (let k in objA) {
if (Object.prototype.hasOwnProperty.call(objA, k)) {
res[k] = arrayify(objA[k])
}
}
for (let k in objB) {
if (Object.prototype.hasOwnProperty.call(objB, k)) {
if (res[k]) {
const curr = res[k]
res[k] = curr.concat(arrayify(objB[k]))
} else {
res[k] = arrayify(objB[k])
}
}
}
return res
}
function arrayify(thing) {
if (thing === undefined || thing === null) {
return thing
}
if (Array.isArray(thing)) {
return thing
} else {
return [thing]
}
}
@@ -0,0 +1,94 @@
import Ajv from "ajv"
import AjvErrors from "ajv-errors"
import AjvKeywords from "ajv-keywords"
import { getLineNumberForPath } from "./shared.js"
import { condenseErrors } from "./condense-errors.js"
import jsonSchema from "./jsonSchema"
const IGNORED_AJV_PARAMS = ["type", "errors"]
export default class JSONSchemaValidator {
constructor() {
this.ajv = new Ajv({
allErrors: true,
jsonPointers: true,
})
AjvKeywords(this.ajv, "switch")
AjvErrors(this.ajv)
this.addSchema(jsonSchema)
}
addSchema(schema, key) {
this.ajv.addSchema(schema, normalizeKey(key))
}
validate({ jsSpec, specStr, schemaPath, source }) {
this.ajv.validate(normalizeKey(schemaPath), jsSpec)
if (!this.ajv.errors || !this.ajv.errors.length) {
return null
}
const condensedErrors = condenseErrors(this.ajv.errors)
try {
const boundGetLineNumber = getLineNumberForPath.bind(null, specStr)
return condensedErrors.map(err => {
let preparedMessage = err.message
if (err.params) {
preparedMessage += "\n"
for (var k in err.params) {
if (IGNORED_AJV_PARAMS.indexOf(k) === -1) {
const ori = err.params[k]
const value = Array.isArray(ori) ? dedupe(ori).join(", ") : ori
preparedMessage += `${k}: ${value}\n`
}
}
}
const errorPathArray = jsonPointerStringToArray(err.dataPath)
return {
level: "error",
line: boundGetLineNumber(errorPathArray || []),
path: errorPathArray,
message: preparedMessage.trim(),
source,
original: err
}
})
}
catch (err) {
return {
level: "error",
line: err.problem_mark && err.problem_mark.line + 1 || 0,
message: err.problem,
source: "parser",
original: err
}
}
}
}
function dedupe(arr) {
return arr.filter((val, i) => {
return arr.indexOf(val) === i
})
}
function pathToJSONPointer(arr) {
return arr.map(a => (a + "").replace("~", "~0").replace("/", "~1")).join("/")
}
function jsonPointerStringToArray(str) {
return str.split("/")
.map(part => (part + "").replace(/~0/g, "~").replace(/~1/g, "/"))
.filter(str => str.length > 0)
}
// Convert arrays into a string. Safely, by using the JSONPath spec
function normalizeKey(key) {
if (!Array.isArray(key)) key = [key]
return pathToJSONPointer(key)
}
@@ -0,0 +1,147 @@
export default {
id: "http://json-schema.org/draft-04/schema#",
$schema: "http://json-schema.org/draft-04/schema#",
description: "Core schema meta-schema",
definitions: {
schemaArray: {
type: "array",
minItems: 1,
items: { $ref: "#" }
},
positiveInteger: {
type: "integer",
minimum: 0
},
positiveIntegerDefault0: {
allOf: [{ $ref: "#/definitions/positiveInteger" }, { default: 0 }]
},
simpleTypes: {
enum: [
"array",
"boolean",
"integer",
/* "null", */ // removed per https://github.com/swagger-api/swagger-editor/issues/1832#issuecomment-483717197
"number",
"object",
"string"
]
},
stringArray: {
type: "array",
items: { type: "string" },
minItems: 1,
uniqueItems: true
}
},
type: "object",
properties: {
id: {
type: "string",
format: "uri"
},
$schema: {
type: "string",
format: "uri"
},
title: {
type: "string"
},
description: {
type: "string"
},
default: {},
multipleOf: {
type: "number",
minimum: 0,
exclusiveMinimum: true
},
maximum: {
type: "number"
},
exclusiveMaximum: {
type: "boolean",
default: false
},
minimum: {
type: "number"
},
exclusiveMinimum: {
type: "boolean",
default: false
},
maxLength: { $ref: "#/definitions/positiveInteger" },
minLength: { $ref: "#/definitions/positiveIntegerDefault0" },
pattern: {
type: "string",
format: "regex"
},
additionalItems: {
anyOf: [{ type: "boolean" }, { $ref: "#" }],
default: {}
},
items: {
anyOf: [{ $ref: "#" }, { $ref: "#/definitions/schemaArray" }],
default: {}
},
maxItems: { $ref: "#/definitions/positiveInteger" },
minItems: { $ref: "#/definitions/positiveIntegerDefault0" },
uniqueItems: {
type: "boolean",
default: false
},
maxProperties: { $ref: "#/definitions/positiveInteger" },
minProperties: { $ref: "#/definitions/positiveIntegerDefault0" },
required: { $ref: "#/definitions/stringArray" },
additionalProperties: {
anyOf: [{ type: "boolean" }, { $ref: "#" }],
default: {}
},
definitions: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
properties: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
patternProperties: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
dependencies: {
type: "object",
additionalProperties: {
anyOf: [{ $ref: "#" }, { $ref: "#/definitions/stringArray" }]
}
},
enum: {
type: "array",
minItems: 1,
uniqueItems: true
},
type: {
$ref: "#/definitions/simpleTypes"
// anyOf: [
// { $ref: "#/definitions/simpleTypes" },
// {
// type: "array",
// items: { $ref: "#/definitions/simpleTypes" },
// minItems: 1,
// uniqueItems: true
// }
// ]
},
allOf: { $ref: "#/definitions/schemaArray" },
anyOf: { $ref: "#/definitions/schemaArray" },
oneOf: { $ref: "#/definitions/schemaArray" },
not: { $ref: "#" }
},
dependencies: {
exclusiveMaximum: ["maximum"],
exclusiveMinimum: ["minimum"]
},
default: {}
}
@@ -0,0 +1,77 @@
import get from "lodash/get"
export function transformPathToArray(property, jsSpec) {
if (property.slice(0, 9) === "instance.") {
var str = property.slice(9)
} else {
// eslint-disable-next-line no-redeclare
var str = property
}
var pathArr = []
// replace '.', '["', '"]' separators with pipes
str = str.replace(/\.(?![^["]*"\])|(\[\")|(\"\]\.?)/g, "|")
// handle single quotes as well
str = str.replace(/\[\'/g, "|")
str = str.replace(/\'\]/g, "|")
// split on our new delimiter, pipe
str = str.split("|")
str
.map(item => {
// "key[0]" becomes ["key", "0"]
if (item.indexOf("[") > -1) {
let index = parseInt(item.match(/\[(.*)\]/)[1])
let keyName = item.slice(0, item.indexOf("["))
return [keyName, index.toString()]
} else {
return item
}
})
.reduce(function(a, b) {
// flatten!
return a.concat(b)
}, [])
.concat([""]) // add an empty item into the array, so we don't get stuck with something in our buffer below
.reduce((buffer, curr) => {
let obj = pathArr.length ? get(jsSpec, pathArr) : jsSpec
if (get(obj, makeAccessArray(buffer, curr))) {
if (buffer.length) {
pathArr.push(buffer)
}
if (curr.length) {
pathArr.push(curr)
}
return ""
} else {
// attach key to buffer
return `${buffer}${buffer.length ? "." : ""}${curr}`
}
}, "")
if (typeof get(jsSpec, pathArr) !== "undefined") {
return pathArr
} else {
// if our path is not correct (there is no value at the path),
// return null
return null
}
}
function makeAccessArray(buffer, curr) {
let arr = []
if (buffer.length) {
arr.push(buffer)
}
if (curr.length) {
arr.push(curr)
}
return arr
}
@@ -0,0 +1,4 @@
// export * from './ast.js'
// These import/exports are shared code between worker and main bundle.
// Putting them here keeps the distiction clear
export { getLineNumberForPath } from "../../ast/ast.js"