Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Deal with discriminator pattern
Browse files Browse the repository at this point in the history
Just the components/schemas cases, everything outside will not resolve
  • Loading branch information
fabien0102 committed Nov 14, 2019
1 parent b9a3f52 commit 0ce7bb6
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 12 deletions.
59 changes: 48 additions & 11 deletions src/scripts/import-open-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,41 @@ export interface ${pascal(name)} ${scalar}`
: `export interface ${pascal(name)} ${scalar}`;
};

/**
* Propagate every `discriminator.propertyName` mapping to the original ref
*
* Note: This method directly mutate the `specs` object.
*
* @param specs
*/
export const resolveDiscriminator = (specs: OpenAPIObject) => {
if (specs.components && specs.components.schemas) {
Object.values(specs.components.schemas).forEach(schema => {
if (!schema.discriminator || !schema.discriminator.mapping) {
return;
}
const { mapping, propertyName } = schema.discriminator;

Object.entries(mapping).map(([name, ref]) => {
if (!ref.startsWith("#/components/schemas/")) {
throw new Error("Discriminator mapping outside of `#/components/schemas` is not supported");
}
if (
specs.components &&
specs.components.schemas &&
specs.components.schemas[ref.slice("#/components/schemas/".length)] &&
specs.components.schemas[ref.slice("#/components/schemas/".length)].properties &&
specs.components.schemas[ref.slice("#/components/schemas/".length)].properties![propertyName] &&
!specs.components.schemas[ref.slice("#/components/schemas/".length)].properties![propertyName].$ref
) {
// @ts-ignore This is check on runtime
specs.components.schemas[ref.slice("#/components/schemas/".length)].properties![propertyName].enum = [name];
}
});
});
}
};

/**
* Extract all types from #/components/schemas
*
Expand Down Expand Up @@ -501,9 +536,9 @@ export interface ${pascal(name)}Response ${type}`;
/**
* Validate the spec with ibm-openapi-validator (with a custom pretty logger).
*
* @param schema openAPI spec
* @param specs openAPI spec
*/
const validate = async (schema: OpenAPIObject) => {
const validate = async (specs: OpenAPIObject) => {
// tslint:disable:no-console
const log = console.log;

Expand All @@ -515,7 +550,7 @@ const validate = async (schema: OpenAPIObject) => {
wasConsoleLogCalledFromBlackBox = true;
log(...props);
};
const { errors, warnings } = await openApiValidator(schema);
const { errors, warnings } = await openApiValidator(specs);
console.log = log; // reset console.log because we're done with the black box

if (wasConsoleLogCalledFromBlackBox) {
Expand Down Expand Up @@ -562,26 +597,28 @@ const importOpenApi = async ({
}: {
data: string;
format: "yaml" | "json";
transformer?: (schema: OpenAPIObject) => OpenAPIObject;
transformer?: (specs: OpenAPIObject) => OpenAPIObject;
validation?: boolean;
customImport?: AdvancedOptions["customImport"];
customProps?: AdvancedOptions["customProps"];
}) => {
const operationIds: string[] = [];
let schema = await importSpecs(data, format);
let specs = await importSpecs(data, format);
if (transformer) {
schema = transformer(schema);
specs = transformer(specs);
}

if (validation) {
await validate(schema);
await validate(specs);
}

resolveDiscriminator(specs);

let output = "";

output += generateSchemasDefinition(schema.components && schema.components.schemas);
output += generateResponsesDefinition(schema.components && schema.components.responses);
Object.entries(schema.paths).forEach(([route, verbs]: [string, PathItemObject]) => {
output += generateSchemasDefinition(specs.components && specs.components.schemas);
output += generateResponsesDefinition(specs.components && specs.components.responses);
Object.entries(specs.paths).forEach(([route, verbs]: [string, PathItemObject]) => {
Object.entries(verbs).forEach(([verb, operation]: [string, OperationObject]) => {
if (["get", "post", "patch", "put", "delete"].includes(verb)) {
output += generateRestfulComponent(
Expand All @@ -590,7 +627,7 @@ const importOpenApi = async ({
route,
operationIds,
verbs.parameters,
schema.components,
specs.components,
customProps,
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/scripts/tests/__snapshots__/import-open-api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type Pet = NewPet & {id: number};
export interface NewPet {name: string; tag?: string}
export type CatOrDog = Cat | Dog;
export interface Cat {type: \\"cat\\"; breed: \\"labrador\\" | \\"carlin\\" | \\"beagle\\"}
export interface Dog {type: \\"dog\\"; breed: \\"saimois\\" | \\"bengal\\" | \\"british shorthair\\"}
export interface Error {code: number; message: string}
export interface FindPetsQueryParams {tags?: string[]; limit?: number}
Expand Down
96 changes: 95 additions & 1 deletion src/scripts/tests/import-open-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from "fs";
import { join } from "path";

import { ComponentsObject, OperationObject, ResponseObject } from "openapi3-ts";
import { ComponentsObject, OpenAPIObject, OperationObject, ResponseObject } from "openapi3-ts";

import importOpenApi, {
generateResponsesDefinition,
Expand All @@ -15,6 +15,7 @@ import importOpenApi, {
getScalar,
isReference,
reactPropsValueToObjectValue,
resolveDiscriminator,
} from "../import-open-api";

describe("scripts/import-open-api", () => {
Expand Down Expand Up @@ -271,6 +272,99 @@ describe("scripts/import-open-api", () => {
});
});

describe("resolveDiscriminator", () => {
it("should propagate any discrimator value as enum", () => {
const specs: OpenAPIObject = {
openapi: "3.0.0",
info: {
title: "Test",
description: "Test",
version: "0.0.1",
contact: {
name: "Fabien Bernard",
url: "https://fabien0102.com",
email: "fabien@contiamo.com",
},
},
paths: {},
components: {
schemas: {
Error: {
oneOf: [{ $ref: "#/components/schemas/GeneralError" }, { $ref: "#/components/schemas/FieldError" }],
discriminator: {
propertyName: "type",
mapping: {
GeneralError: "#/components/schemas/GeneralError",
FieldError: "#/components/schemas/FieldError",
},
},
},
GeneralError: {
type: "object",
properties: {
type: {
type: "string",
},
message: {
type: "string",
},
},
required: ["type", "message"],
},
FieldError: {
type: "object",
properties: {
type: {
type: "string",
},
message: {
type: "string",
},
key: {
type: "string",
},
},
required: ["type", "message", "key"],
},
},
},
};

resolveDiscriminator(specs);

expect(specs.components.schemas.GeneralError).toEqual({
type: "object",
properties: {
type: {
type: "string",
enum: ["GeneralError"],
},
message: {
type: "string",
},
},
required: ["type", "message"],
});

expect(specs.components.schemas.FieldError).toEqual({
type: "object",
properties: {
type: {
type: "string",
enum: ["FieldError"],
},
message: {
type: "string",
},
key: {
type: "string",
},
},
required: ["type", "message", "key"],
});
});
});

describe("generateSchemasDefinition", () => {
it("should declare an interface for simple object", () => {
const schema = {
Expand Down
41 changes: 41 additions & 0 deletions src/scripts/tests/petstore-expanded.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,47 @@ components:
tag:
type: string

CatOrDog:
description: A discriminator example.
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
discriminator:
propertyName: type
mapping:
cat: "#/components/schemas/Cat"
dog: "#/components/schemas/Dog"

Cat:
type: object
properties:
type:
type: string
breed:
type: string
enum:
- labrador
- carlin
- beagle
required:
- type
- breed

Dog:
type: object
properties:
type:
type: string
breed:
type: string
enum:
- saimois
- bengal
- british shorthair
required:
- type
- breed

Error:
required:
- code
Expand Down

0 comments on commit 0ce7bb6

Please sign in to comment.