Skip to content

Commit

Permalink
Export OpenApiTypeChecker
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Jul 5, 2024
1 parent 4e440b4 commit b68f0c3
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 62 deletions.
348 changes: 348 additions & 0 deletions src/OpenApiTypeChecker.ts
Original file line number Diff line number Diff line change
@@ -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<string> = 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<OpenApi.IJsonSchema, Map<OpenApi.IJsonSchema, boolean>>) =>
(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<OpenApi.IJsonSchema, boolean> = 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<OpenApi.IJsonSchema, Map<OpenApi.IJsonSchema, boolean>>) =>
(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<OpenApi.IJsonSchema, Map<OpenApi.IJsonSchema, boolean>>) =>
(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<OpenApi.IJsonSchema, Map<OpenApi.IJsonSchema, boolean>>) =>
(
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<OpenApi.IJsonSchema, Map<OpenApi.IJsonSchema, boolean>>) =>
(
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<OpenApi.IJsonSchema.IString>["format"],
y: Required<OpenApi.IJsonSchema.IString>["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<OpenApi.IJsonSchema, OpenApi.IJsonSchema.IReference> =>
isReference(schema)
? escapeReference(components)(
components.schemas![
schema.$ref.replace("#/components/schemas/", "")
],
)
: schema;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./IMigrateRoute";
export * from "./IMigrateDocument";
export * from "./OpenApi";
export * from "./OpenApiTypeChecker";

export * from "./SwaggerV2";
export * from "./OpenApiV3";
Expand Down
2 changes: 1 addition & 1 deletion src/internal/MigrateRouteConverter.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading

0 comments on commit b68f0c3

Please sign in to comment.