Added Swagger
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import YAML from "yaml-js"
|
||||
import isArray from "lodash/isArray"
|
||||
import lodashFind from "lodash/find"
|
||||
import memoize from "lodash/memoize"
|
||||
|
||||
let cachedCompose = memoize(YAML.compose) // TODO: build a custom cache based on content
|
||||
|
||||
var MAP_TAG = "tag:yaml.org,2002:map"
|
||||
var SEQ_TAG = "tag:yaml.org,2002:seq"
|
||||
|
||||
export function getLineNumberForPath(yaml, path) {
|
||||
|
||||
// Type check
|
||||
if (typeof yaml !== "string") {
|
||||
throw new TypeError("yaml should be a string")
|
||||
}
|
||||
if (!isArray(path)) {
|
||||
throw new TypeError("path should be an array of strings")
|
||||
}
|
||||
|
||||
var i = 0
|
||||
|
||||
let ast = cachedCompose(yaml)
|
||||
|
||||
// simply walks the tree using current path recursively to the point that
|
||||
// path is empty
|
||||
|
||||
return find(ast, path)
|
||||
|
||||
function find(current, path, last) {
|
||||
if(!current) {
|
||||
// something has gone quite wrong
|
||||
// return the last start_mark as a best-effort
|
||||
if(last && last.start_mark)
|
||||
return last.start_mark.line
|
||||
return 0
|
||||
}
|
||||
|
||||
if (path.length && current.tag === MAP_TAG) {
|
||||
for (i = 0; i < current.value.length; i++) {
|
||||
var pair = current.value[i]
|
||||
var key = pair[0]
|
||||
var value = pair[1]
|
||||
|
||||
if (key.value === path[0]) {
|
||||
return find(value, path.slice(1), current)
|
||||
}
|
||||
|
||||
if (key.value === path[0].replace(/\[.*/, "")) {
|
||||
// access the array at the index in the path (example: grab the 2 in "tags[2]")
|
||||
var index = parseInt(path[0].match(/\[(.*)\]/)[1])
|
||||
if(value.value.length === 1 && index !== 0 && !!index) {
|
||||
var nextVal = lodashFind(value.value[0], { value: index.toString() })
|
||||
} else { // eslint-disable-next-line no-redeclare
|
||||
var nextVal = value.value[index]
|
||||
}
|
||||
return find(nextVal, path.slice(1), value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.length && current.tag === SEQ_TAG) {
|
||||
var item = current.value[path[0]]
|
||||
|
||||
if (item && item.tag) {
|
||||
return find(item, path.slice(1), current.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (current.tag === MAP_TAG && !Array.isArray(last)) {
|
||||
return current.start_mark.line
|
||||
} else {
|
||||
return current.start_mark.line + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a position object with given
|
||||
* @param {string} yaml
|
||||
* YAML or JSON string
|
||||
* @param {array} path
|
||||
* an array of stings that constructs a
|
||||
* JSON Path similar to JSON Pointers(RFC 6901). The difference is, each
|
||||
* component of path is an item of the array instead of being separated with
|
||||
* slash(/) in a string
|
||||
*/
|
||||
export function positionRangeForPath(yaml, path) {
|
||||
|
||||
// Type check
|
||||
if (typeof yaml !== "string") {
|
||||
throw new TypeError("yaml should be a string")
|
||||
}
|
||||
if (!isArray(path)) {
|
||||
throw new TypeError("path should be an array of strings")
|
||||
}
|
||||
|
||||
var invalidRange = {
|
||||
start: {line: -1, column: -1},
|
||||
end: {line: -1, column: -1}
|
||||
}
|
||||
var i = 0
|
||||
|
||||
let ast = cachedCompose(yaml)
|
||||
|
||||
// simply walks the tree using astValue path recursively to the point that
|
||||
// path is empty.
|
||||
return find(ast)
|
||||
|
||||
function find(astValue, astKeyValue) {
|
||||
if (astValue.tag === MAP_TAG) {
|
||||
for (i = 0; i < astValue.value.length; i++) {
|
||||
var pair = astValue.value[i]
|
||||
var key = pair[0]
|
||||
var value = pair[1]
|
||||
|
||||
if (key.value === path[0]) {
|
||||
path.shift()
|
||||
return find(value, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (astValue.tag === SEQ_TAG) {
|
||||
var item = astValue.value[path[0]]
|
||||
|
||||
if (item && item.tag) {
|
||||
path.shift()
|
||||
return find(item, astKeyValue)
|
||||
}
|
||||
}
|
||||
|
||||
// if path is still not empty we were not able to find the node
|
||||
if (path.length) {
|
||||
return invalidRange
|
||||
}
|
||||
|
||||
const range = {
|
||||
start: {
|
||||
line: astValue.start_mark.line,
|
||||
column: astValue.start_mark.column,
|
||||
pointer: astValue.start_mark.pointer,
|
||||
},
|
||||
end: {
|
||||
line: astValue.end_mark.line,
|
||||
column: astValue.end_mark.column,
|
||||
pointer: astValue.end_mark.pointer,
|
||||
}
|
||||
}
|
||||
|
||||
if(astKeyValue) {
|
||||
// eslint-disable-next-line camelcase
|
||||
range.key_start = {
|
||||
line: astKeyValue.start_mark.line,
|
||||
column: astKeyValue.start_mark.column,
|
||||
pointer: astKeyValue.start_mark.pointer,
|
||||
}
|
||||
// eslint-disable-next-line camelcase
|
||||
range.key_end = {
|
||||
line: astKeyValue.end_mark.line,
|
||||
column: astKeyValue.end_mark.column,
|
||||
pointer: astKeyValue.end_mark.pointer,
|
||||
}
|
||||
}
|
||||
|
||||
return range
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON Path for position object in the spec
|
||||
* @param {string} yaml
|
||||
* YAML or JSON string
|
||||
* @param {object} position
|
||||
* position in the YAML or JSON string with `line` and `column` properties.
|
||||
* `line` and `column` number are zero indexed
|
||||
*/
|
||||
export function pathForPosition(yaml, position) {
|
||||
|
||||
// Type check
|
||||
if (typeof yaml !== "string") {
|
||||
throw new TypeError("yaml should be a string")
|
||||
}
|
||||
if (typeof position !== "object" || typeof position.line !== "number" ||
|
||||
typeof position.column !== "number") {
|
||||
throw new TypeError("position should be an object with line and column" +
|
||||
" properties")
|
||||
}
|
||||
|
||||
try {
|
||||
var ast = cachedCompose(yaml)
|
||||
} catch (e) {
|
||||
console.error("Error composing AST", e)
|
||||
|
||||
const problemMark = e.problem_mark || {}
|
||||
const errorTraceMessage = [
|
||||
yaml.split("\n").slice(problemMark.line - 5, problemMark.line + 1).join("\n"),
|
||||
Array(problemMark.column).fill(" ").join("") + `^----- ${e.name}: ${e.toString().split("\n")[0]}`,
|
||||
yaml.split("\n").slice(problemMark.line + 1, problemMark.line + 5).join("\n")
|
||||
].join("\n")
|
||||
|
||||
console.error(errorTraceMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
var path = []
|
||||
|
||||
return find(ast)
|
||||
|
||||
/**
|
||||
* recursive find function that finds the node matching the position
|
||||
* @param {object} current - AST object to serach into
|
||||
*/
|
||||
function find(current) {
|
||||
|
||||
// algorythm:
|
||||
// is current a promitive?
|
||||
// // finish recursion without modifying the path
|
||||
// is current a hash?
|
||||
// // find a key or value that position is in their range
|
||||
// // if key is in range, terminate recursion with exisiting path
|
||||
// // if a value is in range push the corresponding key to the path
|
||||
// // andcontinue recursion
|
||||
// is current an array
|
||||
// // find the item that position is in it"s range and push the index
|
||||
// // of the item to the path and continue recursion with that item.
|
||||
|
||||
var i = 0
|
||||
|
||||
if (!current || [MAP_TAG, SEQ_TAG].indexOf(current.tag) === -1) {
|
||||
return path
|
||||
}
|
||||
|
||||
if (current.tag === MAP_TAG) {
|
||||
for (i = 0; i < current.value.length; i++) {
|
||||
var pair = current.value[i]
|
||||
var key = pair[0]
|
||||
var value = pair[1]
|
||||
|
||||
if (isInRange(key)) {
|
||||
return path
|
||||
} else if (isInRange(value)) {
|
||||
path.push(key.value)
|
||||
return find(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current.tag === SEQ_TAG) {
|
||||
for (i = 0; i < current.value.length; i++) {
|
||||
var item = current.value[i]
|
||||
|
||||
if (isInRange(item)) {
|
||||
path.push(i.toString())
|
||||
return find(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
|
||||
/**
|
||||
* Determines if position is in node"s range
|
||||
* @param {object} node - AST node
|
||||
* @return {Boolean} true if position is in node"s range
|
||||
*/
|
||||
function isInRange(node) {
|
||||
/* jshint camelcase: false */
|
||||
|
||||
// if node is in a single line
|
||||
if (node.start_mark.line === node.end_mark.line) {
|
||||
|
||||
return (position.line === node.start_mark.line) &&
|
||||
(node.start_mark.column <= position.column) &&
|
||||
(node.end_mark.column >= position.column)
|
||||
}
|
||||
|
||||
// if position is in the same line as start_mark
|
||||
if (position.line === node.start_mark.line) {
|
||||
return position.column >= node.start_mark.column
|
||||
}
|
||||
|
||||
// if position is in the same line as end_mark
|
||||
if (position.line === node.end_mark.line) {
|
||||
return position.column <= node.end_mark.column
|
||||
}
|
||||
|
||||
// if position is between start and end lines return true, otherwise
|
||||
// return false.
|
||||
return (node.start_mark.line < position.line) &&
|
||||
(node.end_mark.line > position.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utility fns
|
||||
|
||||
|
||||
export let pathForPositionAsync = promisifySyncFn(pathForPosition)
|
||||
export let positionRangeForPathAsync = promisifySyncFn(positionRangeForPath)
|
||||
export let getLineNumberForPathAsync = promisifySyncFn(getLineNumberForPath)
|
||||
|
||||
function promisifySyncFn(fn) {
|
||||
return function(...args) {
|
||||
return new Promise((resolve) => resolve(fn(...args)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as AST from "./ast"
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
fn: { AST }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import keywordMap from "./keyword-map"
|
||||
import getKeywordsForPath from "./get-keywords-for-path"
|
||||
|
||||
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
|
||||
|
||||
const { fn: { getPathForPosition }, specSelectors } = system
|
||||
|
||||
const { isOAS3 } = specSelectors
|
||||
|
||||
if(isOAS3 && isOAS3()) {
|
||||
// isOAS3 selector exists, and returns true
|
||||
return cb(null, null)
|
||||
}
|
||||
|
||||
const { AST } = ctx
|
||||
var editorValue = editor.getValue()
|
||||
const path = getPathForPosition({ pos, prefix, editorValue, AST})
|
||||
|
||||
const suggestions = getKeywordsForPath({ system, path, keywordMap })
|
||||
cb(null, suggestions)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import isArray from "lodash/isArray"
|
||||
import isObject from "lodash/isObject"
|
||||
import mapValues from "lodash/mapValues"
|
||||
import isPlainObject from "lodash/isPlainObject"
|
||||
import toArray from "lodash/toArray"
|
||||
import isString from "lodash/isString"
|
||||
import get from "lodash/get"
|
||||
|
||||
export default function getKeywordsForPath({ system, path, keywordMap }) {
|
||||
keywordMap = Object.assign({}, keywordMap)
|
||||
|
||||
// is getting path was not successful stop here and return no candidates
|
||||
if (!isArray(path)) {
|
||||
return [
|
||||
{
|
||||
name: "array",
|
||||
value: " ",
|
||||
score: 300,
|
||||
meta: "Couldn't load suggestions"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if(path[path.length - 2] === "tags" && path.length > 2) {
|
||||
// 'path.length > 2' excludes top-level 'tags'
|
||||
return system.specSelectors.tags().map(tag => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
value: tag.get("name"),
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
let reversePath = path.slice(0).reverse()
|
||||
if(reversePath[1] === "security" && isNumeric(reversePath[0])) {
|
||||
// **.security[x]
|
||||
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
caption: sec,
|
||||
snippet: `${sec}: []`
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
if(reversePath[0] === "security") {
|
||||
// **.security:
|
||||
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
caption: sec,
|
||||
snippet: `\n- ${sec}: []`
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
// traverse down the keywordMap for each key in the path until there is
|
||||
// no key in the path
|
||||
|
||||
var key = path.shift()
|
||||
|
||||
while (key && isObject(keywordMap)) {
|
||||
keywordMap = getChild(keywordMap, key)
|
||||
key = path.shift()
|
||||
}
|
||||
|
||||
// if no keywordMap was found after the traversal return no candidates
|
||||
if (!isObject(keywordMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// if keywordMap is an array of strings, return the array as list of
|
||||
// suggestions
|
||||
if (isArray(keywordMap) && keywordMap.every(isString)) {
|
||||
return keywordMap.map(constructAceCompletion.bind(null, "value"))
|
||||
}
|
||||
|
||||
// If keywordMap is describing an array unwrap the inner map so we can
|
||||
// suggest for array items
|
||||
if (isArray(keywordMap)) {
|
||||
if(isArray(keywordMap[0])) {
|
||||
return keywordMap[0].map(item => {
|
||||
return {
|
||||
name: "array",
|
||||
value: "- " + item,
|
||||
score: 300,
|
||||
meta: "array item"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return [{
|
||||
name: "array",
|
||||
value: "- ",
|
||||
score: 300,
|
||||
meta: "array item"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// if keywordMap is not an object at this point return no candidates
|
||||
if (!isObject(keywordMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// for each key in keywordMap map construct a completion candidate and
|
||||
// return the array
|
||||
return suggestionFromSchema(keywordMap)
|
||||
}
|
||||
|
||||
function getChild(object, key) {
|
||||
var keys = Object.keys(object)
|
||||
var regex
|
||||
var isArrayAccess = /^\d+$/.test(key)
|
||||
|
||||
if(isArrayAccess && isArray(object)) {
|
||||
return object[0]
|
||||
}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
let childVal = object[keys[i]]
|
||||
|
||||
if(!childVal) {
|
||||
return null
|
||||
}
|
||||
|
||||
regex = new RegExp(childVal.__regex || keys[i])
|
||||
|
||||
if (regex.test(key) && childVal) {
|
||||
if(typeof childVal === "object" && !isArray(childVal)) {
|
||||
return Object.assign({}, childVal)
|
||||
} else {
|
||||
return childVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function suggestionFromSchema(map) {
|
||||
const res = toArray(mapValues(map, (val, key) => {
|
||||
const keyword = get(val, "__value", key)
|
||||
const meta = isPlainObject(val) ? "object" : "keyword"
|
||||
|
||||
return constructAceCompletion(meta, keyword)
|
||||
}))
|
||||
return res
|
||||
}
|
||||
|
||||
function constructAceCompletion(meta, keyword) {
|
||||
if(keyword.slice(0, 2) === "__") {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Give keywords, that extra colon
|
||||
let snippet
|
||||
switch(meta) {
|
||||
case "keyword":
|
||||
snippet = `${keyword}: `
|
||||
break
|
||||
case "object":
|
||||
snippet = `${keyword}:\n `
|
||||
break
|
||||
default:
|
||||
snippet = keyword
|
||||
}
|
||||
|
||||
// snippet's treat `$` as special characters
|
||||
snippet = snippet.replace("$", "\\$")
|
||||
|
||||
return {
|
||||
snippet,
|
||||
caption: keyword,
|
||||
score: 300,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
||||
function isNumeric(obj) {
|
||||
return !isNaN(obj)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestKeywordsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
|
||||
var Bool = ["true", "false"]
|
||||
var Anything = String
|
||||
|
||||
var combine = (...objs) => objs ? Object.assign({}, ...objs) : {}
|
||||
|
||||
var makeValue = (val = "") => {
|
||||
return {
|
||||
__value: val
|
||||
}
|
||||
}
|
||||
|
||||
var emptyValue = makeValue("")
|
||||
|
||||
var externalDocs = {
|
||||
description: String,
|
||||
url: String
|
||||
}
|
||||
|
||||
|
||||
var xml = {
|
||||
name: String,
|
||||
namespace: String,
|
||||
prefix: String,
|
||||
attribute: Bool,
|
||||
wrapped: Bool
|
||||
}
|
||||
|
||||
var schema = {
|
||||
$ref: String,
|
||||
format: String,
|
||||
title: String,
|
||||
description: String,
|
||||
default: String,
|
||||
maximum: Number,
|
||||
minimum: Number,
|
||||
exclusiveMaximum: Bool,
|
||||
exclusiveMinimum: Bool,
|
||||
maxLength: Number,
|
||||
minLength: Number,
|
||||
pattern: String,
|
||||
maxItems: Number,
|
||||
minItems: Number,
|
||||
uniqueItems: Bool,
|
||||
enum: [String],
|
||||
multipleOf: Number,
|
||||
maxProperties: Number,
|
||||
minProperties: Number,
|
||||
required: [String],
|
||||
type: ["string", "number", "integer", "boolean", "array", "object"],
|
||||
get items () { return this },
|
||||
get allOf () { return [this] },
|
||||
get properties () {
|
||||
return {
|
||||
".": this
|
||||
}
|
||||
},
|
||||
get additionalProperties () { return this },
|
||||
discriminator: String,
|
||||
readOnly: Bool,
|
||||
xml: xml,
|
||||
externalDocs: externalDocs,
|
||||
example: String
|
||||
}
|
||||
|
||||
var schemes = [
|
||||
"http",
|
||||
"https",
|
||||
"ws",
|
||||
"wss"
|
||||
]
|
||||
|
||||
var items = {
|
||||
type: ["string", "number", "integer", "boolean", "array"],
|
||||
format: String,
|
||||
get items () { return this },
|
||||
collectionFormat: ["csv"],
|
||||
default: Anything,
|
||||
minimum: String,
|
||||
maximum: String,
|
||||
exclusiveMinimum: Bool,
|
||||
exclusiveMaximum: Bool,
|
||||
minLength: String,
|
||||
maxLength: String,
|
||||
pattern: String,
|
||||
minItems: String,
|
||||
maxItems: String,
|
||||
uniqueItems: Bool,
|
||||
enum: [Anything],
|
||||
multipleOf: String
|
||||
}
|
||||
|
||||
var header = {
|
||||
description: String,
|
||||
type: String,
|
||||
format: String,
|
||||
items: items,
|
||||
collectionFormat: ["csv"],
|
||||
default: Anything,
|
||||
enum: [String],
|
||||
minimum: String,
|
||||
maximum: String,
|
||||
exclusiveMinimum: Bool,
|
||||
exclusiveMaximum: Bool,
|
||||
multipleOf: String,
|
||||
maxLength: String,
|
||||
minLength: String,
|
||||
pattern: String,
|
||||
minItems: String,
|
||||
maxItems: String,
|
||||
uniqueItems: Bool
|
||||
}
|
||||
|
||||
var parameter = {
|
||||
name: String,
|
||||
description: String,
|
||||
required: ["true", "false"],
|
||||
type: [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"integer",
|
||||
"array",
|
||||
"file"
|
||||
],
|
||||
format: String,
|
||||
schema: schema,
|
||||
enum: [String],
|
||||
minimum: String,
|
||||
maximum: String,
|
||||
exclusiveMinimum: Bool,
|
||||
exclusiveMaximum: Bool,
|
||||
multipleOf: String,
|
||||
maxLength: String,
|
||||
minLength: String,
|
||||
pattern: String,
|
||||
minItems: String,
|
||||
maxItems: String,
|
||||
uniqueItems: Bool,
|
||||
allowEmptyValue: Bool,
|
||||
collectionFormat: ["csv", "multi"],
|
||||
default: String,
|
||||
items: items,
|
||||
in: [
|
||||
"body",
|
||||
"formData",
|
||||
"header",
|
||||
"path",
|
||||
"query"
|
||||
]
|
||||
}
|
||||
|
||||
var reference = {
|
||||
"$ref": String
|
||||
}
|
||||
|
||||
var response = {
|
||||
description: String,
|
||||
schema: schema,
|
||||
headers: {
|
||||
".": combine(header, {
|
||||
__value: ""
|
||||
})
|
||||
},
|
||||
examples: String
|
||||
}
|
||||
|
||||
var operation = {
|
||||
summary: String,
|
||||
description: String,
|
||||
schemes: [schemes],
|
||||
externalDocs: externalDocs,
|
||||
operationId: String,
|
||||
produces: [String],
|
||||
consumes: [String],
|
||||
deprecated: Bool,
|
||||
security: [String],
|
||||
parameters: [combine(reference, parameter)],
|
||||
responses: {
|
||||
"[2-6][0-9][0-9]": combine(reference, response, emptyValue),
|
||||
"default": combine(reference, response)
|
||||
},
|
||||
tags: [String]
|
||||
}
|
||||
|
||||
var securityScheme = {
|
||||
type: ["oauth2", "apiKey", "basic"],
|
||||
description: String,
|
||||
name: String,
|
||||
in: ["query", "header"],
|
||||
flow: ["implicit", "password", "application", "accessCode"],
|
||||
authorizationUrl: String,
|
||||
tokenUrl: String,
|
||||
scopes: String // actually an object, but this is equivalent
|
||||
}
|
||||
|
||||
var info = {
|
||||
version: String,
|
||||
title: String,
|
||||
description: String,
|
||||
termsOfService: String,
|
||||
contact: {
|
||||
name: String,
|
||||
url: String,
|
||||
email: String
|
||||
},
|
||||
license: {
|
||||
name: String,
|
||||
url: String
|
||||
}
|
||||
}
|
||||
|
||||
var map = {
|
||||
swagger: ["\'2.0\'"],
|
||||
info: info,
|
||||
|
||||
host: String,
|
||||
basePath: String,
|
||||
|
||||
schemes: [schemes],
|
||||
produces: [String],
|
||||
consumes: [String],
|
||||
|
||||
paths: {
|
||||
|
||||
//path
|
||||
".": {
|
||||
__value: "",
|
||||
parameters: [combine(reference, parameter)],
|
||||
"get": operation,
|
||||
"put": operation,
|
||||
"post": operation,
|
||||
"delete": operation,
|
||||
"options": operation,
|
||||
"head": operation,
|
||||
"patch": operation,
|
||||
"$ref": String
|
||||
}
|
||||
},
|
||||
|
||||
definitions: {
|
||||
|
||||
// Definition name
|
||||
".": combine(schema, emptyValue)
|
||||
},
|
||||
|
||||
parameters: {
|
||||
".": combine(reference, parameter, emptyValue)
|
||||
},
|
||||
responses: {
|
||||
"[2-6][0-9][0-9]": combine(response, emptyValue)
|
||||
},
|
||||
securityDefinitions: {
|
||||
".": combine(securityScheme, emptyValue)
|
||||
},
|
||||
security: [String],
|
||||
tags: [{
|
||||
name: String,
|
||||
description: String,
|
||||
externalDocs: externalDocs
|
||||
}],
|
||||
externalDocs: externalDocs
|
||||
}
|
||||
|
||||
export default map
|
||||
@@ -0,0 +1,11 @@
|
||||
import getCompletions from "./get-completions"
|
||||
|
||||
// Add an autosuggest completer
|
||||
export const addAutosuggestionCompleters = (ori, system) => (context) => {
|
||||
return ori(context).concat([{
|
||||
getCompletions(...args) {
|
||||
// Add `context`, then `system` as the last args
|
||||
return getCompletions(...args, context, system)
|
||||
}
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import keywordMap from "./keyword-map"
|
||||
import getKeywordsForPath from "./get-keywords-for-path"
|
||||
|
||||
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
|
||||
|
||||
const { fn: { getPathForPosition }, specSelectors } = system
|
||||
|
||||
const { isOAS3 } = specSelectors
|
||||
|
||||
if(isOAS3 && !isOAS3()) {
|
||||
// isOAS3 selector exists, and returns false
|
||||
return cb(null, null)
|
||||
}
|
||||
|
||||
const { AST } = ctx
|
||||
var editorValue = editor.getValue()
|
||||
const path = getPathForPosition({ pos, prefix, editorValue, AST})
|
||||
|
||||
const suggestions = getKeywordsForPath({ system, path, keywordMap })
|
||||
cb(null, suggestions)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import isArray from "lodash/isArray"
|
||||
import isObject from "lodash/isObject"
|
||||
import mapValues from "lodash/mapValues"
|
||||
import isPlainObject from "lodash/isPlainObject"
|
||||
import toArray from "lodash/toArray"
|
||||
import isString from "lodash/isString"
|
||||
import get from "lodash/get"
|
||||
|
||||
export default function getKeywordsForPath({ system, path, keywordMap}) {
|
||||
keywordMap = Object.assign({}, keywordMap)
|
||||
|
||||
// is getting path was not successful stop here and return no candidates
|
||||
if (!isArray(path)) {
|
||||
return [
|
||||
{
|
||||
name: "array",
|
||||
value: " ",
|
||||
score: 300,
|
||||
meta: "Couldn't load suggestions"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if(path[path.length - 2] === "tags" && path.length > 2) {
|
||||
// 'path.length > 2' excludes top-level 'tags'
|
||||
return system.specSelectors.tags().map(tag => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
value: tag.get("name"),
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
let reversePath = path.slice(0).reverse()
|
||||
if(reversePath[1] === "security" && isNumeric(reversePath[0])) {
|
||||
// **.security[x]
|
||||
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
caption: sec,
|
||||
snippet: `${sec}: []`
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
if(reversePath[0] === "security") {
|
||||
// **.security:
|
||||
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
|
||||
score: 0,
|
||||
meta: "local",
|
||||
caption: sec,
|
||||
snippet: `\n- ${sec}: []`
|
||||
})).toJS()
|
||||
}
|
||||
|
||||
// traverse down the keywordMap for each key in the path until there is
|
||||
// no key in the path
|
||||
|
||||
var key = path.shift()
|
||||
|
||||
while (key && isObject(keywordMap)) {
|
||||
keywordMap = getChild(keywordMap, key)
|
||||
key = path.shift()
|
||||
}
|
||||
|
||||
// if no keywordMap was found after the traversal return no candidates
|
||||
if (!isObject(keywordMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// if keywordMap is an array of strings, return the array as list of
|
||||
// suggestions
|
||||
if (isArray(keywordMap) && keywordMap.every(isString)) {
|
||||
return keywordMap.map(constructAceCompletion.bind(null, "value"))
|
||||
}
|
||||
|
||||
// If keywordMap is describing an array unwrap the inner map so we can
|
||||
// suggest for array items
|
||||
if (isArray(keywordMap)) {
|
||||
if(isArray(keywordMap[0])) {
|
||||
return keywordMap[0].map(item => {
|
||||
return {
|
||||
name: "array",
|
||||
value: "- " + item,
|
||||
score: 300,
|
||||
meta: "array item"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return [{
|
||||
name: "array",
|
||||
value: "- ",
|
||||
score: 300,
|
||||
meta: "array item"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// if keywordMap is not an object at this point return no candidates
|
||||
if (!isObject(keywordMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// for each key in keywordMap map construct a completion candidate and
|
||||
// return the array
|
||||
return suggestionFromSchema(keywordMap)
|
||||
}
|
||||
|
||||
function getChild(object, key) {
|
||||
var keys = Object.keys(object)
|
||||
var regex
|
||||
var isArrayAccess = /^\d+$/.test(key)
|
||||
|
||||
if(isArrayAccess && isArray(object)) {
|
||||
return object[0]
|
||||
}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
let childVal = object[keys[i]]
|
||||
|
||||
if (!childVal) {
|
||||
return null
|
||||
}
|
||||
|
||||
regex = new RegExp(childVal.__regex || keys[i])
|
||||
|
||||
if (regex.test(key) && childVal) {
|
||||
if(typeof childVal === "object" && !isArray(childVal)) {
|
||||
return Object.assign({}, childVal)
|
||||
} else {
|
||||
return childVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function suggestionFromSchema(map) {
|
||||
const res = toArray(mapValues(map, (val, key) => {
|
||||
const keyword = get(val, "__value", key)
|
||||
const meta = isPlainObject(val) ? "object" : "keyword"
|
||||
|
||||
return constructAceCompletion(meta, keyword)
|
||||
}))
|
||||
return res
|
||||
}
|
||||
|
||||
function constructAceCompletion(meta, keyword) {
|
||||
if(keyword.slice(0, 2) === "__") {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Give keywords, that extra colon
|
||||
let snippet
|
||||
switch(meta) {
|
||||
case "keyword":
|
||||
snippet = `${keyword}: `
|
||||
break
|
||||
case "object":
|
||||
snippet = `${keyword}:\n `
|
||||
break
|
||||
default:
|
||||
snippet = keyword
|
||||
}
|
||||
|
||||
// snippet's treat `$` as special characters
|
||||
snippet = snippet.replace("$", "\\$")
|
||||
|
||||
return {
|
||||
snippet,
|
||||
caption: keyword,
|
||||
score: 300,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
||||
function isNumeric(obj) {
|
||||
return !isNaN(obj)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestOAS3KeywordsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ExternalDocumentation,
|
||||
Info,
|
||||
SecurityRequirement,
|
||||
Server,
|
||||
Tag,
|
||||
Components,
|
||||
Paths
|
||||
} from "./oas3-objects.js"
|
||||
|
||||
export default {
|
||||
openapi: String,
|
||||
info: Info,
|
||||
servers: [Server],
|
||||
paths: Paths,
|
||||
components: Components,
|
||||
security: [SecurityRequirement],
|
||||
tags: [Tag],
|
||||
externalDocs: ExternalDocumentation
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// Adapted from OAS 3.0.0-rc2
|
||||
|
||||
// comma dangles in this file = cleaner diffs
|
||||
/*eslint comma-dangle: ["error", "always-multiline"]*/
|
||||
|
||||
// anyOf and combine are the same for now.
|
||||
// they are seperated for semantics, and for possible future improvement
|
||||
const anyOf = (...objs) => objs ? Object.assign({}, ...objs) : {}
|
||||
const stringEnum = (arr) => arr
|
||||
|
||||
const Any = null
|
||||
|
||||
export const ExternalDocumentation = {
|
||||
description: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
export const Contact = {
|
||||
name: String,
|
||||
url: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
export const License = {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
export const Info = {
|
||||
title: String,
|
||||
description: String,
|
||||
termsOfService: String,
|
||||
contact: Contact,
|
||||
license: License,
|
||||
version: String,
|
||||
}
|
||||
|
||||
export const ServerVariable = {
|
||||
enum: [String],
|
||||
default: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
export const XML = {
|
||||
name: String,
|
||||
namespace: String,
|
||||
prefix: String,
|
||||
attribute: Boolean,
|
||||
wrapped: Boolean,
|
||||
}
|
||||
|
||||
export const OAuthFlow = {
|
||||
authorizationUrl: String,
|
||||
tokenUrl: String,
|
||||
refreshUrl: String,
|
||||
scopes: {
|
||||
".": String,
|
||||
},
|
||||
}
|
||||
|
||||
export const Reference = {
|
||||
"$ref": String,
|
||||
}
|
||||
|
||||
export const Example = {
|
||||
summary: String,
|
||||
description: String,
|
||||
value: Any,
|
||||
externalValue: String,
|
||||
}
|
||||
|
||||
export const SecurityRequirement = {
|
||||
".": [String],
|
||||
}
|
||||
|
||||
export const Server = {
|
||||
url: String,
|
||||
description: String,
|
||||
variables: {
|
||||
".": ServerVariable,
|
||||
},
|
||||
}
|
||||
|
||||
export const Link = {
|
||||
operationRef: String,
|
||||
operationId: String,
|
||||
parameters: {
|
||||
".": Any,
|
||||
},
|
||||
requestBody: Any,
|
||||
description: String,
|
||||
server: Server,
|
||||
}
|
||||
|
||||
export const Schema = {
|
||||
// Lifted from JSONSchema
|
||||
title: String,
|
||||
multipleOf: String,
|
||||
maximum: String,
|
||||
exclusiveMaximum: String,
|
||||
minimum: String,
|
||||
exclusiveMinimum: String,
|
||||
maxLength: String,
|
||||
minLength: String,
|
||||
pattern: RegExp,
|
||||
maxItems: String,
|
||||
minItems: String,
|
||||
uniqueItems: Boolean,
|
||||
maxProperties: String,
|
||||
minProperties: String,
|
||||
required: Boolean,
|
||||
enum: String,
|
||||
// Adapted from JSONSchema
|
||||
type: String,
|
||||
get allOf () { return this },
|
||||
get oneOf () { return this },
|
||||
get anyOf () { return this },
|
||||
get not () { return this },
|
||||
get items () { return this },
|
||||
get properties () {
|
||||
return {
|
||||
".": this,
|
||||
}
|
||||
},
|
||||
get additionalProperties () { return this },
|
||||
description: String,
|
||||
format: String,
|
||||
default: Any,
|
||||
nullable: Boolean,
|
||||
readOnly: Boolean,
|
||||
writeOnly: Boolean,
|
||||
xml: XML,
|
||||
externalDocs: ExternalDocumentation,
|
||||
example: Any,
|
||||
deprecated: Boolean,
|
||||
}
|
||||
|
||||
export const Encoding = {
|
||||
contentType: String,
|
||||
headers: {
|
||||
".": undefined,
|
||||
},
|
||||
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
|
||||
explode: Boolean,
|
||||
allowReserved: Boolean,
|
||||
}
|
||||
|
||||
export const MediaType = {
|
||||
schema: anyOf(Schema, Reference),
|
||||
example: Any,
|
||||
examples: {
|
||||
".": anyOf(Example, Reference),
|
||||
},
|
||||
encoding: {
|
||||
".": Encoding,
|
||||
},
|
||||
}
|
||||
|
||||
export const Parameter = {
|
||||
name: String,
|
||||
in: stringEnum(["query", "header", "path", "cookie"]),
|
||||
description: String,
|
||||
required: Boolean,
|
||||
deprecated: Boolean,
|
||||
allowEmptyValue: Boolean,
|
||||
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
|
||||
explode: String,
|
||||
allowReserved: Boolean,
|
||||
schema: anyOf(Schema, Reference),
|
||||
example: Any,
|
||||
examples: {
|
||||
".": anyOf(Example, Reference),
|
||||
},
|
||||
content: {
|
||||
".": MediaType,
|
||||
},
|
||||
}
|
||||
|
||||
export const Header = {
|
||||
description: String,
|
||||
required: Boolean,
|
||||
deprecated: Boolean,
|
||||
allowEmptyValue: Boolean,
|
||||
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
|
||||
explode: String,
|
||||
allowReserved: Boolean,
|
||||
schema: anyOf(Schema, Reference),
|
||||
example: Any,
|
||||
examples: {
|
||||
".": anyOf(Example, Reference),
|
||||
},
|
||||
content: {
|
||||
".": MediaType,
|
||||
},
|
||||
}
|
||||
|
||||
export const RequestBody = {
|
||||
description: String,
|
||||
content: {
|
||||
".": MediaType,
|
||||
},
|
||||
}
|
||||
|
||||
export const Response = {
|
||||
description: String,
|
||||
headers: {
|
||||
".": anyOf(Header, Reference),
|
||||
},
|
||||
content: {
|
||||
".": MediaType,
|
||||
},
|
||||
links: {
|
||||
".": anyOf(Link, Reference),
|
||||
},
|
||||
}
|
||||
|
||||
export const Responses = {
|
||||
default: anyOf(Response, Reference),
|
||||
"\\d\\d\\d|\\d\\dX|\\dXX": anyOf(Response, Reference),
|
||||
}
|
||||
|
||||
export const Callback = {
|
||||
// ".": PathItem,
|
||||
}
|
||||
|
||||
export const Tag = {
|
||||
name: String,
|
||||
description: String,
|
||||
externalDocs: ExternalDocumentation,
|
||||
}
|
||||
|
||||
export const OAuthFlows = {
|
||||
implicit: OAuthFlow,
|
||||
password: OAuthFlow,
|
||||
clientCredentials: OAuthFlow,
|
||||
authorizationCode: OAuthFlow,
|
||||
}
|
||||
|
||||
export const SecurityScheme = {
|
||||
type: String,
|
||||
description: String,
|
||||
name: String,
|
||||
in: String,
|
||||
scheme: String,
|
||||
bearerFormat: String,
|
||||
flows: OAuthFlows,
|
||||
openIdConnectUrl: String,
|
||||
}
|
||||
|
||||
const ComponentFixedFieldRegex = "^[a-zA-Z0-9\.\-_]+$"
|
||||
|
||||
export const Components = {
|
||||
schemas: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Schema, Reference),
|
||||
},
|
||||
responses: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Response, Reference),
|
||||
},
|
||||
parameters: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Parameter, Reference),
|
||||
},
|
||||
examples: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Example, Reference),
|
||||
},
|
||||
requestBodies: {
|
||||
[ComponentFixedFieldRegex]: anyOf(RequestBody, Reference),
|
||||
},
|
||||
headers: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Header, Reference),
|
||||
},
|
||||
securitySchemes: {
|
||||
[ComponentFixedFieldRegex]: anyOf(SecurityScheme, Reference),
|
||||
},
|
||||
links: {
|
||||
[ComponentFixedFieldRegex]: anyOf(Link, Reference),
|
||||
},
|
||||
callbacks: {
|
||||
get [ComponentFixedFieldRegex]() { return anyOf(Callback, Reference) },
|
||||
},
|
||||
}
|
||||
|
||||
export const Operation = {
|
||||
tags: [String],
|
||||
summary: String,
|
||||
description: String,
|
||||
externalDocs: ExternalDocumentation,
|
||||
operationId: String,
|
||||
parameters: [anyOf(Parameter, Reference)],
|
||||
requestBody: anyOf(RequestBody, Reference),
|
||||
responses: Responses,
|
||||
get callbacks() {
|
||||
return {
|
||||
".": anyOf(Callback, Reference),
|
||||
}
|
||||
},
|
||||
deprecated: Boolean,
|
||||
security: [SecurityRequirement],
|
||||
servers: [Server],
|
||||
}
|
||||
|
||||
export const Discriminator = {
|
||||
propertyName: String,
|
||||
mapping: {
|
||||
".": String,
|
||||
},
|
||||
}
|
||||
|
||||
export const PathItem = anyOf(Reference, {
|
||||
summary: String,
|
||||
description: String,
|
||||
get: Operation,
|
||||
put: Operation,
|
||||
post: Operation,
|
||||
delete: Operation,
|
||||
options: Operation,
|
||||
head: Operation,
|
||||
patch: Operation,
|
||||
trace: Operation,
|
||||
servers: Server,
|
||||
parameters: anyOf(Parameter, Reference),
|
||||
})
|
||||
|
||||
export const Paths = {
|
||||
"/.": PathItem,
|
||||
}
|
||||
|
||||
// solves `PathItem -> Operation -> Callback -> PathItem` circular reference
|
||||
Callback["."] = PathItem
|
||||
|
||||
// solves `Encoding -> Header -> MediaType -> Encoding` circular reference
|
||||
Encoding.headers["."] = Header
|
||||
@@ -0,0 +1,11 @@
|
||||
import getCompletions from "./get-completions"
|
||||
|
||||
// Add an autosuggest completer
|
||||
export const addAutosuggestionCompleters = (ori, system) => (context) => {
|
||||
return ori(context).concat([{
|
||||
getCompletions(...args) {
|
||||
// Add `context`, then `system` as the last args
|
||||
return getCompletions(...args, context, system)
|
||||
}
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import getRefsForPath from "./get-refs-for-path"
|
||||
|
||||
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
|
||||
|
||||
const { fn: { getPathForPosition } } = system
|
||||
const { AST } = ctx
|
||||
var editorValue = editor.getValue()
|
||||
const path = getPathForPosition({ pos, prefix, editorValue, AST})
|
||||
|
||||
const suggestions = getRefsForPath({ system, path})
|
||||
cb(null, suggestions)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import isArray from "lodash/isArray"
|
||||
import last from "lodash/last"
|
||||
|
||||
export default function getRefsForPath({ system, path }) {
|
||||
|
||||
// Note fellow ace hackers:
|
||||
// we have to be weary of _what_ ace will filter on, see the order ( probably should be fixed, but... ): https://github.com/ajaxorg/ace/blob/b219b5584456534fbccb5fb20470c61011fa0b0a/lib/ace/autocomplete.js#L469
|
||||
// Because of that, I'm matching on `caption` and using `snippet` instead of `value` for injecting
|
||||
if(isArray(path) && last(path) === "$ref") {
|
||||
const localRefs = system.specSelectors.localRefs()
|
||||
const refType = system.specSelectors.getRefType(path)
|
||||
return localRefs
|
||||
.filter(r => r.get("type") == refType)
|
||||
.toJS()
|
||||
.map(r => ({
|
||||
score: 100,
|
||||
meta: "local",
|
||||
snippet: `'${r.$ref}'`, // wrap in quotes
|
||||
caption: r.name,
|
||||
}))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestRefsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import getCompletions from "./get-completions"
|
||||
|
||||
// Add an autosuggest completer
|
||||
export const addAutosuggestionCompleters = (ori, system) => (context) => {
|
||||
return ori(context).concat([{
|
||||
getCompletions(...args) {
|
||||
// Add `context`, then `system` as the last args
|
||||
return getCompletions(...args, context, system)
|
||||
}
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import snippets from "./snippets"
|
||||
import getSnippetsForPath from "./get-snippets-for-path"
|
||||
|
||||
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
|
||||
|
||||
const { fn: { getPathForPosition }, specSelectors } = system
|
||||
const { isOAS3 } = specSelectors
|
||||
|
||||
if(isOAS3 && isOAS3()) {
|
||||
// isOAS3 selector exists, and returns true
|
||||
return cb(null, null)
|
||||
}
|
||||
|
||||
const { AST } = ctx
|
||||
const editorValue = editor.getValue()
|
||||
const path = getPathForPosition({ pos, prefix, editorValue, AST})
|
||||
|
||||
const suggestions = getSnippetsForPath({ path, snippets})
|
||||
|
||||
return cb(null, suggestions)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import isArray from "lodash/isArray"
|
||||
|
||||
export default function getSnippetsForPath({ path, snippets }) {
|
||||
// find all possible snippets, modify them to be compatible with Ace and
|
||||
// sort them based on their position. Sorting is done by assigning a score
|
||||
// to each snippet, not by sorting the array
|
||||
if (!isArray(path)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return snippets
|
||||
.filter(snippet => {
|
||||
return snippet.path.length === path.length
|
||||
})
|
||||
.filter(snippet => {
|
||||
return snippet.path.every((k, i) => {
|
||||
return !!(new RegExp(k)).test(path[i])
|
||||
})
|
||||
})
|
||||
.map(snippet => {
|
||||
// change shape of snippets for ACE
|
||||
return {
|
||||
caption: snippet.name,
|
||||
snippet: snippet.content,
|
||||
meta: "snippet"
|
||||
}
|
||||
})
|
||||
.map(snippetSorterForPos(path))
|
||||
}
|
||||
|
||||
export function snippetSorterForPos(path) {
|
||||
return function(snippet) {
|
||||
// by default score is high
|
||||
let score = 1000
|
||||
|
||||
// if snippets content has the keyword it will get a lower score because
|
||||
// it's more likely less relevant
|
||||
// (FIX) is this logic work for all cases?
|
||||
path.forEach(function(keyword) {
|
||||
if (snippet.snippet.indexOf(keyword)) {
|
||||
score = 500
|
||||
}
|
||||
})
|
||||
|
||||
snippet.score = score
|
||||
|
||||
return snippet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestSnippetsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
const operationRegex = "get|put|post|delete|options|head|patch"
|
||||
|
||||
/**
|
||||
* Makes an HTTP operation snippet's content based on operation name
|
||||
*
|
||||
* @param {string} operationName - the HTTP verb
|
||||
*
|
||||
* @return {string} - the snippet content for that operation
|
||||
*/
|
||||
function makeOperationSnippet(operationName) {
|
||||
return [
|
||||
"${1:" + operationName + "}:",
|
||||
" summary: ${2}",
|
||||
" description: ${2}",
|
||||
" responses:",
|
||||
" ${3:200:}",
|
||||
" description: ${4:OK}",
|
||||
"${6}"
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an HTTP response code snippet's content based on code
|
||||
*
|
||||
* @param {string} code - HTTP Response Code
|
||||
*
|
||||
* @return {string} - Snippet content
|
||||
*/
|
||||
function makeResponseCodeSnippet(code) {
|
||||
return [
|
||||
"${1:" + code + "}:",
|
||||
" description: ${2}",
|
||||
"${3}"
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
name: "swagger",
|
||||
trigger: "sw",
|
||||
path: [],
|
||||
content: [
|
||||
"swagger: \'2.0\'",
|
||||
"${1}"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
{
|
||||
name: "info",
|
||||
trigger: "info",
|
||||
path: [],
|
||||
content: [
|
||||
"info:",
|
||||
" version: ${1:0.0.0}",
|
||||
" title: ${2:title}",
|
||||
" description: ${3:description}",
|
||||
" termsOfService: ${4:terms}",
|
||||
" contact:",
|
||||
" name: ${5}",
|
||||
" url: ${6}",
|
||||
" email: ${7}",
|
||||
" license:",
|
||||
" name: ${8:MIT}",
|
||||
" url: ${9:http://opensource.org/licenses/MIT}",
|
||||
"${10}"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
{
|
||||
name: "get",
|
||||
trigger: "get",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("get")
|
||||
},
|
||||
|
||||
{
|
||||
name: "post",
|
||||
trigger: "post",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("post")
|
||||
},
|
||||
|
||||
{
|
||||
name: "put",
|
||||
trigger: "put",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("put")
|
||||
},
|
||||
|
||||
{
|
||||
name: "delete",
|
||||
trigger: "delete",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("delete")
|
||||
},
|
||||
|
||||
{
|
||||
name: "patch",
|
||||
trigger: "patch",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("patch")
|
||||
},
|
||||
|
||||
{
|
||||
name: "options",
|
||||
trigger: "options",
|
||||
path: ["paths", "."],
|
||||
content: makeOperationSnippet("options")
|
||||
},
|
||||
|
||||
// operation level parameter
|
||||
{
|
||||
name: "parameter",
|
||||
trigger: "param",
|
||||
path: ["paths", ".", ".", "parameters"],
|
||||
content: [
|
||||
"- name: ${1:parameter_name}",
|
||||
" in: ${2:query}",
|
||||
" description: ${3:description}",
|
||||
" type: ${4:string}",
|
||||
"${5}"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
// path level parameter
|
||||
{
|
||||
name: "parameter",
|
||||
trigger: "param",
|
||||
path: ["paths", ".", "parameters"],
|
||||
content: [
|
||||
"- name: ${1:parameter_name}",
|
||||
" in: ${2:path}",
|
||||
" required: true",
|
||||
" description: ${3:description}",
|
||||
" type: ${4:string}",
|
||||
"${5}"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
{
|
||||
name: "response",
|
||||
trigger: "resp",
|
||||
path: ["paths", ".", ".", "responses"],
|
||||
content: [
|
||||
"${1:code}:",
|
||||
" description: ${2}",
|
||||
" schema: ${3}",
|
||||
"${4}"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
{
|
||||
name: "200",
|
||||
trigger: "200",
|
||||
path: ["paths", ".", operationRegex, "responses"],
|
||||
content: makeResponseCodeSnippet("200")
|
||||
},
|
||||
|
||||
{
|
||||
name: "300",
|
||||
trigger: "300",
|
||||
path: ["paths", ".", operationRegex, "responses"],
|
||||
content: makeResponseCodeSnippet("300")
|
||||
},
|
||||
|
||||
{
|
||||
name: "400",
|
||||
trigger: "400",
|
||||
path: ["paths", ".", operationRegex, "responses"],
|
||||
content: makeResponseCodeSnippet("400")
|
||||
},
|
||||
|
||||
{
|
||||
name: "500",
|
||||
trigger: "500",
|
||||
path: ["paths", ".", operationRegex, "responses"],
|
||||
content: makeResponseCodeSnippet("500")
|
||||
},
|
||||
|
||||
{
|
||||
name: "model",
|
||||
trigger: "mod|def",
|
||||
regex: "mod|def",
|
||||
path: ["definitions"],
|
||||
content: [
|
||||
"${1:ModelName}:",
|
||||
" type: object",
|
||||
" properties:",
|
||||
" ${2}"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
import getCompletions from "./get-completions"
|
||||
|
||||
// Add an autosuggest completer
|
||||
export const addAutosuggestionCompleters = (ori, system) => (context) => {
|
||||
return ori(context).concat([{
|
||||
getCompletions(...args) {
|
||||
// Add `context`, then `system` as the last args
|
||||
return getCompletions(...args, context, system)
|
||||
}
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Enable Ace editor autocompletions
|
||||
export const enableAutocompletions = ({editor}) => () => {
|
||||
editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
})
|
||||
}
|
||||
|
||||
// Add completers. Just override this method. And concat on your completer(s)
|
||||
// see: https://github.com/ajaxorg/ace/blob/master/lib/ace/autocomplete.js
|
||||
// eg: return ori(...args).concat({ getCompletions() {...}})
|
||||
export const addAutosuggestionCompleters = () => () => {
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export function getPathForPosition({ pos: originalPos, prefix, editorValue, AST }) {
|
||||
var pos = Object.assign({}, originalPos)
|
||||
var lines = editorValue.split(/\r\n|\r|\n/)
|
||||
var previousLine = lines[pos.row - 1] || ""
|
||||
var currentLine = lines[pos.row]
|
||||
var nextLine = lines[pos.row + 1] || ""
|
||||
var prepared = false
|
||||
|
||||
// we're always at the document root when there's no indentation,
|
||||
// so let's save some effort
|
||||
if (pos.column === 1) {
|
||||
return []
|
||||
}
|
||||
|
||||
let prevLineIndent = getIndent(previousLine).length
|
||||
let currLineIndent = getIndent(currentLine).length
|
||||
|
||||
const isCurrentLineEmpty = currentLine.replace(prefix, "").trim() === ""
|
||||
|
||||
if(
|
||||
(previousLine.trim()[0] === "-" || nextLine.trim()[0] === "-")
|
||||
&& currLineIndent >= prevLineIndent
|
||||
&& isCurrentLineEmpty
|
||||
) {
|
||||
// for arrays with existing items under it, on blank lines
|
||||
// example:
|
||||
// myArray:
|
||||
// - a: 1
|
||||
// | <-- user cursor
|
||||
currentLine += "- a: b" // fake array item
|
||||
// pos.column += 1
|
||||
prepared = true
|
||||
}
|
||||
|
||||
// if current position is in at a free line with whitespace insert a fake
|
||||
// key value pair so the generated AST in ASTManager has current position in
|
||||
// editing node
|
||||
if ( !prepared && isCurrentLineEmpty) {
|
||||
currentLine += "a: b" // fake key value pair
|
||||
pos.column += 1
|
||||
prepared = true
|
||||
}
|
||||
|
||||
if(currentLine[currentLine.length - 1] === ":") {
|
||||
// Add a space if a user doesn't put one after a colon
|
||||
// NOTE: this doesn't respect the "prepared" flag.
|
||||
currentLine += " "
|
||||
pos.column += 1
|
||||
}
|
||||
|
||||
//if prefix is empty then add fake, empty value
|
||||
if( !prepared && !prefix){
|
||||
// for scalar values with no values
|
||||
// i.e. "asdf: "
|
||||
currentLine += "~"
|
||||
}
|
||||
|
||||
// append inserted character in currentLine for better AST results
|
||||
lines[originalPos.row] = currentLine
|
||||
editorValue = lines.join("\n")
|
||||
|
||||
let path = AST.pathForPosition(editorValue, {
|
||||
line: pos.row,
|
||||
column: pos.column
|
||||
})
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
function getIndent(str) {
|
||||
let match = str.match(/^ +/)
|
||||
return match ? match[0] : ""
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export function wrapCompleters(completers, cutoff = 100) {
|
||||
let isLiveCompletionDisabled = false
|
||||
let lastSpeeds = []
|
||||
let isPerformant = () => lastSpeeds.every(speed => speed < cutoff)
|
||||
|
||||
if(cutoff === 0 || cutoff === "0") {
|
||||
// never disable live autocomplete
|
||||
return completers
|
||||
}
|
||||
|
||||
return completers.map((completer, i) => {
|
||||
let ori = completer.getCompletions
|
||||
completer.getCompletions = function(editor, session, pos, prefix, callback) {
|
||||
let startTime = Date.now()
|
||||
try {
|
||||
ori(editor, session, pos, prefix, (...args) => {
|
||||
let msElapsed = Date.now() - startTime
|
||||
lastSpeeds[i] = msElapsed
|
||||
|
||||
if(isLiveCompletionDisabled && isPerformant()) {
|
||||
console.warn("Manual autocomplete was performant - re-enabling live autocomplete")
|
||||
editor.setOptions({
|
||||
enableLiveAutocompletion: true
|
||||
})
|
||||
isLiveCompletionDisabled = false
|
||||
}
|
||||
|
||||
if(msElapsed > cutoff && editor.getOption("enableLiveAutocompletion")) {
|
||||
console.warn("Live autocomplete is slow - disabling it")
|
||||
editor.setOptions({
|
||||
enableLiveAutocompletion: false
|
||||
})
|
||||
isLiveCompletionDisabled = true
|
||||
}
|
||||
|
||||
callback(...args)
|
||||
})
|
||||
} catch(e) {
|
||||
console.error("Autocompleter encountered an error")
|
||||
console.error(e)
|
||||
callback(null, [])
|
||||
}
|
||||
}
|
||||
return completer
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as actions from "./actions"
|
||||
import * as fn from "./fn"
|
||||
import * as specSelectors from "./spec-selectors"
|
||||
import { wrapCompleters } from "./helpers"
|
||||
|
||||
export default function EditorAutosuggestPlugin() {
|
||||
return {
|
||||
fn,
|
||||
statePlugins: {
|
||||
spec: {
|
||||
selectors: specSelectors,
|
||||
},
|
||||
editor: {
|
||||
actions,
|
||||
wrapActions: {
|
||||
onLoad: (ori, sys) => (context) => {
|
||||
const { editor } = context
|
||||
|
||||
// Any other calls for editor#onLoad
|
||||
ori(context)
|
||||
|
||||
// Enable autosuggestions ( aka: autocompletions )
|
||||
sys.editorActions.enableAutocompletions(context)
|
||||
|
||||
// Add completers ( for autosuggestions )
|
||||
const completers = sys.editorActions.addAutosuggestionCompleters(context)
|
||||
const cutoff = sys.getConfigs().liveAutocompleteCutoff
|
||||
const wrappedCompleters = wrapCompleters(completers || [], cutoff)
|
||||
editor.completers = wrappedCompleters
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createSelector } from "reselect"
|
||||
import { Set, Map } from "immutable"
|
||||
import { escapeJsonPointerToken } from "../refs-util"
|
||||
|
||||
const SWAGGER2_REF_MAP = {
|
||||
"paths": "pathitems",
|
||||
"definitions": "definitions",
|
||||
"schema": "definitions",
|
||||
"parameters": "parameters",
|
||||
"responses": "responses"
|
||||
}
|
||||
|
||||
const OAS3_REF_MAP = {
|
||||
schemas: "components/schemas", // for Schemas within Components
|
||||
schema: "components/schemas", // for Schemas throughout document
|
||||
parameters: "components/parameters",
|
||||
requestBody: "components/requestBodies",
|
||||
callbacks: "components/callbacks",
|
||||
examples: "components/examples",
|
||||
responses: "components/responses",
|
||||
headers: "components/headers",
|
||||
links: "components/links"
|
||||
}
|
||||
|
||||
const SWAGGER2_TYPES = Set(Object.values(SWAGGER2_REF_MAP))
|
||||
const OAS3_TYPES = Set(Object.values(OAS3_REF_MAP))
|
||||
|
||||
// Return a normalized "type" for a given path [a,b,c]
|
||||
// eg: /definitions/bob => definition
|
||||
// /paths/~1pets/responses/200/schema => definition ( because of schema )
|
||||
export const getRefType = (state, path) => (sys) => createSelector(
|
||||
() => {
|
||||
for( var i=path.length-1; i>-1; i-- ) {
|
||||
let tag = path[i]
|
||||
if(sys.specSelectors.isOAS3 && sys.specSelectors.isOAS3()) {
|
||||
if(OAS3_REF_MAP[tag]) {
|
||||
return OAS3_REF_MAP[tag]
|
||||
}
|
||||
} else if( SWAGGER2_REF_MAP[tag] ) {
|
||||
return SWAGGER2_REF_MAP[tag]
|
||||
}
|
||||
}
|
||||
return null
|
||||
})(state)
|
||||
|
||||
export const localRefs = (state) => (sys) => createSelector(
|
||||
sys.specSelectors.spec,
|
||||
sys.specSelectors.isOAS3 || (() => false),
|
||||
(spec, isOAS3) => {
|
||||
return (isOAS3 ? OAS3_TYPES : SWAGGER2_TYPES).toList().flatMap( type => {
|
||||
return spec
|
||||
.getIn(type.split("/"), Map({}))
|
||||
.keySeq()
|
||||
.map( name => Map({
|
||||
name,
|
||||
type,
|
||||
$ref: `#/${type}/${escapeJsonPointerToken(name)}`,
|
||||
}))
|
||||
})
|
||||
}
|
||||
)(state)
|
||||
@@ -0,0 +1,15 @@
|
||||
export default function(system) {
|
||||
return {
|
||||
rootInjects: {
|
||||
getEditorMetadata() {
|
||||
const allErrors = system.errSelectors.allErrors()
|
||||
return {
|
||||
contentString: system.specSelectors.specStr(),
|
||||
contentObject: system.specSelectors.specJson().toJS(),
|
||||
isValid: allErrors.size === 0,
|
||||
errors: allErrors.toJS()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export const JUMP_TO_LINE = "jump_to_line"
|
||||
|
||||
export function jumpToLine(line) {
|
||||
return {
|
||||
type: JUMP_TO_LINE,
|
||||
payload: line
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This is a hook. Will have editor instance
|
||||
// It needs to be an async-function, to avoid dispatching an object to the reducer
|
||||
export const onLoad = () => () => {}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* global ace */
|
||||
ace.define("ace/snippets/yaml",
|
||||
["require","exports","module"], function(e,t,n){ // eslint-disable-line no-unused-vars
|
||||
t.snippetText=undefined
|
||||
t.scope="yaml"
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class EditorContainer extends React.Component {
|
||||
|
||||
// This is already debounced by editor.jsx
|
||||
onChange = (value) => {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
let { specSelectors, getComponent, errSelectors, fn, editorSelectors, configsSelectors } = this.props
|
||||
|
||||
let Editor = getComponent("Editor")
|
||||
|
||||
let wrapperClasses = ["editor-wrapper"]
|
||||
const readOnly = !!configsSelectors.get("readOnly")
|
||||
|
||||
if(readOnly) {
|
||||
wrapperClasses.push("read-only")
|
||||
}
|
||||
|
||||
let propsForEditor = this.props
|
||||
|
||||
const editorOptions = {
|
||||
enableLiveAutocompletion: configsSelectors.get("editorLiveAutocomplete"),
|
||||
readOnly: readOnly,
|
||||
highlightActiveLine: !readOnly,
|
||||
highlightGutterLine: !readOnly,
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='editor-wrapper' className={wrapperClasses.join(" ")}>
|
||||
{ readOnly ? <h2 className="editor-readonly-watermark">Read Only</h2> : null }
|
||||
<Editor
|
||||
{...propsForEditor}
|
||||
value={specSelectors.specStr()}
|
||||
origin={specSelectors.specOrigin()}
|
||||
editorOptions={editorOptions}
|
||||
specObject={specSelectors.specJson().toJS()}
|
||||
errors={errSelectors.allErrors()}
|
||||
onChange={this.onChange}
|
||||
goToLine={editorSelectors.gotoLine()}
|
||||
AST={fn.AST}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditorContainer.defaultProps = {
|
||||
onChange: Function.prototype
|
||||
}
|
||||
|
||||
EditorContainer.propTypes = {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
configsSelectors: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
fn: PropTypes.object,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
errSelectors: PropTypes.object.isRequired,
|
||||
editorSelectors: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import AceEditor from "react-ace"
|
||||
import editorPluginsHook from "../editor-plugins/hook"
|
||||
import { placeMarkerDecorations } from "../editor-helpers/marker-placer"
|
||||
import Im, { fromJS } from "immutable"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
import win from "src/window"
|
||||
|
||||
import isUndefined from "lodash/isUndefined"
|
||||
import omit from "lodash/omit"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import debounce from "lodash/debounce"
|
||||
|
||||
import ace from "brace"
|
||||
import "brace/mode/yaml"
|
||||
import "brace/theme/tomorrow_night_eighties"
|
||||
import "brace/ext/language_tools"
|
||||
import "brace/ext/searchbox"
|
||||
import "./brace-snippets-yaml"
|
||||
|
||||
const NOOP = Function.prototype // Apparently the best way to no-op
|
||||
|
||||
export default function makeEditor({ editorPluginsToRun }) {
|
||||
|
||||
class Editor extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
this.editor = null
|
||||
|
||||
this.debouncedOnChange = props.debounce > 0
|
||||
? debounce(props.onChange, props.debounce)
|
||||
: props.onChange
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
specId: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
editorOptions: PropTypes.object,
|
||||
origin: PropTypes.string,
|
||||
debounce: PropTypes.number,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
onMarkerLineUpdate: PropTypes.func,
|
||||
|
||||
markers: PropTypes.object,
|
||||
goToLine: PropTypes.object,
|
||||
specObject: PropTypes.object.isRequired,
|
||||
|
||||
editorActions: PropTypes.object,
|
||||
|
||||
AST: PropTypes.object.isRequired,
|
||||
|
||||
errors: ImPropTypes.list,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
value: "",
|
||||
specId: "--unknown--",
|
||||
origin: "not-editor",
|
||||
onChange: NOOP,
|
||||
onMarkerLineUpdate: NOOP,
|
||||
markers: {},
|
||||
goToLine: {},
|
||||
errors: fromJS([]),
|
||||
editorActions: {onLoad(){}},
|
||||
editorOptions: {},
|
||||
debounce: 800 // 0.5 imperial seconds™
|
||||
|
||||
}
|
||||
|
||||
checkForSilentOnChange = (value) => {
|
||||
if(!this.silent) {
|
||||
this.debouncedOnChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
onLoad = (editor) => {
|
||||
|
||||
const { props } = this
|
||||
const { AST, specObject } = props
|
||||
|
||||
const langTools = ace.acequire("ace/ext/language_tools")
|
||||
const session = editor.getSession()
|
||||
|
||||
this.editor = editor
|
||||
|
||||
// fixes a warning, see https://github.com/ajaxorg/ace/issues/2499
|
||||
editor.$blockScrolling = Infinity
|
||||
|
||||
|
||||
session.setUseWrapMode(true)
|
||||
session.on("changeScrollLeft", xPos => { // eslint-disable-line no-unused-vars
|
||||
session.setScrollLeft(0)
|
||||
})
|
||||
|
||||
// TODO Remove this in favour of editorActions.onLoad
|
||||
editorPluginsHook(editor, props, editorPluginsToRun || [], {
|
||||
langTools, AST, specObject
|
||||
})
|
||||
|
||||
editor.setHighlightActiveLine(false)
|
||||
editor.setHighlightActiveLine(true)
|
||||
this.syncOptionsFromState(props.editorOptions)
|
||||
if(props.editorActions && props.editorActions.onLoad)
|
||||
props.editorActions.onLoad({...props, langTools, editor})
|
||||
|
||||
this.updateMarkerAnnotations(this.props)
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
const { editor } = this
|
||||
if(editor) {
|
||||
let session = editor.getSession()
|
||||
editor.resize()
|
||||
let wrapLimit = session.getWrapLimit()
|
||||
editor.setPrintMarginColumn(wrapLimit)
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
// onClick is deferred by 40ms, to give element resizes time to settle.
|
||||
setTimeout(() => {
|
||||
if(this.getWidth() !== this.width) {
|
||||
this.onResize()
|
||||
this.width = this.getWidth()
|
||||
}
|
||||
}, 40)
|
||||
}
|
||||
|
||||
getWidth = () => {
|
||||
let el = win.document.getElementById("editor-wrapper")
|
||||
return el ? el.getBoundingClientRect().width : null
|
||||
}
|
||||
|
||||
updateErrorAnnotations = (nextProps) => {
|
||||
if(this.editor && nextProps.errors) {
|
||||
let editorAnnotations = nextProps.errors.toJS().map(err => {
|
||||
// Create annotation objects that ACE can use
|
||||
return {
|
||||
row: err.line - 1,
|
||||
column: 0,
|
||||
type: err.level,
|
||||
text: err.message
|
||||
}
|
||||
})
|
||||
|
||||
this.editor.getSession().setAnnotations(editorAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
updateMarkerAnnotations = (props) => {
|
||||
const { editor } = this
|
||||
|
||||
const markers = Im.Map.isMap(props.markers) ? props.markers.toJS() : {}
|
||||
this._removeMarkers = placeMarkerDecorations({
|
||||
editor,
|
||||
markers,
|
||||
onMarkerLineUpdate: props.onMarkerLineUpdate,
|
||||
})
|
||||
}
|
||||
|
||||
removeMarkers = () => {
|
||||
if(this._removeMarkers) {
|
||||
this._removeMarkers()
|
||||
this._removeMarkers = null
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdateYaml = (props) => {
|
||||
// No editor instance
|
||||
if(!this.editor)
|
||||
return false
|
||||
|
||||
// Origin is editor
|
||||
if(props.origin === "editor")
|
||||
return false
|
||||
|
||||
// Redundant
|
||||
if(this.editor.getValue() === props.value)
|
||||
return false
|
||||
|
||||
// Value and origin are same, no update.
|
||||
if(this.props.value === props.value
|
||||
&& this.props.origin === props.origin)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
shouldUpdateMarkers = (props) => {
|
||||
const { markers } = props
|
||||
if(Im.Map.isMap(markers)) {
|
||||
return !Im.is(markers, this.props.markers) // Different from previous?
|
||||
}
|
||||
return true // Not going to do a deep compare of object-like markers
|
||||
}
|
||||
|
||||
updateYamlAndMarkers = (props) => {
|
||||
// If we update the yaml, we need to "lift" the yaml first
|
||||
if(this.shouldUpdateYaml(props)) {
|
||||
this.removeMarkers()
|
||||
this.updateYaml(props)
|
||||
this.updateMarkerAnnotations(props)
|
||||
|
||||
} else if (this.shouldUpdateMarkers(props)) {
|
||||
this.removeMarkers()
|
||||
this.updateMarkerAnnotations(props)
|
||||
}
|
||||
}
|
||||
|
||||
updateYaml = (props) => {
|
||||
if (props.origin === "insert") {
|
||||
// Don't clobber the undo stack in this case.
|
||||
this.editor.session.doc.setValue(props.value)
|
||||
this.editor.selection.clearSelection()
|
||||
} else {
|
||||
// session.setValue does not trigger onChange, nor add to undo stack.
|
||||
// Neither of which we want here.
|
||||
this.editor.session.setValue(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
syncOptionsFromState = (editorOptions={}) => {
|
||||
const { editor } = this
|
||||
if(!editor) {
|
||||
return
|
||||
}
|
||||
|
||||
const setOptions = omit(editorOptions, ["readOnly"])
|
||||
editor.setOptions(setOptions)
|
||||
|
||||
|
||||
const readOnly = isUndefined(editorOptions.readOnly)
|
||||
? false
|
||||
: editorOptions.readOnly // If its undefined, default to false.
|
||||
editor.setReadOnly(readOnly)
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// add user agent info to document
|
||||
// allows our custom Editor styling for IE10 to take effect
|
||||
var doc = win.document.documentElement
|
||||
doc.setAttribute("data-useragent", win.navigator.userAgent)
|
||||
this.syncOptionsFromState(this.props.editorOptions)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// eslint-disable-next-line react/no-did-mount-set-state
|
||||
|
||||
this.width = this.getWidth()
|
||||
win.document.addEventListener("click", this.onClick)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
win.document.removeEventListener("click", this.onClick)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let hasChanged = (k) => !isEqual(nextProps[k], this.props[k])
|
||||
const editor = this.editor
|
||||
|
||||
// Change the debounce value/func
|
||||
if(this.props.debounce !== nextProps.debounce) {
|
||||
if(this.debouncedOnChange.flush)
|
||||
this.debouncedOnChange.flush()
|
||||
|
||||
this.debouncedOnChange = nextProps.debounce > 0
|
||||
? debounce(nextProps.onChange, nextProps.debounce)
|
||||
: nextProps.onChange
|
||||
}
|
||||
|
||||
this.updateYamlAndMarkers(nextProps)
|
||||
this.updateErrorAnnotations(nextProps)
|
||||
|
||||
if(hasChanged("editorOptions")) {
|
||||
this.syncOptionsFromState(nextProps.editorOptions)
|
||||
}
|
||||
|
||||
if(editor && nextProps.goToLine && nextProps.goToLine.line && hasChanged("goToLine")) {
|
||||
editor.gotoLine(nextProps.goToLine.line)
|
||||
nextProps.editorActions.jumpToLine(null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false // Never update, see: componentWillRecieveProps and this.updateYaml for where we update things.
|
||||
}
|
||||
|
||||
render() {
|
||||
// NOTE: we're manually managing the value lifecycle, outside of react render
|
||||
// This will only render once.
|
||||
return (
|
||||
<AceEditor
|
||||
mode="yaml"
|
||||
theme="tomorrow_night_eighties"
|
||||
value={this.props.value /* This will only load once, thereafter it'll be via updateYaml */}
|
||||
onLoad={this.onLoad}
|
||||
onChange={this.checkForSilentOnChange}
|
||||
name="ace-editor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
tabSize={2}
|
||||
fontSize={14}
|
||||
useSoftTabs="true"
|
||||
wrapEnabled={true}
|
||||
editorProps={{
|
||||
"display_indent_guides": true,
|
||||
folding: "markbeginandend"
|
||||
}}
|
||||
setOptions={{
|
||||
cursorStyle: "smooth",
|
||||
wrapBehavioursEnabled: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Editor
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// This code is registered as a helper, not a plugin, because its lifecycle is
|
||||
// unique to the needs of the marker placement logic.
|
||||
|
||||
import countBy from "lodash/countBy"
|
||||
import map from "lodash/map"
|
||||
|
||||
let removers = []
|
||||
|
||||
function setRemovers(arr) {
|
||||
removers.forEach(fn => fn()) // remove existing anchors & gutters
|
||||
removers = arr // use parent scope to persist reference
|
||||
}
|
||||
|
||||
export function placeMarkerDecorations({editor, markers, onMarkerLineUpdate}) {
|
||||
|
||||
if(typeof editor !== "object") {
|
||||
return
|
||||
}
|
||||
|
||||
let markerLines = countBy(Object.values(markers), "position")
|
||||
|
||||
let removeFns = map(markerLines, (count, line) => {
|
||||
let className = `editor-marker-${count > 8 ? "9-plus" : count}`
|
||||
let s = editor.getSession()
|
||||
let anchor = s.getDocument().createAnchor(+line, 0)
|
||||
|
||||
anchor.setPosition(+line, 0) // noClip = true
|
||||
s.addGutterDecoration(+line, className)
|
||||
anchor.on("change", function (e) {
|
||||
var oldLine = e.old.row
|
||||
var newLine = e.value.row
|
||||
|
||||
s.removeGutterDecoration(oldLine, className)
|
||||
s.addGutterDecoration(newLine, className)
|
||||
onMarkerLineUpdate([oldLine, newLine, line])
|
||||
})
|
||||
|
||||
return function () {
|
||||
// // Remove the anchor & decoration
|
||||
let currentLine = +anchor.getPosition().row
|
||||
editor.getSession().removeGutterDecoration(currentLine, className)
|
||||
anchor.detach()
|
||||
}
|
||||
})
|
||||
|
||||
setRemovers(removeFns)
|
||||
|
||||
// To manually remove them
|
||||
return () => setRemovers([])
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import isFunction from "lodash/isFunction"
|
||||
|
||||
export default function(editor, { onGutterClick }) {
|
||||
editor.on("guttermousedown", (e) => {
|
||||
let editor = e.editor
|
||||
let line = e.getDocumentPosition().row
|
||||
let region = editor.renderer.$gutterLayer.getRegion(e)
|
||||
|
||||
e.stop()
|
||||
|
||||
if(isFunction(onGutterClick)) {
|
||||
onGutterClick({ region, line })
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// TODO: Turn these into actions, that we can override
|
||||
import GutterClick from "./gutter-click"
|
||||
import JsonToYaml from "./json-to-yaml"
|
||||
import TabHandler from "./tab-handler"
|
||||
|
||||
const plugins = [
|
||||
{fn: GutterClick, name: "gutterClick"},
|
||||
{fn: JsonToYaml, name: "jsonToYaml"},
|
||||
{fn: TabHandler, name: "tabHandler"},
|
||||
]
|
||||
|
||||
export default function (editor, props = {}, editorPluginsToRun = [], helpers = {}) {
|
||||
plugins
|
||||
.filter(plugin => ~editorPluginsToRun.indexOf(plugin.name))
|
||||
.forEach( plugin => {
|
||||
try {
|
||||
plugin.fn(editor, props, helpers)
|
||||
} catch(e) {
|
||||
console.error(`${plugin.name || ""} plugin error:`, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import YAML from "js-yaml"
|
||||
|
||||
export default function(editor) {
|
||||
editor.on("paste", e => {
|
||||
const originalStr = e.text
|
||||
if (!isJSON(originalStr)) {
|
||||
return
|
||||
}
|
||||
|
||||
let yamlString
|
||||
try {
|
||||
yamlString = YAML.safeDump(YAML.safeLoad(originalStr), {
|
||||
lineWidth: -1 // don't generate line folds
|
||||
})
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm("Would you like to convert your JSON into YAML?")) {
|
||||
return
|
||||
}
|
||||
|
||||
// using SelectionRange instead of CursorPosition, because:
|
||||
// SR.start|end === CP when there's no selection
|
||||
// and it catches indentation edge cases when there is one
|
||||
const padding = makePadding(editor.getSelectionRange().start.column)
|
||||
|
||||
// update the pasted content
|
||||
e.text = yamlString
|
||||
.split("\n")
|
||||
.map((line, i) => i == 0 ? line : padding + line) // don't pad first line, it's already indented
|
||||
.join("\n")
|
||||
.replace(/\t/g, " ") // tabs -> spaces, just to be sure
|
||||
})
|
||||
}
|
||||
|
||||
function isJSON (str){
|
||||
// basic test: "does this look like JSON?"
|
||||
let regex = /^[ \r\n\t]*[{\[]/
|
||||
|
||||
return regex.test(str)
|
||||
|
||||
}
|
||||
|
||||
function makePadding(len) {
|
||||
let str = ""
|
||||
|
||||
while(str.length < len) {
|
||||
str += " "
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function(editor) {
|
||||
// NOTE: react-ace has an onPaste prop.. we could refactor to that.
|
||||
editor.on("paste", e => {
|
||||
// replace all U+0009 tabs in pasted string with two spaces
|
||||
e.text = e.text.replace(/\t/g, " ")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import makeEditor from "./components/editor"
|
||||
import EditorContainer from "./components/editor-container"
|
||||
import * as actions from "./actions"
|
||||
import reducers from "./reducers"
|
||||
import * as selectors from "./selectors"
|
||||
import EditorSpecPlugin from "./spec"
|
||||
|
||||
let Editor = makeEditor({
|
||||
editorPluginsToRun: ["gutterClick", "jsonToYaml", "pasteHandler"]
|
||||
})
|
||||
|
||||
export default function () {
|
||||
return [EditorSpecPlugin, {
|
||||
components: { Editor, EditorContainer },
|
||||
statePlugins: {
|
||||
editor: {
|
||||
reducers,
|
||||
actions,
|
||||
selectors
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
JUMP_TO_LINE
|
||||
} from "./actions"
|
||||
|
||||
export default {
|
||||
[JUMP_TO_LINE]: (state, { payload } ) =>{
|
||||
return state.set("gotoLine", { line: payload })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createSelector } from "reselect"
|
||||
import Im from "immutable"
|
||||
|
||||
const state = state => {
|
||||
return state || Im.Map()
|
||||
}
|
||||
|
||||
export const gotoLine = createSelector(
|
||||
state,
|
||||
state => {
|
||||
return state.get("gotoLine") || null
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
const SPEC_UPDATE_ORIGIN = "spec_update_spec_origin"
|
||||
|
||||
// wraps updateSpec to include the "origin" parameter, defaulting to "not-editor"
|
||||
// Includes a selector to get the origin, specSelectors.specOrigin
|
||||
export default function EditorSpecPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
wrapActions: {
|
||||
updateSpec: (ori, system) => (specStr, origin) => {
|
||||
system.specActions.updateSpecOrigin(origin)
|
||||
ori(specStr)
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
[SPEC_UPDATE_ORIGIN]: (state, action) => {
|
||||
return state.set("specOrigin", action.payload)
|
||||
}
|
||||
},
|
||||
selectors: {
|
||||
specOrigin: (state) => state.get("specOrigin") || "not-editor"
|
||||
},
|
||||
actions: {
|
||||
updateSpecOrigin(origin="not-editor") {
|
||||
return {
|
||||
payload: origin+"",
|
||||
type: SPEC_UPDATE_ORIGIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import JumpIcon from "./jump-icon.svg"
|
||||
|
||||
export class JumpToPath extends React.Component {
|
||||
static propTypes = {
|
||||
editorActions: PropTypes.object.isRequired,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
fn: PropTypes.object.isRequired,
|
||||
path: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.string
|
||||
]),
|
||||
content: PropTypes.element,
|
||||
showButton: PropTypes.bool,
|
||||
specPath: PropTypes.array, // The location within the spec. Used as a fallback if `path` doesn't exist
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
path: "",
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
let { shallowEqualKeys } = nextProps.fn
|
||||
return shallowEqualKeys(this.props, nextProps, [
|
||||
"content", "showButton", "path", "specPath"
|
||||
])
|
||||
}
|
||||
|
||||
jumpToPath = (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {
|
||||
specPath=[],
|
||||
path,
|
||||
specSelectors,
|
||||
editorActions
|
||||
} = this.props
|
||||
|
||||
const jumpPath = specSelectors.bestJumpPath({path, specPath})
|
||||
editorActions.jumpToLine(specSelectors.getSpecLineFromPath(jumpPath))
|
||||
}
|
||||
|
||||
|
||||
defaultJumpContent = <img src={JumpIcon} onClick={this.jumpToPath} className="view-line-link" title={"Jump to definition"} />
|
||||
|
||||
render() {
|
||||
let { content, showButton } = this.props
|
||||
|
||||
if (content) {
|
||||
// if we were given content to render, wrap it
|
||||
return (
|
||||
<span onClick={ this.jumpToPath }>
|
||||
{ showButton ? this.defaultJumpContent : null }
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
// just render a link
|
||||
return this.defaultJumpContent
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import spec from "./spec"
|
||||
import * as components from "./components"
|
||||
|
||||
export default function JumpToPathPlugin() {
|
||||
return [
|
||||
spec,
|
||||
{
|
||||
components,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1,77 @@
|
||||
import { unescapeJsonPointerToken } from "../refs-util"
|
||||
|
||||
export default function spec() {
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
selectors: {
|
||||
|
||||
getSpecLineFromPath: (state, path) => ({fn: { AST }, specSelectors: { specStr }}) => {
|
||||
return AST.getLineNumberForPath(specStr(), path.toJS ? path.toJS() : path)
|
||||
},
|
||||
|
||||
// This will search return `path if it exists, else it'll look for the best $ref jump point
|
||||
// There is one caveat, it'll not search _down_ for deeply nested $refs. In those cases, it'll bring you to the shallower $ref.
|
||||
bestJumpPath: (state, {path, specPath}) => (system) => {
|
||||
const {
|
||||
specSelectors: { specJson },
|
||||
fn: { transformPathToArray }
|
||||
} = system
|
||||
|
||||
// We"ve been given an explicit path? Use that...
|
||||
if(path) {
|
||||
return typeof path === "string" ? transformPathToArray(path, specJson().toJS()) : path
|
||||
}
|
||||
|
||||
// Try each path in the resolved spec, starting from the deepest
|
||||
for(let i = specPath.length; i >= 0; i--) {
|
||||
const tryPath = specPath.slice(0,i)
|
||||
|
||||
// A $ref exists in the source? ( ie: pre-resolver)
|
||||
const $ref = specJson().getIn([...tryPath, "$ref"])
|
||||
// We have a $ref in the source?
|
||||
if($ref) {
|
||||
if(!/^#\//.test($ref)) {
|
||||
return [...tryPath, "$ref"]
|
||||
} else { // Is local $ref
|
||||
// Get rid of the trailing '#'
|
||||
const pointer = $ref.charAt(0) === "#" ? $ref.substr(1) : $ref
|
||||
return jsonPointerToArray(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
// This path exists in the source spec?
|
||||
if(specJson().hasIn(tryPath)) {
|
||||
return tryPath
|
||||
}
|
||||
}
|
||||
|
||||
// ...else just specPath, which is hopefully close enough
|
||||
return specPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied out of swagger-client, not sure if it should be exposed as a lib or as part of the public swagger-client api.
|
||||
/**
|
||||
* Converts a JSON pointer to array.
|
||||
* @api public
|
||||
*/
|
||||
function jsonPointerToArray(pointer) {
|
||||
if (typeof pointer !== "string") {
|
||||
throw new TypeError(`Expected a string, got a ${typeof pointer}`)
|
||||
}
|
||||
|
||||
if (pointer[0] === "/") {
|
||||
pointer = pointer.substr(1)
|
||||
}
|
||||
|
||||
if (pointer === "") {
|
||||
return []
|
||||
}
|
||||
|
||||
return pointer.split("/").map(unescapeJsonPointerToken)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import PetstoreYaml from "./petstore"
|
||||
const CONTENT_KEY = "swagger-editor-content"
|
||||
|
||||
let localStorage = window.localStorage
|
||||
|
||||
export const updateSpec = (ori) => (...args) => {
|
||||
let [spec] = args
|
||||
ori(...args)
|
||||
saveContentToStorage(spec)
|
||||
}
|
||||
|
||||
export default function(system) {
|
||||
// setTimeout runs on the next tick
|
||||
setTimeout(() => {
|
||||
if(localStorage.getItem(CONTENT_KEY)) {
|
||||
system.specActions.updateSpec(localStorage.getItem(CONTENT_KEY), "local-storage")
|
||||
} else if(localStorage.getItem("ngStorage-SwaggerEditorCache")) {
|
||||
// Legacy migration for swagger-editor 2.x
|
||||
try {
|
||||
let obj = JSON.parse(localStorage.getItem("ngStorage-SwaggerEditorCache"))
|
||||
let yaml = obj.yaml
|
||||
system.specActions.updateSpec(yaml)
|
||||
saveContentToStorage(yaml)
|
||||
localStorage.setItem("ngStorage-SwaggerEditorCache", null)
|
||||
} catch(e) {
|
||||
system.specActions.updateSpec(PetstoreYaml)
|
||||
}
|
||||
} else {
|
||||
system.specActions.updateSpec(PetstoreYaml)
|
||||
}
|
||||
}, 0)
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
wrapActions: {
|
||||
updateSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveContentToStorage(str) {
|
||||
return localStorage.setItem(CONTENT_KEY, str)
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
// Exports Petstore.yaml
|
||||
|
||||
export default `swagger: "2.0"
|
||||
info:
|
||||
description: "This is a sample server Petstore server. You can find out more about\
|
||||
\ Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).\
|
||||
\ For this sample, you can use the api key \`special-key\` to test the authorization\
|
||||
\ filters."
|
||||
version: "1.0.0"
|
||||
title: "Swagger Petstore"
|
||||
termsOfService: "http://swagger.io/terms/"
|
||||
contact:
|
||||
email: "apiteam@swagger.io"
|
||||
license:
|
||||
name: "Apache 2.0"
|
||||
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
host: "petstore.swagger.io"
|
||||
basePath: "/v2"
|
||||
tags:
|
||||
- name: "pet"
|
||||
description: "Everything about your Pets"
|
||||
externalDocs:
|
||||
description: "Find out more"
|
||||
url: "http://swagger.io"
|
||||
- name: "store"
|
||||
description: "Access to Petstore orders"
|
||||
- name: "user"
|
||||
description: "Operations about user"
|
||||
externalDocs:
|
||||
description: "Find out more about our store"
|
||||
url: "http://swagger.io"
|
||||
schemes:
|
||||
- "https"
|
||||
- "http"
|
||||
paths:
|
||||
/pet:
|
||||
post:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Add a new pet to the store"
|
||||
description: ""
|
||||
operationId: "addPet"
|
||||
consumes:
|
||||
- "application/json"
|
||||
- "application/xml"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Pet object that needs to be added to the store"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Pet"
|
||||
responses:
|
||||
"405":
|
||||
description: "Invalid input"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
put:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Update an existing pet"
|
||||
description: ""
|
||||
operationId: "updatePet"
|
||||
consumes:
|
||||
- "application/json"
|
||||
- "application/xml"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Pet object that needs to be added to the store"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Pet"
|
||||
responses:
|
||||
"400":
|
||||
description: "Invalid ID supplied"
|
||||
"404":
|
||||
description: "Pet not found"
|
||||
"405":
|
||||
description: "Validation exception"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
/pet/findByStatus:
|
||||
get:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Finds Pets by status"
|
||||
description: "Multiple status values can be provided with comma separated strings"
|
||||
operationId: "findPetsByStatus"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "status"
|
||||
in: "query"
|
||||
description: "Status values that need to be considered for filter"
|
||||
required: true
|
||||
type: "array"
|
||||
items:
|
||||
type: "string"
|
||||
enum:
|
||||
- "available"
|
||||
- "pending"
|
||||
- "sold"
|
||||
default: "available"
|
||||
collectionFormat: "multi"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Pet"
|
||||
"400":
|
||||
description: "Invalid status value"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
/pet/findByTags:
|
||||
get:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Finds Pets by tags"
|
||||
description: "Muliple tags can be provided with comma separated strings. Use\
|
||||
\ tag1, tag2, tag3 for testing."
|
||||
operationId: "findPetsByTags"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "tags"
|
||||
in: "query"
|
||||
description: "Tags to filter by"
|
||||
required: true
|
||||
type: "array"
|
||||
items:
|
||||
type: "string"
|
||||
collectionFormat: "multi"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Pet"
|
||||
"400":
|
||||
description: "Invalid tag value"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
deprecated: true
|
||||
/pet/{petId}:
|
||||
get:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Find pet by ID"
|
||||
description: "Returns a single pet"
|
||||
operationId: "getPetById"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "petId"
|
||||
in: "path"
|
||||
description: "ID of pet to return"
|
||||
required: true
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
$ref: "#/definitions/Pet"
|
||||
"400":
|
||||
description: "Invalid ID supplied"
|
||||
"404":
|
||||
description: "Pet not found"
|
||||
security:
|
||||
- api_key: []
|
||||
post:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Updates a pet in the store with form data"
|
||||
description: ""
|
||||
operationId: "updatePetWithForm"
|
||||
consumes:
|
||||
- "application/x-www-form-urlencoded"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "petId"
|
||||
in: "path"
|
||||
description: "ID of pet that needs to be updated"
|
||||
required: true
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
- name: "name"
|
||||
in: "formData"
|
||||
description: "Updated name of the pet"
|
||||
required: false
|
||||
type: "string"
|
||||
- name: "status"
|
||||
in: "formData"
|
||||
description: "Updated status of the pet"
|
||||
required: false
|
||||
type: "string"
|
||||
responses:
|
||||
"405":
|
||||
description: "Invalid input"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
delete:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "Deletes a pet"
|
||||
description: ""
|
||||
operationId: "deletePet"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "api_key"
|
||||
in: "header"
|
||||
required: false
|
||||
type: "string"
|
||||
- name: "petId"
|
||||
in: "path"
|
||||
description: "Pet id to delete"
|
||||
required: true
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
responses:
|
||||
"400":
|
||||
description: "Invalid ID supplied"
|
||||
"404":
|
||||
description: "Pet not found"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
/pet/{petId}/uploadImage:
|
||||
post:
|
||||
tags:
|
||||
- "pet"
|
||||
summary: "uploads an image"
|
||||
description: ""
|
||||
operationId: "uploadFile"
|
||||
consumes:
|
||||
- "multipart/form-data"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "petId"
|
||||
in: "path"
|
||||
description: "ID of pet to update"
|
||||
required: true
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
- name: "additionalMetadata"
|
||||
in: "formData"
|
||||
description: "Additional data to pass to server"
|
||||
required: false
|
||||
type: "string"
|
||||
- name: "file"
|
||||
in: "formData"
|
||||
description: "file to upload"
|
||||
required: false
|
||||
type: "file"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
$ref: "#/definitions/ApiResponse"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
/store/inventory:
|
||||
get:
|
||||
tags:
|
||||
- "store"
|
||||
summary: "Returns pet inventories by status"
|
||||
description: "Returns a map of status codes to quantities"
|
||||
operationId: "getInventory"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters: []
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
type: "object"
|
||||
additionalProperties:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
security:
|
||||
- api_key: []
|
||||
/store/order:
|
||||
post:
|
||||
tags:
|
||||
- "store"
|
||||
summary: "Place an order for a pet"
|
||||
description: ""
|
||||
operationId: "placeOrder"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "order placed for purchasing the pet"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Order"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
$ref: "#/definitions/Order"
|
||||
"400":
|
||||
description: "Invalid Order"
|
||||
/store/order/{orderId}:
|
||||
get:
|
||||
tags:
|
||||
- "store"
|
||||
summary: "Find purchase order by ID"
|
||||
description: "For valid response try integer IDs with value >= 1 and <= 10.\
|
||||
\ Other values will generated exceptions"
|
||||
operationId: "getOrderById"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "orderId"
|
||||
in: "path"
|
||||
description: "ID of pet that needs to be fetched"
|
||||
required: true
|
||||
type: "integer"
|
||||
maximum: 10.0
|
||||
minimum: 1.0
|
||||
format: "int64"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
$ref: "#/definitions/Order"
|
||||
"400":
|
||||
description: "Invalid ID supplied"
|
||||
"404":
|
||||
description: "Order not found"
|
||||
delete:
|
||||
tags:
|
||||
- "store"
|
||||
summary: "Delete purchase order by ID"
|
||||
description: "For valid response try integer IDs with positive integer value.\
|
||||
\ Negative or non-integer values will generate API errors"
|
||||
operationId: "deleteOrder"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "orderId"
|
||||
in: "path"
|
||||
description: "ID of the order that needs to be deleted"
|
||||
required: true
|
||||
type: "integer"
|
||||
minimum: 1.0
|
||||
format: "int64"
|
||||
responses:
|
||||
"400":
|
||||
description: "Invalid ID supplied"
|
||||
"404":
|
||||
description: "Order not found"
|
||||
/user:
|
||||
post:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Create user"
|
||||
description: "This can only be done by the logged in user."
|
||||
operationId: "createUser"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Created user object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
responses:
|
||||
default:
|
||||
description: "successful operation"
|
||||
/user/createWithArray:
|
||||
post:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Creates list of users with given input array"
|
||||
description: ""
|
||||
operationId: "createUsersWithArrayInput"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "List of user object"
|
||||
required: true
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/User"
|
||||
responses:
|
||||
default:
|
||||
description: "successful operation"
|
||||
/user/createWithList:
|
||||
post:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Creates list of users with given input array"
|
||||
description: ""
|
||||
operationId: "createUsersWithListInput"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "List of user object"
|
||||
required: true
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/User"
|
||||
responses:
|
||||
default:
|
||||
description: "successful operation"
|
||||
/user/login:
|
||||
get:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Logs user into the system"
|
||||
description: ""
|
||||
operationId: "loginUser"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "username"
|
||||
in: "query"
|
||||
description: "The user name for login"
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "password"
|
||||
in: "query"
|
||||
description: "The password for login in clear text"
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
type: "string"
|
||||
headers:
|
||||
X-Rate-Limit:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
description: "calls per hour allowed by the user"
|
||||
X-Expires-After:
|
||||
type: "string"
|
||||
format: "date-time"
|
||||
description: "date in UTC when token expires"
|
||||
"400":
|
||||
description: "Invalid username/password supplied"
|
||||
/user/logout:
|
||||
get:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Logs out current logged in user session"
|
||||
description: ""
|
||||
operationId: "logoutUser"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters: []
|
||||
responses:
|
||||
default:
|
||||
description: "successful operation"
|
||||
/user/{username}:
|
||||
get:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Get user by user name"
|
||||
description: ""
|
||||
operationId: "getUserByName"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "username"
|
||||
in: "path"
|
||||
description: "The name that needs to be fetched. Use user1 for testing. "
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "successful operation"
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
"400":
|
||||
description: "Invalid username supplied"
|
||||
"404":
|
||||
description: "User not found"
|
||||
put:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Updated user"
|
||||
description: "This can only be done by the logged in user."
|
||||
operationId: "updateUser"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "username"
|
||||
in: "path"
|
||||
description: "name that need to be updated"
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Updated user object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
responses:
|
||||
"400":
|
||||
description: "Invalid user supplied"
|
||||
"404":
|
||||
description: "User not found"
|
||||
delete:
|
||||
tags:
|
||||
- "user"
|
||||
summary: "Delete user"
|
||||
description: "This can only be done by the logged in user."
|
||||
operationId: "deleteUser"
|
||||
produces:
|
||||
- "application/xml"
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "username"
|
||||
in: "path"
|
||||
description: "The name that needs to be deleted"
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
"400":
|
||||
description: "Invalid username supplied"
|
||||
"404":
|
||||
description: "User not found"
|
||||
securityDefinitions:
|
||||
petstore_auth:
|
||||
type: "oauth2"
|
||||
authorizationUrl: "http://petstore.swagger.io/oauth/dialog"
|
||||
flow: "implicit"
|
||||
scopes:
|
||||
write:pets: "modify pets in your account"
|
||||
read:pets: "read your pets"
|
||||
api_key:
|
||||
type: "apiKey"
|
||||
name: "api_key"
|
||||
in: "header"
|
||||
definitions:
|
||||
Order:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
petId:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
quantity:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
shipDate:
|
||||
type: "string"
|
||||
format: "date-time"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Order Status"
|
||||
enum:
|
||||
- "placed"
|
||||
- "approved"
|
||||
- "delivered"
|
||||
complete:
|
||||
type: "boolean"
|
||||
default: false
|
||||
xml:
|
||||
name: "Order"
|
||||
Category:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
name:
|
||||
type: "string"
|
||||
xml:
|
||||
name: "Category"
|
||||
User:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
username:
|
||||
type: "string"
|
||||
firstName:
|
||||
type: "string"
|
||||
lastName:
|
||||
type: "string"
|
||||
email:
|
||||
type: "string"
|
||||
password:
|
||||
type: "string"
|
||||
phone:
|
||||
type: "string"
|
||||
userStatus:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
description: "User Status"
|
||||
xml:
|
||||
name: "User"
|
||||
Tag:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
name:
|
||||
type: "string"
|
||||
xml:
|
||||
name: "Tag"
|
||||
Pet:
|
||||
type: "object"
|
||||
required:
|
||||
- "name"
|
||||
- "photoUrls"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
category:
|
||||
$ref: "#/definitions/Category"
|
||||
name:
|
||||
type: "string"
|
||||
example: "doggie"
|
||||
photoUrls:
|
||||
type: "array"
|
||||
xml:
|
||||
name: "photoUrl"
|
||||
wrapped: true
|
||||
items:
|
||||
type: "string"
|
||||
tags:
|
||||
type: "array"
|
||||
xml:
|
||||
name: "tag"
|
||||
wrapped: true
|
||||
items:
|
||||
$ref: "#/definitions/Tag"
|
||||
status:
|
||||
type: "string"
|
||||
description: "pet status in the store"
|
||||
enum:
|
||||
- "available"
|
||||
- "pending"
|
||||
- "sold"
|
||||
xml:
|
||||
name: "Pet"
|
||||
ApiResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
code:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
type:
|
||||
type: "string"
|
||||
message:
|
||||
type: "string"
|
||||
externalDocs:
|
||||
description: "Find out more about Swagger"
|
||||
url: "http://swagger.io"`
|
||||
@@ -0,0 +1,69 @@
|
||||
const getTimestamp = ((that) => {
|
||||
if(that.performance && that.performance.now) {
|
||||
return that.performance.now.bind(that.performance)
|
||||
}
|
||||
return Date.now.bind(Date)
|
||||
})(self || window)
|
||||
|
||||
export default function PerformancePlugin() {
|
||||
if(!(window || {}).LOG_PERF) {
|
||||
return {
|
||||
fn: {
|
||||
getTimestamp,
|
||||
Timer: TimerStub,
|
||||
timeCall: (name,fn) => fn(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fn: {
|
||||
getTimestamp,
|
||||
Timer,
|
||||
timeCall,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timeCall(name,fn) {
|
||||
fn = fn || name
|
||||
name = typeof name === "function" ? "that" : name
|
||||
const a = getTimestamp()
|
||||
const r = fn()
|
||||
const b = getTimestamp()
|
||||
console.log(name,"took", b - a, "ms") // eslint-disable-line no-console
|
||||
return r
|
||||
}
|
||||
|
||||
function TimerStub() {
|
||||
this.start = this.mark = this.print = Function.prototype
|
||||
}
|
||||
|
||||
function Timer(name, _getTimestamp=getTimestamp) {
|
||||
this._name = name
|
||||
this.getTimestamp = _getTimestamp
|
||||
this._markers = []
|
||||
this.start()
|
||||
}
|
||||
|
||||
Timer.prototype.start = function() {
|
||||
this._start = this.getTimestamp()
|
||||
}
|
||||
|
||||
Timer.prototype.mark = function(name) {
|
||||
this._markers = this._markers || []
|
||||
this._markers.push({
|
||||
time: this.getTimestamp(),
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
Timer.prototype.print = function(name) {
|
||||
this.mark(name)
|
||||
this._markers.forEach(m => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(this._name, m.name, m.time - this._start, "ms")
|
||||
})
|
||||
this._markers = []
|
||||
this.start()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import qs from "querystring-browser"
|
||||
|
||||
/**
|
||||
* Unescapes a JSON pointer.
|
||||
* @api public
|
||||
*/
|
||||
export function unescapeJsonPointerToken(token) {
|
||||
if (typeof token !== "string") {
|
||||
return token
|
||||
}
|
||||
return qs.unescape(token.replace(/~1/g, "/").replace(/~0/g, "~"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a JSON pointer.
|
||||
* @api public
|
||||
*/
|
||||
export function escapeJsonPointerToken(token) {
|
||||
return qs.escape(token.replace(/~/g, "~0").replace(/\//g, "~1"))
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import SplitPane from "react-split-pane"
|
||||
|
||||
const MODE_KEY = ["split-pane-mode"]
|
||||
const MODE_LEFT = "left"
|
||||
const MODE_RIGHT = "right"
|
||||
const MODE_BOTH = "both" // or anything other than left/right
|
||||
|
||||
export default class SplitPaneMode extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
threshold: PropTypes.number,
|
||||
|
||||
children: PropTypes.array,
|
||||
|
||||
layoutSelectors: PropTypes.object.isRequired,
|
||||
layoutActions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
threshold: 100, // in pixels
|
||||
children: [],
|
||||
};
|
||||
|
||||
initializeComponent = (c) => {
|
||||
this.splitPane = c
|
||||
}
|
||||
|
||||
onDragFinished = () => {
|
||||
let { threshold, layoutActions } = this.props
|
||||
let { position, draggedSize } = this.splitPane.state
|
||||
this.draggedSize = draggedSize
|
||||
|
||||
let nearLeftEdge = position <= threshold
|
||||
let nearRightEdge = draggedSize <= threshold
|
||||
|
||||
layoutActions
|
||||
.changeMode(MODE_KEY, (
|
||||
nearLeftEdge
|
||||
? MODE_RIGHT : nearRightEdge
|
||||
? MODE_LEFT : MODE_BOTH
|
||||
))
|
||||
}
|
||||
|
||||
sizeFromMode = (mode, defaultSize) => {
|
||||
if(mode === MODE_LEFT) {
|
||||
this.draggedSize = null
|
||||
return "0px"
|
||||
} else if (mode === MODE_RIGHT) {
|
||||
this.draggedSize = null
|
||||
return "100%"
|
||||
}
|
||||
// mode === "both"
|
||||
return this.draggedSize || defaultSize
|
||||
}
|
||||
|
||||
render() {
|
||||
let { children, layoutSelectors } = this.props
|
||||
|
||||
const mode = layoutSelectors.whatMode(MODE_KEY)
|
||||
const left = mode === MODE_RIGHT ? <noscript/> : children[0]
|
||||
const right = mode === MODE_LEFT ? <noscript/> : children[1]
|
||||
const size = this.sizeFromMode(mode, "50%")
|
||||
|
||||
return (
|
||||
<SplitPane
|
||||
disabledClass={""}
|
||||
ref={this.initializeComponent}
|
||||
split='vertical'
|
||||
defaultSize={"50%"}
|
||||
primary="second"
|
||||
minSize={0}
|
||||
size={size}
|
||||
onDragFinished={this.onDragFinished}
|
||||
allowResize={mode !== MODE_LEFT && mode !== MODE_RIGHT }
|
||||
resizerStyle={{"flex": "0 0 auto", "position": "relative", "background": "#000", "opacity": ".2", "width": "11px", "cursor": "col-resize"}}
|
||||
>
|
||||
{ left }
|
||||
{ right }
|
||||
</SplitPane>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import SplitPaneMode from "./components/split-pane-mode"
|
||||
export default function SplitPaneModePlugin() {
|
||||
return {
|
||||
// statePlugins: {
|
||||
// layout: {
|
||||
// actions,
|
||||
// selectors,
|
||||
// }
|
||||
// },
|
||||
|
||||
components: {
|
||||
SplitPaneMode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Base validate plugin that provides a placeholder `validateSpec` that fires
|
||||
// after `updateJsonSpec` is dispatched.
|
||||
|
||||
export const updateJsonSpec = (ori, {specActions}) => (...args) => {
|
||||
ori(...args)
|
||||
/*
|
||||
To allow us to remove this, we should prefer the practice of
|
||||
only _composing_ inside of wrappedActions. It keeps us free to
|
||||
remove pieces added by plugins. In this case, I want to toggle validation.
|
||||
But I can't look inside the updateJsonSpec action,
|
||||
I can only remove it ( which isn't desirable ). However I _can_ remove `validateSpec` action,
|
||||
making it a noop. That way, the only overhead I end up with is a bunch of noops inside wrappedActions.
|
||||
Which isn't bad.
|
||||
*/
|
||||
const [ spec ] = args
|
||||
specActions.validateSpec(spec)
|
||||
}
|
||||
|
||||
//eslint-disable-next-line no-unused-vars
|
||||
export const validateSpec = (jsSpec) => ({ specSelectors, errActions }) => {
|
||||
|
||||
}
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
actions: {
|
||||
validateSpec,
|
||||
},
|
||||
wrapActions: {
|
||||
updateJsonSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
## Semantic Validators
|
||||
Ie: anything not covered by the json-schema validation
|
||||
|
||||
All the following belongs to the `validate` namespace
|
||||
Eg: a plugin with an action ( to validate things ) and a selector, to get nodes and stuff.
|
||||
```js
|
||||
export function SomeAwesomePlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
validate: { // "validate" Namespace
|
||||
actions: {
|
||||
validateSomeNewValidateFunction() {}
|
||||
},
|
||||
selectors: {
|
||||
someNewSelector() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Make a new one
|
||||
```js
|
||||
|
||||
// Under the validate namespace
|
||||
export const validateOnlyFoos = () => (system) => {
|
||||
system.validateSelectors.allSchemas().then(schemas => {
|
||||
const errors = []
|
||||
schemas.forEach( schema => {
|
||||
if(schema.node.type === "array") { // `node` is the value at that point
|
||||
errors.push({
|
||||
level: "error",
|
||||
message: "We can do something with this, array.",
|
||||
path: schema.path // it'll figure out the line # from this
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Make a selector, to later validate
|
||||
We use a single traverser although its performance leaves a little to be desired.
|
||||
The idea is that you provide a name and a filter function and
|
||||
in turn your validators can then iterate over those "nodes" to validate them.
|
||||
|
||||
```js
|
||||
export const allParameters = () => (system) => {
|
||||
return system.fn.traverseOnce({ // Returns a promise
|
||||
name: "allParameterSchemas",
|
||||
fn: (node) => { // called for each node, you need to return the node if you want it in the collecction
|
||||
if(system.validateSelectors.isParameter(node)) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
import debounce from "lodash/debounce"
|
||||
|
||||
export const SOURCE = "semantic"
|
||||
|
||||
// the test system does not tolerate slowness!
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const CI = process.env.CI
|
||||
const DEBOUNCE_MS = (NODE_ENV === "test" || CI === "true") ? 0 : 30
|
||||
|
||||
// System for buffering/batching errors
|
||||
var errorCollector = []
|
||||
const debNewSpecErrBatch = debounce(() => {
|
||||
const system = errorCollector.system // Just a reference to the "latest" system
|
||||
try {
|
||||
errorCollector.forEach(obj => {
|
||||
obj.line = obj.line || system.fn.AST.getLineNumberForPath(system.specSelectors.specStr(), obj.path)
|
||||
obj.source = SOURCE
|
||||
})
|
||||
system.errActions.newSpecErrBatch(errorCollector)
|
||||
delete errorCollector.system
|
||||
errorCollector = [] // Clear stack
|
||||
} catch(e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
}, DEBOUNCE_MS)
|
||||
|
||||
const bufferedNewSpecErrBatch = (system, obj) => {
|
||||
errorCollector.push(obj)
|
||||
errorCollector.system = system
|
||||
debNewSpecErrBatch()
|
||||
}
|
||||
|
||||
export const all = () => system => {
|
||||
if (!system.validateSelectors.shouldValidate()) {
|
||||
return
|
||||
}
|
||||
|
||||
system.validateActions.beforeValidate()
|
||||
|
||||
const errCb = (obj) => bufferedNewSpecErrBatch(system, obj)
|
||||
|
||||
system.validateSelectors.validators().forEach(name => {
|
||||
const fn = system.validateActions[name]
|
||||
// nothing about oas3 or swagger2
|
||||
if(name.indexOf("validateAsync") === 0) {
|
||||
fn(errCb) // Function send messages on its own, it won't be cached ( due to the nature of async operations )
|
||||
} else {
|
||||
Promise.resolve(fn())
|
||||
.then(validationObjs => {
|
||||
if(validationObjs) {
|
||||
validationObjs.forEach(errCb)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const beforeValidate = () => (system) => {
|
||||
system.errActions.clear({
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const HARD_CYCLE_LIMIT = 200
|
||||
|
||||
export function getRootNode(node) {
|
||||
var i = 0
|
||||
while(node.notRoot && i < HARD_CYCLE_LIMIT) {
|
||||
node = node.parent
|
||||
i++
|
||||
}
|
||||
return node || {}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as selectors from "./selectors"
|
||||
import * as actions from "./actions"
|
||||
import traverse from "traverse"
|
||||
import {createSelector} from "reselect"
|
||||
import debounce from "lodash/debounce"
|
||||
import memoize from "lodash/memoize"
|
||||
|
||||
import * as formDataValidateActions from "./validators/form-data"
|
||||
import * as schemaValidateActions from "./validators/schema"
|
||||
import * as pathsValidateActions from "./validators/paths"
|
||||
import * as securityValidateActions from "./validators/security"
|
||||
import * as parametersValidateActions from "./validators/parameters"
|
||||
import * as operationsOAS3ValidateActions from "./validators/oas3/operations"
|
||||
import * as parametersOAS3ValidateActions from "./validators/oas3/parameters"
|
||||
import * as componentsOAS3ValidateActions from "./validators/oas3/components"
|
||||
import * as refsOAS3ValidateActions from "./validators/oas3/refs"
|
||||
import * as refs2and3ValidateActions from "./validators/2and3/refs"
|
||||
import * as parameters2and3ValidateActions from "./validators/2and3/parameters"
|
||||
import * as paths2and3ValidateActions from "./validators/2and3/paths"
|
||||
import * as schemas2and3ValidateActions from "./validators/2and3/schemas"
|
||||
import * as operations2and3ValidateActions from "./validators/2and3/operations"
|
||||
import * as security2and3ValidateActions from "./validators/2and3/security"
|
||||
import * as tags2and3ValidateActions from "./validators/2and3/tags"
|
||||
|
||||
export default function SemanticValidatorsPlugin({getSystem}) {
|
||||
|
||||
const debAll = debounce((system) => system.validateActions.all(), 300)
|
||||
const traverseOnce = makeTraverseOnce(getSystem)
|
||||
|
||||
return {
|
||||
fn: {
|
||||
traverse,
|
||||
traverseOnce,
|
||||
memoizedResolveSubtree: makeMemoizedResolveSubtree(getSystem())
|
||||
},
|
||||
statePlugins: {
|
||||
spec: {
|
||||
selectors: {
|
||||
jsonAsJS: createSelector(
|
||||
state => state.get("json"),
|
||||
(spec) => spec ? spec.toJS() : null
|
||||
)
|
||||
},
|
||||
wrapActions: {
|
||||
validateSpec: (ori, system) => (...args) => {
|
||||
// verify editor plugin already loaded and function is available (for tests)
|
||||
if (system.specSelectors.specOrigin) {
|
||||
const specOrigin = system.specSelectors.specOrigin()
|
||||
if (specOrigin === "editor") {
|
||||
ori(...args)
|
||||
debAll(system)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
validate: {
|
||||
selectors,
|
||||
actions: {
|
||||
...actions,
|
||||
...formDataValidateActions,
|
||||
...schemaValidateActions,
|
||||
...pathsValidateActions,
|
||||
...securityValidateActions,
|
||||
...parametersValidateActions,
|
||||
...operations2and3ValidateActions,
|
||||
...refs2and3ValidateActions,
|
||||
...operationsOAS3ValidateActions,
|
||||
...parametersOAS3ValidateActions,
|
||||
...componentsOAS3ValidateActions,
|
||||
...refsOAS3ValidateActions,
|
||||
...parameters2and3ValidateActions,
|
||||
...paths2and3ValidateActions,
|
||||
...schemas2and3ValidateActions,
|
||||
...security2and3ValidateActions,
|
||||
...tags2and3ValidateActions
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeTraverseOnce(getSystem) {
|
||||
let traversers = {}
|
||||
let results = {}
|
||||
let deferred = null
|
||||
|
||||
const debTraverse = debounce(() => {
|
||||
// Setup collections
|
||||
for(let name in traversers) {
|
||||
results[name] = []
|
||||
}
|
||||
|
||||
const system = getSystem()
|
||||
|
||||
const json = system.specSelectors.jsonAsJS()
|
||||
|
||||
getSystem().fn.traverse(json)
|
||||
.forEach(function() { // Remember: this cannot be a fat-arrow function, because we need to read "this"
|
||||
for(let name in traversers) {
|
||||
const fn = traversers[name]
|
||||
const fnRes = fn(this)
|
||||
if(fnRes) {
|
||||
results[name].push(fnRes)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
deferred.resolve(results)
|
||||
deferred = null
|
||||
|
||||
traversers = {}
|
||||
results = {}
|
||||
}, 20) // 20ms might be more than enough, since most of these are called immediately (within a tick)
|
||||
|
||||
const defer = () => {
|
||||
let d = {}
|
||||
d.promise = new Promise((resolve, reject) => {
|
||||
d.resolve = resolve
|
||||
d.reject = reject
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
return ({fn, name}) => {
|
||||
traversers[name] = fn
|
||||
deferred = deferred || defer()
|
||||
debTraverse()
|
||||
return deferred.promise.then( a => a[name] )
|
||||
}
|
||||
}
|
||||
|
||||
function makeMemoizedResolveSubtree(system) {
|
||||
const cacheKeymaker = (obj, path) => {
|
||||
return `${obj.toString()} ${path.join("<>")}`
|
||||
}
|
||||
return memoize(async (obj, path, opts) => {
|
||||
const res = await system.fn.resolveSubtree(obj.toJS(), path, opts)
|
||||
return res
|
||||
}, cacheKeymaker)
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import flatten from "lodash/flatten"
|
||||
|
||||
export const isVendorExt = (state,node) => node.path.some(a => a.indexOf("x-") === 0)
|
||||
export const isDefinition = (state,node) => node.path[0] == "definitions" && node.path.length == 2
|
||||
export const isTag = (state, node) => node.path[0] === "tags" && node.path.length === 2
|
||||
export const isRootParameter = (state, node) => node.path[0] === "parameters" && node.path.length === 2
|
||||
export const isPathItemParameter = (state, node) => node.path[2] === "parameters" && node.path.length === 4
|
||||
export const isRootParameters = (state, node) => node.path[0] === "parameters" && node.path.length === 1
|
||||
export const isPathItemParameters = (state, node) => node.path[2] === "parameters" && node.path.length === 3
|
||||
export const isOperationParameters = (state, node) => node.path[3] === "parameters" && node.path.length === 4
|
||||
export const isRootResponse = (state, node) => node.path[0] === "responses" && node.path.length === 2
|
||||
export const isRootHeader = (state, node) => node.path[0] === "headers" && node.path.length === 2
|
||||
export const isRef = (state, node) => node.key === "$ref" && typeof node.node === "string" // This selector can be fooled.
|
||||
export const isRefArtifact = (state, node) => node.key === "$$ref" && typeof node.node === "string"
|
||||
export const isOAS3RootRequestBody = (state, node) => node.path.length === 3 && node.path[1] === "requestBodies"
|
||||
export const isOAS3OperationRequestBody = (state, node) => node.path.length === 4 && node.path[3] === "requestBody"
|
||||
export const isOAS3OperationCallbackRequestBody = (state, node) => node.path.length === 8 && node.path[7] === "requestBody"
|
||||
export const isOAS3RootParameter = (state, node) => node.path[0] === "components" && node.path[1] === "parameters" && node.path.length === 3
|
||||
export const isOAS3RootResponse = (state, node) => node.path[0] === "components" && node.path[1] === "responses" && node.path.length === 3
|
||||
export const isOAS3RootSchema = (state, node) => node.path[0] === "components" && node.path[1] === "schemas" && node.path.length === 3
|
||||
|
||||
export const isSubSchema = (state, node) => (sys) => {
|
||||
const path = node.path
|
||||
if(path.length < 3) {
|
||||
return false
|
||||
}
|
||||
if(node.parent.key == "properties") {
|
||||
if(node.parent.parent && node.parent.parent.node && node.parent.parent.node.type === "object") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
} else if(node.key === "additionalProperties") {
|
||||
if(node.parent && node.parent.node && node.parent.node.type === "object") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
} else if(node.key == "items") {
|
||||
if(node.parent.node && node.parent.node.type === "array") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isParameter = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isRootParameter(node)
|
||||
|| sys.validateSelectors.isOAS3RootParameter(node)
|
||||
|| sys.validateSelectors.isPathItemParameter(node)
|
||||
|| (node.path[0] === "paths"
|
||||
&& node.path[3] === "parameters"
|
||||
&& node.path.length === 5)
|
||||
)
|
||||
}
|
||||
|
||||
export const isOAS3RequestBody = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isOAS3RootRequestBody(node)
|
||||
|| sys.validateSelectors.isOAS3OperationRequestBody(node)
|
||||
|| sys.validateSelectors.isOAS3OperationCallbackRequestBody(node)
|
||||
)
|
||||
}
|
||||
|
||||
export const isParameterSchema = (state, node) => (sys) => {
|
||||
if(sys.specSelectors.isOAS3 && sys.specSelectors.isOAS3()) {
|
||||
// OAS3
|
||||
return node.key === "schema" && sys.validateSelectors.isParameter(node.parent)
|
||||
}
|
||||
// parameter.x.in != body
|
||||
if(sys.validateSelectors.isParameter(node) && node.node.in !== "body") {
|
||||
return true
|
||||
}
|
||||
// parameter.x.in == body
|
||||
if(node.key === "schema" && node.parent && sys.validateSelectors.isParameter(node.parent) && node.parent.node.in === "body") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const isOAS3RequestBodySchema = (state, node) => () => {
|
||||
const [key,, gpKey, ggpKey] = node.path.slice().reverse()
|
||||
|
||||
return key === "schema"
|
||||
&& gpKey === "content"
|
||||
&& ggpKey === "requestBody"
|
||||
}
|
||||
|
||||
export const isOAS3ResponseSchema = (state, node) => () => {
|
||||
const [key,, gpKey,, gggpKey] = node.path.slice().reverse()
|
||||
|
||||
return key === "schema"
|
||||
&& gpKey === "content"
|
||||
&& gggpKey === "responses"
|
||||
}
|
||||
|
||||
export const isResponse = (state, node) => (sys) => {
|
||||
const isOperationResponse = (
|
||||
node.path[0] === "paths"
|
||||
&& node.path[3] === "responses"
|
||||
&& node.path.length === 5
|
||||
&& !sys.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
return (
|
||||
isOperationResponse
|
||||
|| sys.validateSelectors.isRootResponse(node)
|
||||
|| sys.validateSelectors.isOAS3RootResponse(node)
|
||||
)
|
||||
}
|
||||
|
||||
export const allResponses = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allResponses",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isResponse(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const isHeader = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isRootHeader(node)
|
||||
|| ( node.path[0] === "paths"
|
||||
&& node.path[3] === "responses"
|
||||
&& node.path[5] === "headers"
|
||||
&& node.path.length === 7)
|
||||
)
|
||||
}
|
||||
|
||||
export const isResponseSchema = (state, node) => (sys) => {
|
||||
// paths.<operation>.<method>.responses.XXX.schema
|
||||
// respones.<response>.schema
|
||||
if(node.key === "schema" && node.parent && sys.validateSelectors.isResponse(node.parent)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const allSchemas = () => (system) => {
|
||||
const { validateSelectors } = system
|
||||
|
||||
const selectors = [
|
||||
validateSelectors.allParameterSchemas(),
|
||||
validateSelectors.allResponseSchemas(),
|
||||
validateSelectors.allDefinitions(),
|
||||
validateSelectors.allHeaders(),
|
||||
validateSelectors.allSubSchemas(),
|
||||
validateSelectors.allOAS3OperationSchemas()
|
||||
]
|
||||
|
||||
return Promise.all(selectors)
|
||||
.then((schemasAr) => {
|
||||
return flatten(schemasAr)
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameters = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allParameters",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isParameter(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3RequestBodies = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3RequestBodies",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isOAS3RequestBody(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameterArrays = () => (system) => {
|
||||
return system.validateSelectors.allParameters()
|
||||
.then(parameters => {
|
||||
return parameters.map(node => node.parent)
|
||||
.filter((node, i, arr) => {
|
||||
return Array.isArray(node.node) && arr.indexOf(node) === i
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const allTags = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allTags",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isTag(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allSubSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSubSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isSubSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const all$refs = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "all$refs",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isRef(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const all$refArtifacts = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "all$refArtifacts",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isRefArtifact(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allDefinitions = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allDefinitions",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isDefinition(node)
|
||||
|| system.validateSelectors.isOAS3RootSchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameterSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allParameterSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isParameterSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3OperationSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3OperationSchemas",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isOAS3RequestBodySchema(node)
|
||||
|| system.validateSelectors.isOAS3ResponseSchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3RequestBodySchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3RequestBodySchemas",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isOAS3RequestBodySchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allHeaders = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allHeader",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isHeader(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allResponseSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allResponseSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isResponseSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOperations = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOperations",
|
||||
fn: (node) => {
|
||||
const isOperation = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path.length === 3
|
||||
&& !system.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
if(isOperation) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allPathItems = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allPathItems",
|
||||
fn: (node) => {
|
||||
const isPathItem = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path.length === 2
|
||||
&& !system.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
if(isPathItem) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allSecurityDefinitions = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSecurityDefinitions",
|
||||
fn: (node) => {
|
||||
const isSecurityDefinition = (
|
||||
node.path[0] == "securityDefinitions"
|
||||
&& node.path.length === 2
|
||||
)
|
||||
|
||||
const isOAS3SecurityScheme = (
|
||||
node.path[0] == "components"
|
||||
&& node.path[1] == "securitySchemes"
|
||||
&& node.path.length === 3
|
||||
)
|
||||
|
||||
if(isSecurityDefinition || isOAS3SecurityScheme) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allSecurityRequirements = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSecurityRequirements",
|
||||
fn: (node) => {
|
||||
const isGlobalSecurityRequirement = (
|
||||
node.path[0] == "security"
|
||||
&& node.path.length === 2
|
||||
)
|
||||
|
||||
const isOperationSecurityRequirement = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path[3] == "security"
|
||||
&& node.path.length === 5
|
||||
&& !system.validateSelectors.isVendorExt(node.parent) // ignore extension keys in path items
|
||||
&& !system.validateSelectors.isVendorExt(node.parent.parent.parent) // ignore extension keys in "paths"
|
||||
)
|
||||
|
||||
if(isGlobalSecurityRequirement || isOperationSecurityRequirement) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3Components = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3Components",
|
||||
fn: (node) => {
|
||||
const isComponent = (
|
||||
node.path[0] === "components"
|
||||
&& node.path.length === 3
|
||||
&& !system.validateSelectors.isVendorExt(node.parent)
|
||||
)
|
||||
|
||||
if(isComponent) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// List of validators to run...
|
||||
export const validators = () => (system) => {
|
||||
return Object.keys(system.validateActions)
|
||||
.filter(name => {
|
||||
// The action needs to start with the prefix "validate..."
|
||||
if(name.indexOf("validate") !== 0)
|
||||
return false
|
||||
|
||||
// This is for both types...
|
||||
if(name.startsWith("validate2And3"))
|
||||
return true
|
||||
|
||||
// Now for the exclusive validations...
|
||||
if(system.specSelectors.isOAS3())
|
||||
return name.startsWith("validateOAS3")
|
||||
|
||||
// Swagger2 only...
|
||||
return !name.startsWith("validateOAS3")
|
||||
|
||||
//TODO: This doesn't account for validateAsync with oas3 or swagger2...
|
||||
})
|
||||
}
|
||||
|
||||
// Should we validate at all?
|
||||
export const shouldValidate = () => (system) => {
|
||||
// don't run validation if spec is empty
|
||||
if(system.specSelectors.specStr().trim().length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't validate if ambiguous version...
|
||||
const { specSelectors: { isSwagger2=Function.prototype, isOAS3=Function.prototype } } = system
|
||||
|
||||
// Can't handle TWO versions!
|
||||
if(isSwagger2() && isOAS3())
|
||||
return false
|
||||
|
||||
// Can't handle no version!
|
||||
if(!isSwagger2() && !isOAS3())
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export const validate2And3OperationHasUniqueId = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOperations()
|
||||
.then(nodes => {
|
||||
const seen = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
const id = value.operationId
|
||||
|
||||
if (id) {
|
||||
if (seen.indexOf(id) > -1) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Operations must have unique operationIds.",
|
||||
path: [...node.path, "operationId"]
|
||||
})
|
||||
}
|
||||
seen.push(id)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export const validate2And3ParametersHaveUniqueNameAndInCombinations = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameters = node.node || []
|
||||
|
||||
const seen = []
|
||||
|
||||
parameters.forEach((param, i) => {
|
||||
const { name: paramName, in: paramIn } = param
|
||||
|
||||
if(!paramName || !paramIn) {
|
||||
// name or in is missing, so we can't match the param to anything else
|
||||
return
|
||||
}
|
||||
const key = `${paramName}::${paramIn}`
|
||||
if(seen.indexOf(key) > -1) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Sibling parameters must have unique name + in values",
|
||||
path: [
|
||||
...node.path,
|
||||
(param.__i || i).toString()
|
||||
]
|
||||
})
|
||||
}
|
||||
seen.push(key)
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3PathParameterIsDefinedInPath = () => (system) => {
|
||||
const refArray = []
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameter = node.node || {}
|
||||
const path = node.path
|
||||
const isFromPath = path[0] === "paths" ? true : false
|
||||
const pathString = path[1]
|
||||
const paramName = parameter.name
|
||||
const paramInPath = `{${paramName}}`
|
||||
const ref = parameter.$ref
|
||||
const pathStringIncludesParamInPath = pathString && !pathString.toUpperCase().includes("" + paramInPath.toUpperCase())
|
||||
if (parameter.in === "path") {
|
||||
if (isFromPath && pathStringIncludesParamInPath) {
|
||||
acc.push({
|
||||
message: `Path parameter "${paramName}" must have the corresponding ${paramInPath} segment in the "${pathString}" path`,
|
||||
path: [...node.path, "name"],
|
||||
level: "error"
|
||||
})
|
||||
} else {
|
||||
const paramReference = refArray.find(({ referenceParamName }) => referenceParamName === node.key)
|
||||
if (paramReference && paramReference.pathString && !paramReference.pathString.toUpperCase().includes("" + paramInPath.toUpperCase())) {
|
||||
acc.push({
|
||||
message: `Path parameter "${paramName}" must have the corresponding ${paramInPath} segment in the "${paramReference.pathString}" path`,
|
||||
path: [...paramReference.node.path, "name"],
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (ref !== undefined) {
|
||||
const refStrings = ref.split("/")
|
||||
refArray.push({referenceParamName:refStrings[refStrings.length-1], pathString:pathString, node: node})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
checkForDefinition,
|
||||
PATH_TEMPLATES_REGEX
|
||||
} from "../helpers"
|
||||
|
||||
export const validate2And3PathParameterKeysDontContainQuestionMarks = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.key.indexOf("?") > -1) {
|
||||
acc.push({
|
||||
message: `Query strings in paths are not allowed.`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3PathParameterDeclarationHasMatchingDefiniton = () => async system => {
|
||||
const nodes = await system.validateSelectors.allPathItems()
|
||||
|
||||
return nodes.reduce(async (prev, node) => {
|
||||
const acc = await prev
|
||||
const pathTemplates = (node.key.match(PATH_TEMPLATES_REGEX) || [])
|
||||
.map(str => str.replace("{", "").replace("}", ""))
|
||||
if(pathTemplates.length) {
|
||||
for (let paramName of pathTemplates) {
|
||||
if(paramName.length === 0) {
|
||||
// don't validate empty param names... they're invalid anyway
|
||||
continue
|
||||
}
|
||||
const resolverResult = await system.fn.memoizedResolveSubtree(system.specSelectors.specJson(), node.path)
|
||||
const res = checkForDefinition(paramName, resolverResult.spec)
|
||||
if(res.inOperation && res.missingFromOperations.length) {
|
||||
const missingStr = res.missingFromOperations
|
||||
.map(str => `"${str}"`)
|
||||
.join(", ")
|
||||
|
||||
acc.push({
|
||||
message: `Declared path parameter "${paramName}" needs to be defined within every operation in the path (missing in ${missingStr}), or moved to the path-level parameters object`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
} else if(res.caseMatch) {
|
||||
acc.push({
|
||||
message: `Parameter names are case-sensitive. The parameter named "${res.paramCase}" does not match the case used in the path "${node.key}".`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
} else if(!res.found) {
|
||||
acc.push({
|
||||
message: `Declared path parameter "${paramName}" needs to be defined as a path parameter at either the path or operation level`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, Promise.resolve([]))
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import get from "lodash/get"
|
||||
import { escapeJsonPointerToken } from "../../../refs-util"
|
||||
import qs from "querystring-browser"
|
||||
import { pathFromPtr } from "json-refs"
|
||||
|
||||
export const validate2And3RefHasNoSiblings = () => system => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((nodes) => {
|
||||
const immSpecJson = system.specSelectors.specJson()
|
||||
const specJson = immSpecJson.toJS ? immSpecJson.toJS() : {}
|
||||
|
||||
return nodes.reduce((acc, node) => {
|
||||
const unresolvedValue = get(specJson, node.parent.path) || {}
|
||||
const unresolvedKeys = Object.keys(unresolvedValue) || []
|
||||
const isPathItem = node.parent.key === "paths" && node.path.length === 2
|
||||
|
||||
unresolvedKeys.forEach(k => {
|
||||
if(!isPathItem && k !== "$ref" && unresolvedKeys.indexOf("$ref") > -1) {
|
||||
acc.push({
|
||||
message: `Sibling values alongside $refs are ignored.\nTo add properties to a $ref, wrap the $ref into allOf, or move the extra properties into the referenced definition (if applicable).`,
|
||||
path: [...node.path.slice(0, -1), k],
|
||||
level: "warning"
|
||||
})
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
// Add warnings for unused definitions
|
||||
export const validate2And3UnusedDefinitions = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((nodes) => {
|
||||
const references = nodes.map(node => node.node)
|
||||
const errors = []
|
||||
const basePath = system.specSelectors.isOAS3() ?
|
||||
["components", "schemas"] :
|
||||
["definitions"]
|
||||
|
||||
system.specSelectors.definitions()
|
||||
.forEach((val, key) => {
|
||||
const escapedKey = escapeJsonPointerToken(key)
|
||||
if(references.indexOf(`#/${basePath.join("/")}/${escapedKey}`) < 0) {
|
||||
const path = [...basePath, key]
|
||||
errors.push({
|
||||
level: "warning",
|
||||
path,
|
||||
message: "Definition was declared but never used in document"
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3RefPathFormatting = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refArtifacts) => {
|
||||
|
||||
const errors = []
|
||||
refArtifacts.forEach((node) => {
|
||||
const value = node.node
|
||||
if(typeof value === "string") {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [refUrl, refPath] = value.split("#")
|
||||
|
||||
if(refPath && refPath[0] !== "/") {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$ref paths must begin with `#/`",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3RefPointersExist = () => (system) => {
|
||||
const json = system.specSelectors.specJson()
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refs) => {
|
||||
const errors = []
|
||||
|
||||
refs.forEach((node) => {
|
||||
const value = node.node
|
||||
if(typeof value === "string" && value[0] === "#") {
|
||||
// if pointer starts with "#", it is a local ref
|
||||
const path = pathFromPtr(qs.unescape(value))
|
||||
|
||||
if(json.getIn(path) === undefined) {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$refs must reference a valid location in the document",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
// from RFC3986: https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
// plus "%", since it is needed for encoding.
|
||||
const RFC3986_UNRESERVED_CHARACTERS = /[A-Za-z0-9\-_\.~%]/g
|
||||
|
||||
export const validate2And3RefPointersAreProperlyEscaped = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refs) => {
|
||||
const errors = []
|
||||
|
||||
refs.forEach((node) => {
|
||||
const value = node.node
|
||||
const hashIndex = value.indexOf("#")
|
||||
const fragment = hashIndex > -1 ? value.slice(hashIndex + 1) : null
|
||||
if(typeof fragment === "string") {
|
||||
const rawPath = fragment.split("/")
|
||||
const hasReservedChars = rawPath
|
||||
.some(p => p.replace(RFC3986_UNRESERVED_CHARACTERS, "").length > 0)
|
||||
|
||||
if(hasReservedChars) {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$ref values must be RFC3986-compliant percent-encoded URIs",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
export const validate2And3TypeArrayRequiresItems = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { type, items } = schemaObj || {}
|
||||
if(type === "array" && typeof items === "undefined") {
|
||||
acc.push({
|
||||
message: "Schemas with 'type: array', require a sibling 'items: ' field",
|
||||
path: node.path,
|
||||
level: "error",
|
||||
})
|
||||
} else if(type === "array" && (typeof items !== "object" || Array.isArray(items))) {
|
||||
acc.push({
|
||||
message: "`items` must be an object",
|
||||
path: [...node.path, "items"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const validate2And3TypesInDefaultValuesMatchesWithEnum = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { type } = schemaObj || {}
|
||||
const isNullable = !!schemaObj.nullable
|
||||
const enumeration = schemaObj.enum
|
||||
if (enumeration !== null && typeof enumeration !== "undefined") {
|
||||
var enumIndex = 0
|
||||
enumeration.forEach((element, index) => {
|
||||
var isValidFormat = true
|
||||
if (element === null && isNullable) {
|
||||
return
|
||||
}
|
||||
if (type === "array" && (!Array.isArray(element) || element === null)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if ((type === "number" || type === "string" || type === "boolean") && !(typeof element === type)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if (type === "integer" && !Number.isInteger(element)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if (type === "object" && ((element === null) || !(typeof element === type) || Array.isArray(element))) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
}
|
||||
if (!isValidFormat) {
|
||||
acc.push({
|
||||
message: "enum value should conform to its schema's `type`",
|
||||
path: [...node.path, "enum", enumIndex],
|
||||
level: "warning",
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3SchemasDefaultsMatchAnEnum = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const element = node.node || {}
|
||||
|
||||
if(!element || element.enum === undefined || element.default === undefined) {
|
||||
// nothing to do
|
||||
return acc
|
||||
}
|
||||
|
||||
if(element.enum.indexOf(element.default) === -1) {
|
||||
acc.push({
|
||||
message: "Default values must be present in `enum`",
|
||||
path: [...node.path, "default"]
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3MinAndMax = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const {minimum, maximum, minLength, maxLength, minProperties, maxProperties, minItems, maxItems} = schemaObj
|
||||
if(typeof minimum === "number" && typeof maximum === "number" && (minimum > maximum)) {
|
||||
acc.push({
|
||||
message: "'minimum' must be lower value than 'maximum'",
|
||||
path: [...node.path, "minimum"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minLength === "number" && typeof maxLength === "number" && (minLength > maxLength)) {
|
||||
acc.push({
|
||||
message: "'minLength' must be lower value than 'maxLength'",
|
||||
path: [...node.path, "minLength"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minProperties === "number" && typeof maxProperties === "number" && (minProperties > maxProperties)) {
|
||||
acc.push({
|
||||
message: "'minProperties' must be lower value than 'maxProperties'",
|
||||
path: [...node.path, "minProperties"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minItems === "number" && typeof maxItems === "number" && (minItems > maxItems)) {
|
||||
acc.push({
|
||||
message: "'minItems' must be lower value than 'maxItems'",
|
||||
path: [...node.path, "minItems"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export const validate2And3SecurityRequirementsHaveDefinitions = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([requirementNodes, definitionNodes]) => {
|
||||
const definedSecuritySchemes = definitionNodes
|
||||
.map(node => node.key)
|
||||
|
||||
return requirementNodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const requiredSecurityDefinitions = Object.keys(value) || []
|
||||
|
||||
requiredSecurityDefinitions.forEach(name => {
|
||||
if(definedSecuritySchemes.indexOf(name) < 0) {
|
||||
acc.push({
|
||||
message: "Security requirements must match a security definition",
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3UnusedSecuritySchemes = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([securityRequirements, securitySchemes]) => {
|
||||
// Get just the names of security schemes used in `security`
|
||||
const usedSecurities = securityRequirements
|
||||
.map(node => Object.keys(node.node) || [])
|
||||
.reduce(function(a, b) {
|
||||
// flatten!
|
||||
return a.concat(b)
|
||||
}, [])
|
||||
|
||||
return securitySchemes.reduce((acc, node) => {
|
||||
if(usedSecurities.indexOf(node.key) < 0) {
|
||||
acc.push({
|
||||
message: "Security scheme was defined but never used. To apply security, use the `security` section in operations or on the root level of your API definition.",
|
||||
path: node.path,
|
||||
level: "warning",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const validate2And3TagObjectsHaveUniqueNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allTags()
|
||||
.then(nodes => {
|
||||
const seenNames = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const tagObj = node.node
|
||||
const { name } = tagObj || {}
|
||||
if(!name || seenNames.indexOf(name) > -1) {
|
||||
acc.push({
|
||||
message: "Tag Objects must have unique `name` field values.",
|
||||
path: node.path,
|
||||
level: "error",
|
||||
})
|
||||
} else {
|
||||
seenNames.push(name)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { SOURCE } from "../actions"
|
||||
import { getRootNode } from "src/plugins/validate-semantic/helpers"
|
||||
const operationKeys = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
||||
|
||||
export const validateParameterFormDataCaseTypo = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
if(
|
||||
value.in &&
|
||||
typeof value.in === "string" &&
|
||||
value.in.toLowerCase() === "formdata" &&
|
||||
value.in !== "formData"
|
||||
) {
|
||||
acc.push({
|
||||
message: `Parameter "in: ${value.in}" is invalid, did you mean "in: formData"?`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterFormDataForFileTypes = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
if(value.type === "file" && value.in !== "formData") {
|
||||
acc.push({
|
||||
message: `Parameters with "type: file" must have "in: formData"`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterFormDataConsumesType = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const pathItemValue = node.node
|
||||
const globalConsumes = getRootNode(node).node.consumes
|
||||
const pathItemParameters = pathItemValue.parameters
|
||||
|
||||
const hasPathItemFormDataParameter = pathItemParameters != null && pathItemParameters.find(parameter => parameter.in === "formData")
|
||||
const hasPathItemFileParameter = pathItemParameters != null && pathItemParameters.find(parameter => parameter.type === "file")
|
||||
|
||||
for (const method of operationKeys) {
|
||||
const operationValue = pathItemValue[method]
|
||||
|
||||
if (operationValue) {
|
||||
const effectiveConsumes = operationValue.consumes || globalConsumes || []
|
||||
const operationParameters = operationValue.parameters || []
|
||||
const hasOperationFormDataParameter = operationParameters.find(parameter => parameter.in === "formData")
|
||||
const hasOperationFileParameter = operationParameters.find(parameter => parameter.type === "file")
|
||||
|
||||
if(hasPathItemFileParameter || hasOperationFileParameter){
|
||||
if (!effectiveConsumes.includes("multipart/form-data")) {
|
||||
acc.push({
|
||||
message: `Operations with parameters of "type: file" must include "multipart/form-data" in their "consumes" property`,
|
||||
path: [...node.path, method],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
} else if (hasPathItemFormDataParameter || hasOperationFormDataParameter) {
|
||||
if (!effectiveConsumes.includes("application/x-www-form-urlencoded") && !effectiveConsumes.includes("multipart/form-data")) {
|
||||
acc.push({
|
||||
message: `Operations with parameters of "in: formData" must include "application/x-www-form-urlencoded" or "multipart/form-data" in their "consumes" property`,
|
||||
path: [...node.path, method],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterArraysDontContainBodyAndFormData = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(paramArrays => {
|
||||
return paramArrays.reduce((acc, node) => {
|
||||
const bodyParams = node.node.filter(param => param.in === "body")
|
||||
const formDataParams = node.node.filter(param => param.in === "formData")
|
||||
|
||||
if(bodyParams.length && formDataParams.length) {
|
||||
acc.push({
|
||||
message: `Parameters cannot have both a "in: body" and "in: formData", as "formData" _will_ be the body`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const operationKeys = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
||||
|
||||
export const PATH_TEMPLATES_REGEX = /\{(.*?)\}/g
|
||||
|
||||
export function checkForDefinition(paramName, pathItem) {
|
||||
const pathItemParameters = pathItem.parameters
|
||||
const operationsInPathItem = (Object.keys(pathItem) || [])
|
||||
.filter(key => operationKeys.indexOf(key) > -1)
|
||||
.map(key => {
|
||||
const obj = pathItem[key]
|
||||
obj.method = key
|
||||
return obj
|
||||
})
|
||||
|
||||
const res = {
|
||||
found: false,
|
||||
inPath: false,
|
||||
inOperation: false,
|
||||
caseMatch: false,
|
||||
paramCase: "",
|
||||
missingFromOperations: []
|
||||
}
|
||||
|
||||
// Look at the path parameters
|
||||
if(Array.isArray(pathItemParameters)) {
|
||||
pathItemParameters.forEach(param => {
|
||||
if(param.name === paramName && param.in === "path") {
|
||||
res.found = true
|
||||
res.inPath = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Next, look at the operations...
|
||||
if(!res.found && operationsInPathItem.length) {
|
||||
operationsInPathItem
|
||||
.forEach(op => {
|
||||
const inThisOperation = (op.parameters || [])
|
||||
.some(param => param.name === paramName && param.in === "path")
|
||||
|
||||
const caseMatch = (op.parameters || [])
|
||||
.find(param => param.name && !(param.name === paramName) && (param.name.toLowerCase() === paramName.toLowerCase()) && param.in === "path")
|
||||
|
||||
if(inThisOperation) {
|
||||
res.found = true
|
||||
res.inOperation = true
|
||||
}
|
||||
|
||||
if(caseMatch) {
|
||||
res.caseMatch = true
|
||||
res.paramCase = caseMatch.name
|
||||
}
|
||||
|
||||
if(!inThisOperation) {
|
||||
res.missingFromOperations.push(op.method)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const COMPONENT_NAME_REGEX = /^[A-Za-z0-9\-\._]+$/
|
||||
|
||||
export const validateOAS3ComponentNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allOAS3Components()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(!COMPONENT_NAME_REGEX.test(node.key)) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Component names can only contain the characters A-Z a-z 0-9 - . _",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const validateOAS3GetAndDeleteOpsHaveNoRequestBody = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOperations()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const key = (node.key || "").toLowerCase()
|
||||
const value = node.node
|
||||
|
||||
if((key === "get" || key === "delete") && value.requestBody !== undefined) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `${key.toUpperCase()} operations cannot have a requestBody.`,
|
||||
path: [...node.path, "requestBody"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export const validateOAS3HeaderParameterNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.node.in === "header") {
|
||||
const name = (node.node.name || "").toLowerCase()
|
||||
if(name === "authorization") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Authorization\" are ignored. Use the `securitySchemes` and `security` sections instead to define authorization.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
} else if(name === "content-type") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Content-Type\" are ignored. The values for the \"Content-Type\" header are defined by `requestBody.content.<media-type>`.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
} else if(name === "accept") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Accept\" are ignored. The values for the \"Accept\" header are defined by `responses.<code>.content.<media-type>`.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
export const validateOAS3RefsForRequestBodiesReferenceRequestBodyPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOAS3RequestBodies()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if (!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const [refPath = ""] = ref.split("#")
|
||||
const pathArr = refPath.split("/") || []
|
||||
|
||||
// Ignore external references
|
||||
if (ref.startsWith("#/")) {
|
||||
// Local cases
|
||||
if (refPath.endsWith("requestBody") && (refPath.startsWith("/paths") || refPath.startsWith("/components"))){
|
||||
return acc
|
||||
}
|
||||
|
||||
// Starting with #/compontents/schemas is not allowed
|
||||
if (ref.startsWith("#/components/schemas")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody $refs cannot point to '#/components/schemas/…', they must point to '#/components/requestBodies/…'`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
} else
|
||||
if (ref.startsWith("#/components") && !ref.startsWith("#/components/requestBodies/")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody $refs must point to a position where a requestBody can be legally placed`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
|
||||
// Extensions are valid
|
||||
if (ref.startsWith("#/") && pathArr.some(element => element.startsWith("x-"))){
|
||||
return acc
|
||||
}
|
||||
}
|
||||
return acc
|
||||
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3RequestBodyRefsReferenceAllowableSchemaPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOAS3RequestBodySchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const [, refPath = ""] = ref.split("#")
|
||||
const pathArr = refPath.split("/") || []
|
||||
const parentRefKey = pathArr.slice(-2)[0]
|
||||
const targetRefKey = pathArr.slice(-1)[0]
|
||||
if(
|
||||
targetRefKey !== "schema"
|
||||
&& parentRefKey !== "schemas"
|
||||
&& ref.startsWith("#/")
|
||||
) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody schema $refs must point to a position where a Schema Object can be legally placed`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3ParameterRefsReferenceParameterPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (ref.startsWith("#/components/headers")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `OAS3 parameter $refs should point to #/components/parameters/... and not #/components/headers/...`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3RefsForHeadersReferenceHeadersPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allHeaders()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (ref.startsWith("#/components/parameters")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `OAS3 header $refs should point to #/components/headers/... and not #/components/parameters/...`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export const validateParameterBadKeys = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.node.required !== true && node.node.in === "path") {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Path parameters must have 'required: true'. You can always create another path/operation without this parameter to get the same behaviour.",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParametersHasOnlyOneBody = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameters = node.node || []
|
||||
let bodyParamSeen = false
|
||||
|
||||
parameters.forEach((param) => {
|
||||
if(param.in === "body" && bodyParamSeen) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Multiple body parameters are not allowed.",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
if(param.in === "body") {
|
||||
bodyParamSeen = true
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
PATH_TEMPLATES_REGEX
|
||||
} from "./helpers"
|
||||
|
||||
export const validatePathParameterDeclarationIsNotEmpty = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const pathTemplates = (node.key.match(PATH_TEMPLATES_REGEX) || [])
|
||||
.map(str => str.replace("{", "").replace("}", ""))
|
||||
|
||||
const emptyPathTemplates = pathTemplates.filter(v => !v.length)
|
||||
|
||||
if(emptyPathTemplates.length) {
|
||||
acc.push({
|
||||
message: `Empty path parameter declarations are not valid`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validatePathParameterKeysAreDifferent = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
const seen = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const realPath = node.key.replace(PATH_TEMPLATES_REGEX, "~~")
|
||||
if(seen.indexOf(realPath) > -1) {
|
||||
acc.push({
|
||||
message: `Equivalent paths are not allowed.`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
seen.push(realPath)
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export const validateTypeKeyShouldBeString = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
|
||||
if(schemaObj.type !== undefined && typeof schemaObj.type !== "string") {
|
||||
acc.push({
|
||||
message: `Schema "type" key must be a string`,
|
||||
path: [...node.path, "type"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateReadOnlyPropertiesNotRequired = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
if(Array.isArray(schemaObj.required) && typeof schemaObj.properties === "object") {
|
||||
schemaObj.required.forEach((prop, i) => {
|
||||
if(schemaObj.properties[prop] && schemaObj.properties[prop].readOnly) {
|
||||
acc.push({
|
||||
message: `Read only properties cannot be marked as required by a schema.`,
|
||||
path: [...node.path, "required", i.toString()],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
// See https://github.com/swagger-api/swagger-editor/issues/1601
|
||||
export const validateSchemaPatternHasNoZAnchors = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { pattern } = schemaObj || {}
|
||||
if(typeof pattern === "string" && pattern.indexOf("\\Z") > -1) {
|
||||
acc.push({
|
||||
message: `"\\Z" anchors are not allowed in regular expression patterns`,
|
||||
path: [...node.path, "pattern"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export const validateSecurityRequirementReferenceExistingScopes = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([requirementNodes, definitionNodes]) => {
|
||||
const definedSecuritySchemes = definitionNodes
|
||||
.reduce((p, node) => Object.assign(p, { [node.key]: node.node }), {})
|
||||
|
||||
return requirementNodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const requiredSecurityDefinitions = Object.keys(value) || []
|
||||
|
||||
requiredSecurityDefinitions.forEach(name => {
|
||||
const scopes = value[name]
|
||||
const definition = definedSecuritySchemes[name]
|
||||
if(Array.isArray(scopes) && scopes.length && definition) {
|
||||
scopes.forEach((scope, i) => {
|
||||
if(!definition.scopes || definition.scopes[scope] === undefined) {
|
||||
acc.push({
|
||||
message: `Security scope definition ${scope} could not be resolved`,
|
||||
path: [...node.path, i.toString()],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user