Skip to content

Commit

Permalink
fix: Added validation to ensure x-provided-by methods have matching s…
Browse files Browse the repository at this point in the history
…chemas per spec
  • Loading branch information
jlacivita committed May 23, 2024
1 parent 6d0ea39 commit 7dfdf7d
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 7 deletions.
3 changes: 2 additions & 1 deletion src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const knownOpts = {
'static-module': [String, Array],
'language': [path],
'examples': [path, Array],
'as-path': [Boolean]
'as-path': [Boolean],
'pass-throughs': [Boolean]
}

const shortHands = {
Expand Down
35 changes: 31 additions & 4 deletions src/shared/json-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,13 @@ const getPropertySchema = (json, dotPath, document) => {
for (var i=0; i<path.length; i++) {
const property = path[i]
const remainingPath = path.filter((x, j) => j >= i ).join('.')
if (node.type === 'object') {
if (node.$ref) {
node = getPropertySchema(getPath(node.$ref, document), remainingPath, document)
}
else if (property === '') {
return node
}
else if (node.type === 'object') {
if (node.properties && node.properties[property]) {
node = node.properties[property]
}
Expand All @@ -161,9 +167,6 @@ const getPropertySchema = (json, dotPath, document) => {
node = node.additionalProperties
}
}
else if (node.$ref) {
node = getPropertySchema(getPath(node.$ref, document), remainingPath, document)
}
else if (Array.isArray(node.allOf)) {
node = node.allOf.find(s => {
let schema
Expand All @@ -184,6 +187,29 @@ const getPropertySchema = (json, dotPath, document) => {
return node
}

const getPropertiesInSchema = (json, document) => {
let node = json

while (node.$ref) {
node = getPath(node.$ref, document)
}

if (node.type === 'object') {
const props = []
if (node.properties) {
props.push(...Object.keys(node.properties))
}

if (node.propertyNames) {
props.push(...node.propertyNames)
}

return props
}

return null
}

function getSchemaConstraints(schema, module, options = { delimiter: '\n' }) {
if (schema.schema) {
schema = schema.schema
Expand Down Expand Up @@ -484,6 +510,7 @@ export {
getLinkedSchemaPaths,
getPath,
getPropertySchema,
getPropertiesInSchema,
isDefinitionReferencedBySchema,
isNull,
isSchema,
Expand Down
10 changes: 8 additions & 2 deletions src/validate/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { readJson, readFiles, readDir } from "../shared/filesystem.mjs"
import { addExternalMarkdown, addExternalSchemas, fireboltize } from "../shared/modules.mjs"
import { removeIgnoredAdditionalItems, replaceUri } from "../shared/json-schema.mjs"
import { validate, displayError } from "./validator/index.mjs"
import { validate, displayError, validatePasshtroughs } from "./validator/index.mjs"
import { logHeader, logSuccess, logError } from "../shared/io.mjs"

import Ajv from 'ajv'
Expand All @@ -33,7 +33,8 @@ const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const run = async ({
input: input,
schemas: schemas,
transformations = false
transformations = false,
'pass-throughs': passThroughs
}) => {

logHeader(`Validating ${path.relative('.', input)} with${transformations ? '' : 'out'} Firebolt transformations.`)
Expand Down Expand Up @@ -286,6 +287,11 @@ const run = async ({
// console.dir(exampleSpec, { depth: 100 })
}
}

if (passThroughs) {
const passthroughResult = validatePasshtroughs(json)
printResult(passthroughResult, "Firebolt App pass-through")
}
}
catch (error) {
throw error
Expand Down
101 changes: 101 additions & 0 deletions src/validate/validator/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
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 = []
Expand Down Expand Up @@ -164,4 +166,103 @@ export const validate = (json = {}, schemas = {}, ajv, validator, additionalPack
}

return { valid: valid, title: json.title || json.info.title, errors: errors }
}

const schemasMatch = (a, b) => {
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

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
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']))
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) {
const destinationProperty = properties.find(property => {
let candidate = getPropertySchema(destination, `properties.${property}`, json)

candidate && (candidate = getPropertySchema(candidate, '.', json)) // follow $refs

if (schemasMatch(candidate, source)) {
return true
}
})

if (!destinationProperty) {
result.errors.push({
message: `The x-provided-by method '${providerName}' does not have a matching schema or sub-schema`,
instancePath: `/methods/${json.methods.indexOf(method)}`
})
result.title = `Mismatched x-provided-by schemas in ${result.errors.length} methods.`
}
}
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

}

0 comments on commit 7dfdf7d

Please sign in to comment.