@firebolt-js/openrpc
Version:
The Firebolt SDK Code & Doc Generator
280 lines (247 loc) • 9.97 kB
JavaScript
/*
* Copyright 2021 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
import groupBy from 'array.prototype.groupby'
import util from 'util'
import { getPayloadFromEvent } from '../../shared/modules.mjs'
import { getPropertiesInSchema, getPropertySchema } from '../../shared/json-schema.mjs'
const addPrettyPath = (error, json) => {
const path = []
const root = json.title || json.info.title
let pointer = json
error.instancePath.substr(1).split('/').forEach(x => {
if (x.match(/^[0-9]+$/)) {
path.push(pointer[parseInt(x)].name || pointer[parseInt(x)].title || x)
pointer = pointer[parseInt(x)]
}
else {
path.push(x)
pointer = pointer[x]
}
})
error.prettyPath = '/' + path.join('/')
error.document = root
error.node = pointer
return error
}
const addFailingMethodSchema = (error, json, schema) => {
if (error.instancePath.match(/\/methods\/[0-9]+/)) {
if (error.keyword == 'if') {
if (error.params && error.params.failingKeyword == 'then') {
const i = parseInt(error.schemaPath.split("/")[2])
error.params.failingSchema = schema.definitions.Method.allOf[i].then.$ref
}
}
}
}
// this method keeps errors that are deeper in the JSON structure, and hides "parent" errors with an overlapping path
export const pruneErrors = (errors = []) => {
const groups = groupBy(errors, ({ instancePath }) => instancePath )
const pruned = []
Object.values(groups).forEach( group => {
const paths = []
pruned.push(group.sort( (a, b) => b.schemaPath.split('/').length - a.schemaPath.split('/').length ).pop())
})
return pruned
//const pruned = []
const paths = []
if (errors) {
return errors.filter((value, index, array) => {
if ((paths.length === 0) || (!paths.find(path => path.startsWith(value.instancePath)))) {
pruned.push(value)
paths.push(value.instancePath)
return true
}
return false
})
}
else return errors
}
// this method outputs a much more readable error than the raw JSON
export const displayError = (error) => {
let errorLocation
let errorLocationType
let errorFileType
if (!error.instancePath) {
errorLocation = '/'
errorLocationType = `json`
errorFileType = `???`
}
else if (error.instancePath.startsWith('/components/schemas/')) {
errorLocation = error.instancePath.split('/').slice(3, 4).join('/')
errorLocationType = 'schema'
errorFileType = 'OpenRPC'
}
else if (error.instancePath.startsWith('/definitions/')) {
errorLocation = error.instancePath.split('/').slice(2, 3).join('/')
errorLocationType = 'schema'
errorFileType = 'JSON-Schema'
}
else if (error.instancePath.startsWith('/methods/')) {
errorLocation = error.prettyPath.split('/').slice(2, 3).join('/')
errorLocationType = 'method'
errorFileType = 'OpenRPC'
}
const pad = str => str + ' '.repeat(Math.max(0, 20 - str.length))
// hard to read this code, but these color escape codes make the errors glorious! :)
console.error(`Error in ${errorLocationType} '\x1b[32m${errorLocation}\x1b[0m'\n`)
console.error(`\t\x1b[2m${pad('path:')}\x1b[0m${error.instancePath}`)
console.error(`\t\x1b[2m${pad('message:')}\x1b[0m\x1b[38;5;2m${error.message}\x1b[0m`)
console.error(`\t\x1b[2m${pad('schema:')}\x1b[0m\x1b[38;5;2m${error.schemaPath}\x1b[0m`)
if (error.params) {
Object.keys(error.params).forEach(key => {
const param = util.inspect(error.params[key], { colors: true, breakLength: Infinity })
console.error(`\t\x1b[2m${pad(key+':')}\x1b[0m\x1b[38;5;2m${param}\x1b[0m`)
})
}
if (error.propertyName) {
console.error(`\t\x1b[2m${pad('property:')}\x1b[0m\x1b[38;5;208m${error.propertyName}\x1b[0m`)
}
console.error(`\t\x1b[2m${pad('document:')}\x1b[0m\x1b[38;5;208m${error.document}\x1b[0m \x1b[2m(${errorFileType})\x1b[2m\x1b[0m`)
console.error(`\t\x1b[2m${pad('source:')}\x1b[0m\x1b[38;5;208m${error.source}\x1b[0m`)
if (error.value) {
console.error(`\t\x1b[2m${pad('value:')}\x1b[0m\n`)
console.dir(error.value, {depth: null, colors: true})// + JSON.stringify(example, null, ' ') + '\n')
}
// This is useful for debugging... please leave comment here for quick access :)
// console.dir(error, {depth: 1000})
// console.dir(error.node, {depth: 100})
console.error()
}
export const validate = (json = {}, schemas = {}, ajv, validator, additionalPackages = []) => {
let valid = validator(json)
let root = json.title || json.info.title
const errors = []
if (valid) {
if (json.methods) {
additionalPackages.forEach((addtnlValidator) => {
const additionalValid = addtnlValidator(json)
if (!additionalValid) {
valid = false
addtnlValidator.errors.forEach(error => addPrettyPath(error, json))
addtnlValidator.errors.forEach(error => error.source = 'Firebolt OpenRPC')
addtnlValidator.errors.forEach(error => addFailingMethodSchema(error, json, addtnlValidator.schema))
errors.push(...pruneErrors(addtnlValidator.errors))
}
})
}
}
else {
validator.errors.forEach(error => addPrettyPath(error, json))
validator.errors.forEach(error => error.source = 'OpenRPC')
errors.push(...pruneErrors(validator.errors))
}
return { valid: valid, title: json.title || json.info.title, errors: errors }
}
const schemasMatch = (a, b) => {
if (a == null) {
return b == null
}
if (b == null) {
return a == null
}
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
const keysMatch = (aKeys.length == bKeys.length) && aKeys.every(key => bKeys.includes(key))
if (keysMatch) {
const typesMatch = aKeys.every(key => typeof a[key] === typeof b[key])
if (typesMatch) {
const valuesMatch = aKeys.every(key => typeof a[key] === 'object' || (a[key] === b[key]))
if (valuesMatch) {
const objectsMatch = aKeys.every(key => typeof a[key] !== 'object' || schemasMatch(a[key], b[key]))
if (objectsMatch) {
return true
}
}
}
}
return false
}
export const validatePasshtroughs = (json) => {
const providees = json.methods.filter(m => m.tags.find(t => t['x-provided-by']))
const result = {
valid: true,
title: 'Mapping of all x-provided-by methods',
errors: []
}
providees.forEach(method => {
const providerName = method.tags.find(t => t['x-provided-by'])['x-provided-by']
const provider = json.methods.find(m => m.name === providerName)
let destination, examples1
let source, examples2
let sourceName
if (!provider) {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not exist`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
return
}
else if (method.tags.find(t => t.name === 'event')) {
destination = getPayloadFromEvent(method)
examples1 = method.examples.map(e => e.result.value)
source = provider.params[provider.params.length-1].schema
sourceName = provider.params[provider.params.length-1].name
examples2 = provider.examples.map(e => e.params[e.params.length-1].value)
}
else {
destination = method.result.schema
examples1 = method.examples.map(e => e.result.value)
source = JSON.parse(JSON.stringify(provider.tags.find(t => t['x-response'])['x-response']))
sourceName = provider.tags.find(t => t['x-response'])['x-response-name']
examples2 = provider.tags.find(t => t['x-response'])['x-response'].examples
delete source.examples
}
if (!schemasMatch(source, destination)) {
const properties = getPropertiesInSchema(destination, json)
// follow $refs so we can see the schemas
source = getPropertySchema(source, '.', json)
destination = getPropertySchema(destination, '.', json)
if (properties && properties.length && sourceName) {
let candidate = getPropertySchema(getPropertySchema(destination, `properties.${sourceName}`, json), '.', json)
if (!candidate) {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not have a matching result schema or ${sourceName} property`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
} else if (!schemasMatch(candidate, source)) {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not have a matching result schema or ${sourceName} schema`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
}
}
else if (!sourceName) {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not have a matching result schema and has no x-response-name property to inject into`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
}
else {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not have a matching schema and has not candidate sub-schemas`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
}
}
})
if (result.errors.length) {
result.valid = false
result.errors.forEach(error => addPrettyPath(error, json))
}
return result
}