From bea4963b817909c2bb9f26a2500d18b40f43cca9 Mon Sep 17 00:00:00 2001 From: kgtkr Date: Tue, 1 Feb 2022 16:08:49 +0900 Subject: [PATCH 1/3] schema input from stdin fix: #869 --- bin/cli.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/cli.js b/bin/cli.js index 276e143f0..6e8b900f4 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -140,6 +140,13 @@ async function main() { return; } + // handle stdin schema, exit + if (pathToSpec === "-") { + if (output !== "." && output === OUTPUT_FILE) fs.mkdirSync(path.dirname(flags.output), { recursive: true }); + await generateSchema("/dev/stdin"); + return; + } + // handle local schema(s) const inputSpecPaths = await glob(pathToSpec, { filesOnly: true }); const isGlob = inputSpecPaths.length > 1; From 2b98406723ab16ea0bcd5e76f342c8c38b5c1fde Mon Sep 17 00:00:00 2001 From: kgtkr Date: Tue, 15 Feb 2022 17:30:50 +0900 Subject: [PATCH 2/3] Add test --- test/bin/cli.test.js | 9 + test/bin/expected/stdin.ts | 489 +++++++++++++++++++++++++++++++++++++ 2 files changed, 498 insertions(+) create mode 100644 test/bin/expected/stdin.ts diff --git a/test/bin/cli.test.js b/test/bin/cli.test.js index 3ed298c20..5211df015 100644 --- a/test/bin/cli.test.js +++ b/test/bin/cli.test.js @@ -34,6 +34,15 @@ describe("cli", () => { expect(generated.toString("utf8")).to.equal(expected); }); + it("stdin", async () => { + execSync(`${cmd} - -o generated/stdin.ts < ./specs/petstore.yaml`, { + cwd, + }); + const generated = fs.readFileSync(new URL("./generated/stdin.ts", cwd), "utf8"); + const expected = eol.lf(fs.readFileSync(new URL("./expected/stdin.ts", cwd), "utf8")); + expect(generated).to.equal(expected); + }); + it("supports glob paths", async () => { execSync(`${cmd} "specs/*.yaml" -o generated/`, { cwd }); // Quotes are necessary because shells like zsh treats glob weirdly const generatedPetstore = fs.readFileSync(new URL("./generated/specs/petstore.ts", cwd), "utf8"); diff --git a/test/bin/expected/stdin.ts b/test/bin/expected/stdin.ts new file mode 100644 index 000000000..9199645c1 --- /dev/null +++ b/test/bin/expected/stdin.ts @@ -0,0 +1,489 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/pet": { + put: operations["updatePet"]; + post: operations["addPet"]; + }; + "/pet/findByStatus": { + /** Multiple status values can be provided with comma separated strings */ + get: operations["findPetsByStatus"]; + }; + "/pet/findByTags": { + /** Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ + get: operations["findPetsByTags"]; + }; + "/pet/{petId}": { + /** Returns a single pet */ + get: operations["getPetById"]; + post: operations["updatePetWithForm"]; + delete: operations["deletePet"]; + }; + "/pet/{petId}/uploadImage": { + post: operations["uploadFile"]; + }; + "/store/inventory": { + /** Returns a map of status codes to quantities */ + get: operations["getInventory"]; + }; + "/store/order": { + post: operations["placeOrder"]; + }; + "/store/order/{orderId}": { + /** For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions */ + get: operations["getOrderById"]; + /** For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors */ + delete: operations["deleteOrder"]; + }; + "/user": { + /** This can only be done by the logged in user. */ + post: operations["createUser"]; + }; + "/user/createWithArray": { + post: operations["createUsersWithArrayInput"]; + }; + "/user/createWithList": { + post: operations["createUsersWithListInput"]; + }; + "/user/login": { + get: operations["loginUser"]; + }; + "/user/logout": { + get: operations["logoutUser"]; + }; + "/user/{username}": { + get: operations["getUserByName"]; + /** This can only be done by the logged in user. */ + put: operations["updateUser"]; + /** This can only be done by the logged in user. */ + delete: operations["deleteUser"]; + }; +} + +export interface components { + schemas: { + Order: { + /** Format: int64 */ + id?: number; + /** Format: int64 */ + petId?: number; + /** Format: int32 */ + quantity?: number; + /** Format: date-time */ + shipDate?: string; + /** + * @description Order Status + * @enum {string} + */ + status?: "placed" | "approved" | "delivered"; + complete?: boolean; + }; + Category: { + /** Format: int64 */ + id?: number; + name?: string; + }; + User: { + /** Format: int64 */ + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + /** + * Format: int32 + * @description User Status + */ + userStatus?: number; + }; + Tag: { + /** Format: int64 */ + id?: number; + name?: string; + }; + Pet: { + /** Format: int64 */ + id?: number; + category?: components["schemas"]["Category"]; + /** @example doggie */ + name: string; + photoUrls: string[]; + tags?: components["schemas"]["Tag"][]; + /** + * @description pet status in the store + * @enum {string} + */ + status?: "available" | "pending" | "sold"; + }; + ApiResponse: { + /** Format: int32 */ + code?: number; + type?: string; + message?: string; + }; + }; +} + +export interface operations { + updatePet: { + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + /** Validation exception */ + 405: unknown; + }; + /** Pet object that needs to be added to the store */ + requestBody: { + content: { + "application/json": components["schemas"]["Pet"]; + "application/xml": components["schemas"]["Pet"]; + }; + }; + }; + addPet: { + responses: { + /** Invalid input */ + 405: unknown; + }; + /** Pet object that needs to be added to the store */ + requestBody: { + content: { + "application/json": components["schemas"]["Pet"]; + "application/xml": components["schemas"]["Pet"]; + }; + }; + }; + /** Multiple status values can be provided with comma separated strings */ + findPetsByStatus: { + parameters: { + query: { + /** Status values that need to be considered for filter */ + status: ("available" | "pending" | "sold")[]; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"][]; + "application/json": components["schemas"]["Pet"][]; + }; + }; + /** Invalid status value */ + 400: unknown; + }; + }; + /** Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ + findPetsByTags: { + parameters: { + query: { + /** Tags to filter by */ + tags: string[]; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"][]; + "application/json": components["schemas"]["Pet"][]; + }; + }; + /** Invalid tag value */ + 400: unknown; + }; + }; + /** Returns a single pet */ + getPetById: { + parameters: { + path: { + /** ID of pet to return */ + petId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"]; + "application/json": components["schemas"]["Pet"]; + }; + }; + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + }; + }; + updatePetWithForm: { + parameters: { + path: { + /** ID of pet that needs to be updated */ + petId: number; + }; + }; + responses: { + /** Invalid input */ + 405: unknown; + }; + requestBody: { + content: { + "application/x-www-form-urlencoded": { + /** @description Updated name of the pet */ + name?: string; + /** @description Updated status of the pet */ + status?: string; + }; + }; + }; + }; + deletePet: { + parameters: { + header: { + api_key?: string; + }; + path: { + /** Pet id to delete */ + petId: number; + }; + }; + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + }; + }; + uploadFile: { + parameters: { + path: { + /** ID of pet to update */ + petId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/json": components["schemas"]["ApiResponse"]; + }; + }; + }; + requestBody: { + content: { + "multipart/form-data": { + /** @description Additional data to pass to server */ + additionalMetadata?: string; + /** + * Format: binary + * @description file to upload + */ + file?: string; + }; + }; + }; + }; + /** Returns a map of status codes to quantities */ + getInventory: { + responses: { + /** successful operation */ + 200: { + content: { + "application/json": { [key: string]: number }; + }; + }; + }; + }; + placeOrder: { + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Order"]; + "application/json": components["schemas"]["Order"]; + }; + }; + /** Invalid Order */ + 400: unknown; + }; + /** order placed for purchasing the pet */ + requestBody: { + content: { + "*/*": components["schemas"]["Order"]; + }; + }; + }; + /** For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions */ + getOrderById: { + parameters: { + path: { + /** ID of pet that needs to be fetched */ + orderId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Order"]; + "application/json": components["schemas"]["Order"]; + }; + }; + /** Invalid ID supplied */ + 400: unknown; + /** Order not found */ + 404: unknown; + }; + }; + /** For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors */ + deleteOrder: { + parameters: { + path: { + /** ID of the order that needs to be deleted */ + orderId: number; + }; + }; + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Order not found */ + 404: unknown; + }; + }; + /** This can only be done by the logged in user. */ + createUser: { + responses: { + /** successful operation */ + default: unknown; + }; + /** Created user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"]; + }; + }; + }; + createUsersWithArrayInput: { + responses: { + /** successful operation */ + default: unknown; + }; + /** List of user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"][]; + }; + }; + }; + createUsersWithListInput: { + responses: { + /** successful operation */ + default: unknown; + }; + /** List of user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"][]; + }; + }; + }; + loginUser: { + parameters: { + query: { + /** The user name for login */ + username: string; + /** The password for login in clear text */ + password: string; + }; + }; + responses: { + /** successful operation */ + 200: { + headers: { + /** calls per hour allowed by the user */ + "X-Rate-Limit"?: number; + /** date in UTC when token expires */ + "X-Expires-After"?: string; + }; + content: { + "application/xml": string; + "application/json": string; + }; + }; + /** Invalid username/password supplied */ + 400: unknown; + }; + }; + logoutUser: { + responses: { + /** successful operation */ + default: unknown; + }; + }; + getUserByName: { + parameters: { + path: { + /** The name that needs to be fetched. Use user1 for testing. */ + username: string; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["User"]; + "application/json": components["schemas"]["User"]; + }; + }; + /** Invalid username supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + }; + /** This can only be done by the logged in user. */ + updateUser: { + parameters: { + path: { + /** name that need to be updated */ + username: string; + }; + }; + responses: { + /** Invalid user supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + /** Updated user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"]; + }; + }; + }; + /** This can only be done by the logged in user. */ + deleteUser: { + parameters: { + path: { + /** The name that needs to be deleted */ + username: string; + }; + }; + responses: { + /** Invalid username supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + }; +} + +export interface external {} From e13a9f1e8a2fd3282a759f55bef684752bee463f Mon Sep 17 00:00:00 2001 From: tkr Date: Thu, 10 Mar 2022 16:15:10 +0900 Subject: [PATCH 3/3] fixup! schema input from stdin --- bin/cli.js | 2 +- src/index.ts | 3 ++- src/load.ts | 29 +++++++++++++++++++++++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 6e8b900f4..bc764f39d 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -143,7 +143,7 @@ async function main() { // handle stdin schema, exit if (pathToSpec === "-") { if (output !== "." && output === OUTPUT_FILE) fs.mkdirSync(path.dirname(flags.output), { recursive: true }); - await generateSchema("/dev/stdin"); + await generateSchema(process.stdin); return; } diff --git a/src/index.ts b/src/index.ts index 660708621..d4c229f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { URL } from "url"; import load, { resolveSchema, VIRTUAL_JSON_URL } from "./load.js"; import { swaggerVersion } from "./utils.js"; import { transformAll } from "./transform/index.js"; +import { Readable } from "stream"; export * from "./types.js"; // expose all types to consumers export const WARNING_MESSAGE = `/** @@ -30,7 +31,7 @@ export const WARNING_MESSAGE = `/** * @return {Promise} {Promise} Parsed file schema */ async function openapiTS( - schema: string | URL | OpenAPI2 | OpenAPI3 | Record, + schema: string | URL | OpenAPI2 | OpenAPI3 | Record | Readable, options: SwaggerToTSOptions = {} as Partial ): Promise { const ctx: GlobalContext = { diff --git a/src/load.ts b/src/load.ts index 5fa60b06e..cb2df70bc 100644 --- a/src/load.ts +++ b/src/load.ts @@ -7,6 +7,7 @@ import { URL } from "url"; import mime from "mime"; import yaml from "js-yaml"; import { parseRef } from "./utils.js"; +import { Readable } from "stream"; type PartialSchema = Record; // not a very accurate type, but this is easier to deal with before we know we’re dealing with a valid spec type SchemaMap = { [url: string]: PartialSchema }; @@ -95,13 +96,13 @@ interface LoadOptions extends GlobalContext { /** Load a schema from local path or remote URL */ export default async function load( - schema: URL | PartialSchema, + schema: URL | PartialSchema | Readable, options: LoadOptions ): Promise<{ [url: string]: PartialSchema }> { const urlCache = options.urlCache || new Set(); - const isJSON = schema instanceof URL === false; // if this is dynamically-passed-in JSON, we’ll have to change a few things - let schemaID = isJSON ? new URL(VIRTUAL_JSON_URL).href : (schema.href as string); + const isJSON = schema instanceof URL === false && !(schema instanceof Readable); // if this is dynamically-passed-in JSON, we’ll have to change a few things + let schemaID = isJSON || schema instanceof Readable ? new URL(VIRTUAL_JSON_URL).href : (schema.href as string); const schemas = options.schemas; @@ -116,9 +117,25 @@ export default async function load( let contents = ""; let contentType = ""; - const schemaURL = schema as URL; // helps TypeScript - - if (isFile(schemaURL)) { + const schemaURL = schema instanceof Readable ? new URL(VIRTUAL_JSON_URL) : (schema as URL); // helps TypeScript + + if (schema instanceof Readable) { + const readable = schema; + contents = await new Promise((resolve) => { + readable.resume(); + readable.setEncoding("utf8"); + + let content = ""; + readable.on("data", (chunk: string) => { + content += chunk; + }); + + readable.on("end", () => { + resolve(content); + }); + }); + contentType = "text/yaml"; + } else if (isFile(schemaURL)) { // load local contents = fs.readFileSync(schemaURL, "utf8"); contentType = mime.getType(schemaID) || "";