From 1038afbc1b4ea90f95de2a056b34f108d9a4e388 Mon Sep 17 00:00:00 2001 From: tatomyr Date: Thu, 6 Jun 2024 18:49:47 +0300 Subject: [PATCH] resolveAndMerge $refs and $ands before translating x-type to jsonschema --- README.md | 2 - applications/__tests__/adapter.test.js | 22 +++--- applications/resources/openapi-never.yaml | 2 +- applications/x-types-adapter.js | 83 ++++++++++++----------- applications/x-types-decorators.js | 8 ++- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index aa5796b..04c3316 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,6 @@ Array literals allow defining multiple available options, one of which is applic ["string", "undefined"] ``` -The relation between the items is `XOR`. - ## Types Combining It is possible to combine several types into one using the `$and` keyword: diff --git a/applications/__tests__/adapter.test.js b/applications/__tests__/adapter.test.js index b110ca8..acdfb6e 100644 --- a/applications/__tests__/adapter.test.js +++ b/applications/__tests__/adapter.test.js @@ -1,5 +1,5 @@ import {describe, expect, test} from "vitest" -import {translateXTypeToSchema} from "../x-types-adapter" +import {resolveAndMerge, translateXTypeToSchema} from "../x-types-adapter" describe("adapter", () => { test("translates primitive strings", () => { @@ -18,11 +18,11 @@ describe("adapter", () => { }) test("translates a correct $and into an object", () => { - expect( - translateXTypeToSchema({ - $and: [{foo: "string"}, {bar: "number"}], - }) - ).toEqual({ + const merged = resolveAndMerge({ + $and: [{foo: "string"}, {bar: "number"}], + }) + expect(merged).toEqual({foo: "string", bar: "number"}) + expect(translateXTypeToSchema(merged)).toEqual({ type: "object", properties: {foo: {type: "string"}, bar: {type: "number"}}, additionalProperties: false, @@ -31,10 +31,10 @@ describe("adapter", () => { }) test("translates an incorrect $and into `never`", () => { - expect( - translateXTypeToSchema({ - $and: ["string", "number"], - }) - ).toEqual({not: {}}) + const merged = resolveAndMerge({ + $and: ["string", "number"], + }) + expect(merged).toEqual("undefined") + expect(translateXTypeToSchema(merged)).toEqual({not: {}}) }) }) diff --git a/applications/resources/openapi-never.yaml b/applications/resources/openapi-never.yaml index 642cedc..0866a6a 100644 --- a/applications/resources/openapi-never.yaml +++ b/applications/resources/openapi-never.yaml @@ -6,7 +6,7 @@ paths: /test: get: responses: - 200: + 400: description: Test content: application/json: diff --git a/applications/x-types-adapter.js b/applications/x-types-adapter.js index 9febd9a..16b5171 100644 --- a/applications/x-types-adapter.js +++ b/applications/x-types-adapter.js @@ -1,6 +1,45 @@ const {isObject, mergeAll} = require("./x-types-utils") -const translateXTypeToSchema = (xType, ctx) => { +const resolveAndMerge = (xType, ctx) => { + if (typeof xType.$ref !== "undefined") { + const resolved = ctx.resolve(xType).node + if (resolved === undefined) { + console.error() + console.error("ERROR! Cannot resolve $ref:") + console.error(xType.$ref) + console.error() + return "any" + } + return resolveAndMerge(resolved, ctx) + } + + if (typeof xType.$and !== "undefined") { + if (!Array.isArray(xType.$and)) { + console.error() + console.error("ERROR! Expected array but got:") + console.error(xType.$and) + console.error() + return "any" + } + return mergeAll(...resolveAndMerge(xType.$and, ctx)) + } + + if (Array.isArray(xType)) { + return xType.map(type => resolveAndMerge(type, ctx)) + } + + if (isObject(xType)) { + let obj = {} + for (const key in xType) { + obj[key] = resolveAndMerge(xType[key], ctx) + } + return obj + } + + return xType +} + +const translateXTypeToSchema = xType => { if (typeof xType === "undefined") { throw new Error('Expected "x-type" but got "undefined"') } @@ -46,38 +85,7 @@ const translateXTypeToSchema = (xType, ctx) => { } if (typeof xType.array !== "undefined") { - return {type: "array", items: translateXTypeToSchema(xType.array, ctx)} - } - - if (typeof xType.$and !== "undefined") { - if (!Array.isArray(xType.$and)) { - console.error() - console.error("ERROR! Expected array but got:") - console.error(xType.$and) - console.error() - return {} - } - return translateXTypeToSchema(mergeAll(...xType.$and), ctx) - /* - // TODO: consider also this: - return { - allOf: xType.$and.map((item) => { - const translated = translateXTypeToSchema(item); - if (isObject(translated) && translated.additionalProperties === undefined) { - translated.additionalProperties = true; - } - return translated; - }), - }; - */ - } - - if (typeof xType.$ref !== "undefined") { - if (ctx.resolve(xType).node === undefined) { - return translateXTypeToSchema("any") - } - - return translateXTypeToSchema(ctx.resolve(xType).node, ctx) + return {type: "array", items: translateXTypeToSchema(xType.array)} } if (typeof xType === "string" && xType.startsWith("$literal:")) { @@ -96,7 +104,7 @@ const translateXTypeToSchema = (xType, ctx) => { return { anyOf: xType .filter(type => type !== "undefined") - .map(type => translateXTypeToSchema(type, ctx)), + .map(type => translateXTypeToSchema(type)), } } @@ -105,16 +113,14 @@ const translateXTypeToSchema = (xType, ctx) => { let required = [] const {string, ...props} = xType const additionalProperties = - typeof string === "undefined" - ? false - : translateXTypeToSchema(string, ctx) + typeof string === "undefined" ? false : translateXTypeToSchema(string) for (const key in props) { const realKey = key.startsWith("$literal:") ? key.slice("$literal:".length) : key - properties[realKey] = translateXTypeToSchema(props[key], ctx) + properties[realKey] = translateXTypeToSchema(props[key]) if (props[key] instanceof Array && props[key].includes("undefined")) { // skip @@ -131,4 +137,5 @@ const translateXTypeToSchema = (xType, ctx) => { module.exports = { translateXTypeToSchema, + resolveAndMerge, } diff --git a/applications/x-types-decorators.js b/applications/x-types-decorators.js index 0be37a2..98f0577 100644 --- a/applications/x-types-decorators.js +++ b/applications/x-types-decorators.js @@ -1,19 +1,21 @@ const {isObject} = require("./x-types-utils") -const {translateXTypeToSchema} = require("./x-types-adapter") +const {translateXTypeToSchema, resolveAndMerge} = require("./x-types-adapter") const generateSchema = () => { return { MediaType: { leave(mediaType, ctx) { if (typeof mediaType["x-type"] === "undefined") return - const schema = translateXTypeToSchema(mediaType["x-type"], ctx) + const resolvedXType = resolveAndMerge(mediaType["x-type"], ctx) + const schema = translateXTypeToSchema(resolvedXType) mediaType.schema = schema }, }, Parameter: { leave(parameter, ctx) { if (typeof parameter["x-type"] === "undefined") return - const schema = translateXTypeToSchema(parameter["x-type"], ctx) + const resolvedXType = resolveAndMerge(parameter["x-type"], ctx) + const schema = translateXTypeToSchema(resolvedXType) parameter.schema = schema }, },