diff --git a/src/OpenApiTypeChecker.ts b/src/OpenApiTypeChecker.ts new file mode 100644 index 0000000..1d1d4ff --- /dev/null +++ b/src/OpenApiTypeChecker.ts @@ -0,0 +1,348 @@ +import { OpenApi } from "./OpenApi"; +import { MapUtil } from "./utils/MapUtil"; + +export namespace OpenApiTypeChecker { + export const visit = + (closure: (schema: OpenApi.IJsonSchema) => void) => + (components: OpenApi.IComponents) => { + const already: Set = new Set(); + const next = (schema: OpenApi.IJsonSchema): void => { + closure(schema); + if (OpenApiTypeChecker.isReference(schema)) { + const key: string = schema.$ref.split("#/components/schemas/").pop()!; + if (already.has(key) === true) return; + already.add(key); + const found: OpenApi.IJsonSchema | undefined = + components.schemas?.[key]; + if (found !== undefined) next(found); + } else if (OpenApiTypeChecker.isOneOf(schema)) + schema.oneOf.forEach(next); + else if (OpenApiTypeChecker.isObject(schema)) { + for (const value of Object.values(schema.properties ?? {})) + next(value); + if ( + typeof schema.additionalProperties === "object" && + schema.additionalProperties !== null + ) + next(schema.additionalProperties); + } else if (OpenApiTypeChecker.isArray(schema)) next(schema.items); + else if (OpenApiTypeChecker.isTuple(schema)) { + (schema.prefixItems ?? []).forEach(next); + if ( + typeof schema.additionalItems === "object" && + schema.additionalItems !== null + ) + next(schema.additionalItems); + } + }; + return next; + }; + + /* ----------------------------------------------------------- + TYPE CHECKERS + ----------------------------------------------------------- */ + export const isNull = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.INull => + (schema as OpenApi.IJsonSchema.INull).type === "null"; + export const isUnknown = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IUnknown => + (schema as OpenApi.IJsonSchema.IUnknown).type === undefined && + !isConstant(schema) && + !isOneOf(schema) && + !isReference(schema); + + export const isConstant = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IConstant => + (schema as OpenApi.IJsonSchema.IConstant).const !== undefined; + export const isBoolean = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IBoolean => + (schema as OpenApi.IJsonSchema.IBoolean).type === "boolean"; + export const isInteger = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IInteger => + (schema as OpenApi.IJsonSchema.IInteger).type === "integer"; + export const isNumber = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.INumber => + (schema as OpenApi.IJsonSchema.INumber).type === "number"; + export const isString = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IString => + (schema as OpenApi.IJsonSchema.IString).type === "string"; + + export const isArray = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IArray => + (schema as OpenApi.IJsonSchema.IArray).type === "array" && + (schema as OpenApi.IJsonSchema.IArray).items !== undefined; + export const isTuple = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.ITuple => + (schema as OpenApi.IJsonSchema.ITuple).type === "array" && + (schema as OpenApi.IJsonSchema.ITuple).prefixItems !== undefined; + export const isObject = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IObject => + (schema as OpenApi.IJsonSchema.IObject).type === "object"; + export const isReference = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IReference => + (schema as any).$ref !== undefined; + export const isOneOf = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IOneOf => + (schema as OpenApi.IJsonSchema.IOneOf).oneOf !== undefined; + + /* ----------------------------------------------------------- + OPERATORS + ----------------------------------------------------------- */ + export const covers = ( + components: OpenApi.IComponents, + ): ((x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema) => boolean) => + coverStation(components)(new Map()); + + const coverStation = + (components: OpenApi.IComponents) => + (visited: Map>) => + (x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema): boolean => { + const cache: boolean | undefined = visited.get(x)?.get(y); + if (cache !== undefined) return cache; + + // FOR RECURSIVE CASE + const nested: Map = MapUtil.take(visited)( + x, + )(() => new Map()); + nested.set(y, true); + + // COMPUTE IT + const result: boolean = coverSchema(components)(visited)(x, y); + nested.set(y, result); + return result; + }; + + const coverSchema = + (components: OpenApi.IComponents) => + (visited: Map>) => + (x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema): boolean => { + // CHECK EQUALITY + if (x === y) return true; + else if (isReference(x) && isReference(y) && x.$ref === y.$ref) + return true; + + // COMPARE WITH FLATTENING + const alpha: OpenApi.IJsonSchema[] = flatSchema(components)(x); + const beta: OpenApi.IJsonSchema[] = flatSchema(components)(y); + if (alpha.some((x) => isUnknown(x))) return true; + else if (beta.some((x) => isUnknown(x))) return false; + return beta.every((b) => + alpha.some((a) => coverEscapedSchema(components)(visited)(a, b)), + ); + }; + + const coverEscapedSchema = + (components: OpenApi.IComponents) => + (visited: Map>) => + (x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema): boolean => { + // CHECK EQUALITY + if (x === y) return true; + else if (isUnknown(x)) return true; + else if (isUnknown(y)) return false; + else if (isNull(x)) return isNull(y); + // ATOMIC CASE + else if (isConstant(x)) return isConstant(y) && x.const === y.const; + else if (isBoolean(x)) + return isBoolean(y) || (isConstant(y) && typeof y.const === "boolean"); + else if (isInteger(x)) + return (isInteger(y) || isConstant(y)) && coverInteger(x, y); + else if (isNumber(x)) + return ( + (isConstant(y) || isInteger(y) || isNumber(y)) && coverNumber(x, y) + ); + else if (isString(x)) + return (isConstant(y) || isString(y)) && coverString(x, y); + // INSTANCE CASE + else if (isArray(x)) + return ( + (isArray(y) || isTuple(y)) && coverArray(components)(visited)(x, y) + ); + else if (isObject(x)) + return isObject(y) && coverObject(components)(visited)(x, y); + else if (isReference(x)) return isReference(y) && x.$ref === y.$ref; + return false; + }; + + const coverArray = + (components: OpenApi.IComponents) => + (visited: Map>) => + ( + x: OpenApi.IJsonSchema.IArray, + y: OpenApi.IJsonSchema.IArray | OpenApi.IJsonSchema.ITuple, + ): boolean => { + if (isTuple(y)) + return ( + y.prefixItems.every((v) => + coverStation(components)(visited)(x.items, v), + ) && + (y.additionalItems === undefined || + (typeof y.additionalItems === "object" && + coverStation(components)(visited)(x.items, y.additionalItems))) + ); + else if ( + !( + x.minItems === undefined || + (y.minItems !== undefined && x.minItems <= y.minItems) + ) + ) + return false; + else if ( + !( + x.maxItems === undefined || + (y.maxItems !== undefined && x.maxItems >= y.maxItems) + ) + ) + return false; + return coverStation(components)(visited)(x.items, y.items); + }; + + const coverObject = + (components: OpenApi.IComponents) => + (visited: Map>) => + ( + x: OpenApi.IJsonSchema.IObject, + y: OpenApi.IJsonSchema.IObject, + ): boolean => { + if (!x.additionalProperties && !!y.additionalProperties) return false; + else if ( + !!x.additionalProperties && + !!y.additionalProperties && + ((typeof x.additionalProperties === "object" && + y.additionalProperties === true) || + (typeof x.additionalProperties === "object" && + typeof y.additionalProperties === "object" && + !coverStation(components)(visited)( + x.additionalProperties, + y.additionalProperties, + ))) + ) + return false; + return Object.entries(y.properties ?? {}).every(([key, b]) => { + const a: OpenApi.IJsonSchema | undefined = x.properties?.[key]; + if (a === undefined) return false; + else if ( + (x.required?.includes(key) ?? false) === true && + (y.required?.includes(key) ?? false) === false + ) + return false; + return coverStation(components)(visited)(a, b); + }); + }; + + const coverInteger = ( + x: OpenApi.IJsonSchema.IInteger, + y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IInteger, + ): boolean => { + if (isConstant(y)) + return typeof y.const === "number" && Number.isInteger(y.const); + return [ + x.type === y.type, + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + const coverNumber = ( + x: OpenApi.IJsonSchema.INumber, + y: + | OpenApi.IJsonSchema.IConstant + | OpenApi.IJsonSchema.IInteger + | OpenApi.IJsonSchema.INumber, + ): boolean => { + if (isConstant(y)) return typeof y.const === "number"; + return [ + x.type === y.type || (x.type === "number" && y.type === "integer"), + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + const coverString = ( + x: OpenApi.IJsonSchema.IString, + y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IString, + ): boolean => { + if (isConstant(y)) return typeof y.const === "string"; + return [ + x.format === undefined || + (y.format !== undefined && coverFormat(x.format, y.format)), + x.pattern === undefined || x.pattern === y.pattern, + x.minLength === undefined || + (y.minLength !== undefined && x.minLength <= y.minLength), + x.maxLength === undefined || + (y.maxLength !== undefined && x.maxLength >= y.maxLength), + ].every((v) => v); + }; + + const coverFormat = ( + x: Required["format"], + y: Required["format"], + ): boolean => + x === y || + (x === "idn-email" && y === "email") || + (x === "idn-hostname" && y === "hostname") || + (["uri", "iri"].includes(x) && y === "url") || + (x === "iri" && y === "uri") || + (x === "iri-reference" && y === "uri-reference"); + + const flatSchema = + (components: OpenApi.IComponents) => + (schema: OpenApi.IJsonSchema): OpenApi.IJsonSchema[] => { + schema = escapeReference(components)(schema); + if (isOneOf(schema)) + return schema.oneOf.map(flatSchema(components)).flat(); + return [schema]; + }; + + const escapeReference = + (components: OpenApi.IComponents) => + ( + schema: OpenApi.IJsonSchema, + ): Exclude => + isReference(schema) + ? escapeReference(components)( + components.schemas![ + schema.$ref.replace("#/components/schemas/", "") + ], + ) + : schema; +} diff --git a/src/index.ts b/src/index.ts index f569352..1681d00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from "./IMigrateRoute"; export * from "./IMigrateDocument"; export * from "./OpenApi"; +export * from "./OpenApiTypeChecker"; export * from "./SwaggerV2"; export * from "./OpenApiV3"; diff --git a/src/internal/MigrateRouteConverter.ts b/src/internal/MigrateRouteConverter.ts index 9fddabd..0500b16 100644 --- a/src/internal/MigrateRouteConverter.ts +++ b/src/internal/MigrateRouteConverter.ts @@ -1,8 +1,8 @@ import { IMigrateRoute } from "../IMigrateRoute"; import { OpenApi } from "../OpenApi"; +import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { Escaper } from "../utils/Escaper"; import { StringUtil } from "../utils/StringUtil"; -import { OpenApiTypeChecker } from "./OpenApiTypeChecker"; export namespace MigrateRouteConverter { export interface IProps { diff --git a/src/internal/OpenApiTypeChecker.ts b/src/internal/OpenApiTypeChecker.ts index 6d84f8f..64f9a56 100644 --- a/src/internal/OpenApiTypeChecker.ts +++ b/src/internal/OpenApiTypeChecker.ts @@ -1,59 +1,2 @@ -import { OpenApi } from "../OpenApi"; - -export namespace OpenApiTypeChecker { - export const isNull = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.INull => - (schema as OpenApi.IJsonSchema.INull).type === "null"; - export const isUnknown = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IUnknown => - (schema as OpenApi.IJsonSchema.IUnknown).type === undefined && - !isConstant(schema) && - !isOneOf(schema) && - !isReference(schema); - - export const isConstant = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IConstant => - (schema as OpenApi.IJsonSchema.IConstant).const !== undefined; - export const isBoolean = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IBoolean => - (schema as OpenApi.IJsonSchema.IBoolean).type === "boolean"; - export const isInteger = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IInteger => - (schema as OpenApi.IJsonSchema.IInteger).type === "integer"; - export const isNumber = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.INumber => - (schema as OpenApi.IJsonSchema.INumber).type === "number"; - export const isString = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IString => - (schema as OpenApi.IJsonSchema.IString).type === "string"; - - export const isArray = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IArray => - (schema as OpenApi.IJsonSchema.IArray).type === "array" && - (schema as OpenApi.IJsonSchema.IArray).items !== undefined; - export const isTuple = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.ITuple => - (schema as OpenApi.IJsonSchema.ITuple).type === "array" && - (schema as OpenApi.IJsonSchema.ITuple).prefixItems !== undefined; - export const isObject = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IObject => - (schema as OpenApi.IJsonSchema.IObject).type === "object"; - export const isReference = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IReference => - (schema as any).$ref !== undefined; - export const isOneOf = ( - schema: OpenApi.IJsonSchema, - ): schema is OpenApi.IJsonSchema.IOneOf => - (schema as OpenApi.IJsonSchema.IOneOf).oneOf !== undefined; -} +// FOR LEGACY VERSIONS +export * from "../OpenApiTypeChecker"; diff --git a/src/internal/OpenApiV3Downgrader.ts b/src/internal/OpenApiV3Downgrader.ts index c273207..f16acf1 100644 --- a/src/internal/OpenApiV3Downgrader.ts +++ b/src/internal/OpenApiV3Downgrader.ts @@ -1,6 +1,6 @@ import { OpenApi } from "../OpenApi"; +import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { OpenApiV3 } from "../OpenApiV3"; -import { OpenApiTypeChecker } from "./OpenApiTypeChecker"; export namespace OpenApiV3Downgrader { export interface IComponentsCollection { diff --git a/src/internal/SwaggerV2Downgrader.ts b/src/internal/SwaggerV2Downgrader.ts index cb7f389..aa4bdb0 100644 --- a/src/internal/SwaggerV2Downgrader.ts +++ b/src/internal/SwaggerV2Downgrader.ts @@ -1,6 +1,6 @@ import { OpenApi } from "../OpenApi"; +import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { SwaggerV2 } from "../SwaggerV2"; -import { OpenApiTypeChecker } from "./OpenApiTypeChecker"; export namespace SwaggerV2Downgrader { export interface IComponentsCollection {