openapi-typescript
Advanced tools
Comparing version 4.0.1 to 4.0.2
@@ -33,2 +33,3 @@ "use strict"; | ||
const parser_typescript_1 = __importDefault(require("prettier/parser-typescript")); | ||
const url_1 = require("url"); | ||
const load_1 = __importStar(require("./load")); | ||
@@ -57,2 +58,3 @@ const utils_1 = require("./utils"); | ||
let external = {}; | ||
const allSchemas = {}; | ||
if (typeof schema === "string") { | ||
@@ -62,14 +64,13 @@ const schemaURL = load_1.resolveSchema(schema); | ||
console.log(kleur_1.yellow(`🔭 Loading spec from ${kleur_1.bold(schemaURL.href)}…`)); | ||
const schemas = {}; | ||
await load_1.default(schemaURL, { | ||
...ctx, | ||
schemas, | ||
schemas: allSchemas, | ||
rootURL: schemaURL, | ||
}); | ||
for (const k of Object.keys(schemas)) { | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === schemaURL.href) { | ||
rootSchema = schemas[k]; | ||
rootSchema = allSchemas[k]; | ||
} | ||
else { | ||
external[k] = schemas[k]; | ||
external[k] = allSchemas[k]; | ||
} | ||
@@ -79,3 +80,11 @@ } | ||
else { | ||
rootSchema = schema; | ||
await load_1.default(schema, { ...ctx, schemas: allSchemas, rootURL: new url_1.URL(load_1.VIRTUAL_JSON_URL) }); | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === load_1.VIRTUAL_JSON_URL) { | ||
rootSchema = allSchemas[k]; | ||
} | ||
else { | ||
external[k] = allSchemas[k]; | ||
} | ||
} | ||
} | ||
@@ -82,0 +91,0 @@ let output = exports.WARNING_MESSAGE; |
@@ -5,12 +5,14 @@ /// <reference types="node" /> | ||
declare type PartialSchema = Record<string, any>; | ||
declare type SchemaMap = { | ||
[url: string]: PartialSchema; | ||
}; | ||
export declare const VIRTUAL_JSON_URL = "file:///_json"; | ||
export declare function resolveSchema(url: string): URL; | ||
interface LoadOptions extends GlobalContext { | ||
rootURL: URL; | ||
schemas: { | ||
[url: string]: PartialSchema; | ||
}; | ||
schemas: SchemaMap; | ||
} | ||
export default function load(schemaURL: URL, options: LoadOptions): Promise<{ | ||
export default function load(schema: URL | PartialSchema, options: LoadOptions): Promise<{ | ||
[url: string]: PartialSchema; | ||
}>; | ||
export {}; |
@@ -25,3 +25,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.resolveSchema = void 0; | ||
exports.resolveSchema = exports.VIRTUAL_JSON_URL = void 0; | ||
const fs_1 = __importDefault(require("fs")); | ||
@@ -31,5 +31,7 @@ const path_1 = __importDefault(require("path")); | ||
const node_fetch_1 = __importStar(require("node-fetch")); | ||
const slash_1 = __importDefault(require("slash")); | ||
const mime_1 = __importDefault(require("mime")); | ||
const js_yaml_1 = __importDefault(require("js-yaml")); | ||
const utils_1 = require("./utils"); | ||
exports.VIRTUAL_JSON_URL = `file:///_json`; | ||
function parseSchema(schema, type) { | ||
@@ -60,3 +62,5 @@ if (type === "YAML") { | ||
} | ||
const localPath = path_1.default.isAbsolute(url) ? new url_1.URL("", `file://${url}`) : new url_1.URL(url, `file://${process.cwd()}/`); | ||
const localPath = path_1.default.isAbsolute(url) | ||
? new url_1.URL("", `file://${slash_1.default(url)}`) | ||
: new url_1.URL(url, `file://${slash_1.default(process.cwd())}/`); | ||
if (!fs_1.default.existsSync(localPath)) { | ||
@@ -72,41 +76,50 @@ throw new Error(`Could not locate ${url}`); | ||
let urlCache = new Set(); | ||
async function load(schemaURL, options) { | ||
if (urlCache.has(schemaURL.href)) | ||
return options.schemas; | ||
urlCache.add(schemaURL.href); | ||
async function load(schema, options) { | ||
const isJSON = schema instanceof url_1.URL === false; | ||
let schemaID = isJSON ? new url_1.URL(exports.VIRTUAL_JSON_URL).href : schema.href; | ||
const schemas = options.schemas; | ||
let contents = ""; | ||
let contentType = ""; | ||
if (isFile(schemaURL)) { | ||
contents = await fs_1.default.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime_1.default.getType(schemaURL.href) || ""; | ||
if (isJSON) { | ||
schemas[schemaID] = schema; | ||
} | ||
else { | ||
const headers = new node_fetch_1.Headers(); | ||
if (options.auth) | ||
headers.set("Authorization", options.auth); | ||
const res = await node_fetch_1.default(schemaURL.href, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
} | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
} | ||
else if (isJSON) { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
} | ||
else { | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
if (urlCache.has(schemaID)) | ||
return options.schemas; | ||
urlCache.add(schemaID); | ||
let contents = ""; | ||
let contentType = ""; | ||
const schemaURL = schema; | ||
if (isFile(schemaURL)) { | ||
contents = await fs_1.default.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime_1.default.getType(schemaID) || ""; | ||
} | ||
catch (err1) { | ||
else { | ||
const headers = new node_fetch_1.Headers(); | ||
headers.set("User-Agent", "openapi-typescript"); | ||
if (options.auth) | ||
headers.set("Authorization", options.auth); | ||
const res = await node_fetch_1.default(schemaID, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
} | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} | ||
else if (isJSON) { | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} | ||
else { | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} | ||
catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); | ||
catch (err1) { | ||
try { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} | ||
catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); | ||
} | ||
} | ||
@@ -116,3 +129,3 @@ } | ||
const refPromises = []; | ||
schemas[schemaURL.href] = JSON.parse(JSON.stringify(schemas[schemaURL.href]), (k, v) => { | ||
schemas[schemaID] = JSON.parse(JSON.stringify(schemas[schemaID]), (k, v) => { | ||
if (k !== "$ref" || typeof v !== "string") | ||
@@ -122,3 +135,7 @@ return v; | ||
if (refURL) { | ||
const nextURL = refURL.startsWith("http://") || refURL.startsWith("https://") ? new url_1.URL(refURL) : new url_1.URL(refURL, schemaURL); | ||
const isRemoteURL = refURL.startsWith("http://") || refURL.startsWith("https://"); | ||
if (isJSON && !isRemoteURL) { | ||
throw new Error(`Can’t load URL "${refURL}" from dynamic JSON. Load this schema from a URL instead.`); | ||
} | ||
const nextURL = isRemoteURL ? new url_1.URL(refURL) : new url_1.URL(slash_1.default(refURL), schema); | ||
refPromises.push(load(nextURL, options).then((subschemas) => { | ||
@@ -134,3 +151,3 @@ for (const subschemaURL of Object.keys(subschemas)) { | ||
await Promise.all(refPromises); | ||
if (schemaURL.href === options.rootURL.href) { | ||
if (schemaID === options.rootURL.href) { | ||
for (const subschemaURL of Object.keys(schemas)) { | ||
@@ -137,0 +154,0 @@ schemas[subschemaURL] = JSON.parse(JSON.stringify(schemas[subschemaURL]), (k, v) => { |
@@ -6,2 +6,3 @@ import { GlobalContext } from "../types"; | ||
export declare function transformSchemaObjMap(obj: Record<string, any>, options: TransformSchemaObjOptions): string; | ||
export declare function addRequiredProps(properties: Record<string, any>, required: Set<string>): string[]; | ||
export declare function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string; | ||
@@ -8,0 +9,0 @@ export declare function transformOneOf(oneOf: any, options: TransformSchemaObjOptions): string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.transformSchemaObj = exports.transformOneOf = exports.transformAnyOf = exports.transformSchemaObjMap = void 0; | ||
exports.transformSchemaObj = exports.transformOneOf = exports.transformAnyOf = exports.addRequiredProps = exports.transformSchemaObjMap = void 0; | ||
const utils_1 = require("../utils"); | ||
@@ -25,2 +25,14 @@ function hasDefaultValue(node) { | ||
exports.transformSchemaObjMap = transformSchemaObjMap; | ||
function addRequiredProps(properties, required) { | ||
const missingRequired = [...required].filter((r) => !(r in properties)); | ||
if (missingRequired.length == 0) { | ||
return []; | ||
} | ||
let output = ""; | ||
for (const r of missingRequired) { | ||
output += `${r}: unknown;\n`; | ||
} | ||
return [`{\n${output}}`]; | ||
} | ||
exports.addRequiredProps = addRequiredProps; | ||
function transformAnyOf(anyOf, options) { | ||
@@ -82,6 +94,8 @@ const schemas = anyOf.filter((s) => { | ||
const isAnyOfOrOneOfOrAllOf = "anyOf" in node || "oneOf" in node || "allOf" in node; | ||
const missingRequired = addRequiredProps(node.properties || {}, node.required || []); | ||
if (!isAnyOfOrOneOfOrAllOf && | ||
(!node.properties || !Object.keys(node.properties).length) && | ||
!node.additionalProperties) { | ||
output += `{ ${readonly}[key: string]: any }`; | ||
const emptyObj = `{ ${readonly}[key: string]: unknown }`; | ||
output += utils_1.tsIntersectionOf([emptyObj, ...missingRequired]); | ||
break; | ||
@@ -118,2 +132,3 @@ } | ||
...(properties ? [`{\n${properties}\n}`] : []), | ||
...missingRequired, | ||
...(additionalProperties ? [additionalProperties] : []), | ||
@@ -120,0 +135,0 @@ ]); |
@@ -5,3 +5,4 @@ import path from "path"; | ||
import parserTypescript from "prettier/parser-typescript"; | ||
import load, { resolveSchema } from "./load"; | ||
import { URL } from "url"; | ||
import load, { resolveSchema, VIRTUAL_JSON_URL } from "./load"; | ||
import { swaggerVersion } from "./utils"; | ||
@@ -29,2 +30,3 @@ import { transformAll } from "./transform/index"; | ||
let external = {}; | ||
const allSchemas = {}; | ||
if (typeof schema === "string") { | ||
@@ -34,14 +36,13 @@ const schemaURL = resolveSchema(schema); | ||
console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`)); | ||
const schemas = {}; | ||
await load(schemaURL, { | ||
...ctx, | ||
schemas, | ||
schemas: allSchemas, | ||
rootURL: schemaURL, | ||
}); | ||
for (const k of Object.keys(schemas)) { | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === schemaURL.href) { | ||
rootSchema = schemas[k]; | ||
rootSchema = allSchemas[k]; | ||
} | ||
else { | ||
external[k] = schemas[k]; | ||
external[k] = allSchemas[k]; | ||
} | ||
@@ -51,3 +52,11 @@ } | ||
else { | ||
rootSchema = schema; | ||
await load(schema, { ...ctx, schemas: allSchemas, rootURL: new URL(VIRTUAL_JSON_URL) }); | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === VIRTUAL_JSON_URL) { | ||
rootSchema = allSchemas[k]; | ||
} | ||
else { | ||
external[k] = allSchemas[k]; | ||
} | ||
} | ||
} | ||
@@ -54,0 +63,0 @@ let output = WARNING_MESSAGE; |
@@ -5,12 +5,14 @@ /// <reference types="node" /> | ||
declare type PartialSchema = Record<string, any>; | ||
declare type SchemaMap = { | ||
[url: string]: PartialSchema; | ||
}; | ||
export declare const VIRTUAL_JSON_URL = "file:///_json"; | ||
export declare function resolveSchema(url: string): URL; | ||
interface LoadOptions extends GlobalContext { | ||
rootURL: URL; | ||
schemas: { | ||
[url: string]: PartialSchema; | ||
}; | ||
schemas: SchemaMap; | ||
} | ||
export default function load(schemaURL: URL, options: LoadOptions): Promise<{ | ||
export default function load(schema: URL | PartialSchema, options: LoadOptions): Promise<{ | ||
[url: string]: PartialSchema; | ||
}>; | ||
export {}; |
@@ -5,5 +5,7 @@ import fs from "fs"; | ||
import fetch, { Headers } from "node-fetch"; | ||
import slash from "slash"; | ||
import mime from "mime"; | ||
import yaml from "js-yaml"; | ||
import { parseRef } from "./utils"; | ||
export const VIRTUAL_JSON_URL = `file:///_json`; | ||
function parseSchema(schema, type) { | ||
@@ -34,3 +36,5 @@ if (type === "YAML") { | ||
} | ||
const localPath = path.isAbsolute(url) ? new URL("", `file://${url}`) : new URL(url, `file://${process.cwd()}/`); | ||
const localPath = path.isAbsolute(url) | ||
? new URL("", `file://${slash(url)}`) | ||
: new URL(url, `file://${slash(process.cwd())}/`); | ||
if (!fs.existsSync(localPath)) { | ||
@@ -45,41 +49,50 @@ throw new Error(`Could not locate ${url}`); | ||
let urlCache = new Set(); | ||
export default async function load(schemaURL, options) { | ||
if (urlCache.has(schemaURL.href)) | ||
return options.schemas; | ||
urlCache.add(schemaURL.href); | ||
export default async function load(schema, options) { | ||
const isJSON = schema instanceof URL === false; | ||
let schemaID = isJSON ? new URL(VIRTUAL_JSON_URL).href : schema.href; | ||
const schemas = options.schemas; | ||
let contents = ""; | ||
let contentType = ""; | ||
if (isFile(schemaURL)) { | ||
contents = await fs.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime.getType(schemaURL.href) || ""; | ||
if (isJSON) { | ||
schemas[schemaID] = schema; | ||
} | ||
else { | ||
const headers = new Headers(); | ||
if (options.auth) | ||
headers.set("Authorization", options.auth); | ||
const res = await fetch(schemaURL.href, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
} | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
} | ||
else if (isJSON) { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
} | ||
else { | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
if (urlCache.has(schemaID)) | ||
return options.schemas; | ||
urlCache.add(schemaID); | ||
let contents = ""; | ||
let contentType = ""; | ||
const schemaURL = schema; | ||
if (isFile(schemaURL)) { | ||
contents = await fs.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime.getType(schemaID) || ""; | ||
} | ||
catch (err1) { | ||
else { | ||
const headers = new Headers(); | ||
headers.set("User-Agent", "openapi-typescript"); | ||
if (options.auth) | ||
headers.set("Authorization", options.auth); | ||
const res = await fetch(schemaID, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
} | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} | ||
else if (isJSON) { | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} | ||
else { | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} | ||
catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); | ||
catch (err1) { | ||
try { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} | ||
catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); | ||
} | ||
} | ||
@@ -89,3 +102,3 @@ } | ||
const refPromises = []; | ||
schemas[schemaURL.href] = JSON.parse(JSON.stringify(schemas[schemaURL.href]), (k, v) => { | ||
schemas[schemaID] = JSON.parse(JSON.stringify(schemas[schemaID]), (k, v) => { | ||
if (k !== "$ref" || typeof v !== "string") | ||
@@ -95,3 +108,7 @@ return v; | ||
if (refURL) { | ||
const nextURL = refURL.startsWith("http://") || refURL.startsWith("https://") ? new URL(refURL) : new URL(refURL, schemaURL); | ||
const isRemoteURL = refURL.startsWith("http://") || refURL.startsWith("https://"); | ||
if (isJSON && !isRemoteURL) { | ||
throw new Error(`Can’t load URL "${refURL}" from dynamic JSON. Load this schema from a URL instead.`); | ||
} | ||
const nextURL = isRemoteURL ? new URL(refURL) : new URL(slash(refURL), schema); | ||
refPromises.push(load(nextURL, options).then((subschemas) => { | ||
@@ -107,3 +124,3 @@ for (const subschemaURL of Object.keys(subschemas)) { | ||
await Promise.all(refPromises); | ||
if (schemaURL.href === options.rootURL.href) { | ||
if (schemaID === options.rootURL.href) { | ||
for (const subschemaURL of Object.keys(schemas)) { | ||
@@ -110,0 +127,0 @@ schemas[subschemaURL] = JSON.parse(JSON.stringify(schemas[subschemaURL]), (k, v) => { |
@@ -6,2 +6,3 @@ import { GlobalContext } from "../types"; | ||
export declare function transformSchemaObjMap(obj: Record<string, any>, options: TransformSchemaObjOptions): string; | ||
export declare function addRequiredProps(properties: Record<string, any>, required: Set<string>): string[]; | ||
export declare function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string; | ||
@@ -8,0 +9,0 @@ export declare function transformOneOf(oneOf: any, options: TransformSchemaObjOptions): string; |
@@ -21,2 +21,13 @@ import { comment, nodeType, tsArrayOf, tsIntersectionOf, tsPartial, tsReadonly, tsTupleOf, tsUnionOf } from "../utils"; | ||
} | ||
export function addRequiredProps(properties, required) { | ||
const missingRequired = [...required].filter((r) => !(r in properties)); | ||
if (missingRequired.length == 0) { | ||
return []; | ||
} | ||
let output = ""; | ||
for (const r of missingRequired) { | ||
output += `${r}: unknown;\n`; | ||
} | ||
return [`{\n${output}}`]; | ||
} | ||
export function transformAnyOf(anyOf, options) { | ||
@@ -75,6 +86,8 @@ const schemas = anyOf.filter((s) => { | ||
const isAnyOfOrOneOfOrAllOf = "anyOf" in node || "oneOf" in node || "allOf" in node; | ||
const missingRequired = addRequiredProps(node.properties || {}, node.required || []); | ||
if (!isAnyOfOrOneOfOrAllOf && | ||
(!node.properties || !Object.keys(node.properties).length) && | ||
!node.additionalProperties) { | ||
output += `{ ${readonly}[key: string]: any }`; | ||
const emptyObj = `{ ${readonly}[key: string]: unknown }`; | ||
output += tsIntersectionOf([emptyObj, ...missingRequired]); | ||
break; | ||
@@ -111,2 +124,3 @@ } | ||
...(properties ? [`{\n${properties}\n}`] : []), | ||
...missingRequired, | ||
...(additionalProperties ? [additionalProperties] : []), | ||
@@ -113,0 +127,0 @@ ]); |
{ | ||
"name": "openapi-typescript", | ||
"description": "Generate TypeScript types from Swagger OpenAPI specs", | ||
"version": "4.0.1", | ||
"version": "4.0.2", | ||
"engines": { | ||
"node": ">= 10.0.0" | ||
"node": ">= 12.0.0" | ||
}, | ||
@@ -47,8 +47,8 @@ "author": "drew@pow.rs", | ||
"build": "rm -rf dist && tsc --build tsconfig.json && tsc --build tsconfig.cjs.json", | ||
"format": "yarn prettier -w .", | ||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts src", | ||
"format": "npm run prettier -w .", | ||
"lint": "eslint .", | ||
"prepare": "npm run build", | ||
"pregenerate": "npm run build", | ||
"test": "npm run build && jest --no-cache --test-timeout=10000", | ||
"test:coverage": "npm run build && jest --no-cache --test-timeout=10000 --coverage && codecov", | ||
"test": "npm run build && jest --no-cache", | ||
"test:coverage": "npm run build && jest --no-cache --coverage && codecov", | ||
"typecheck": "tsc --noEmit", | ||
@@ -65,2 +65,3 @@ "version": "npm run build" | ||
"prettier": "^2.3.1", | ||
"slash": "^3.0.0", | ||
"tiny-glob": "^0.2.9" | ||
@@ -67,0 +68,0 @@ }, |
@@ -6,3 +6,3 @@ [![version(scoped)](https://img.shields.io/npm/v/openapi-typescript.svg)](https://www.npmjs.com/package/openapi-typescript) | ||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> | ||
[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) | ||
[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) | ||
<!-- ALL-CONTRIBUTORS-BADGE:END --> | ||
@@ -245,2 +245,4 @@ | ||
<td align="center"><a href="https://github.com/sharmarajdaksh"><img src="https://avatars.githubusercontent.com/u/33689528?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dakshraj Sharma</b></sub></a><br /><a href="https://github.com/drwpow/openapi-typescript/commits?author=sharmarajdaksh" title="Code">💻</a></td> | ||
<td align="center"><a href="https://github.com/shuluster"><img src="https://avatars.githubusercontent.com/u/1707910?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shaosu Liu</b></sub></a><br /><a href="https://github.com/drwpow/openapi-typescript/commits?author=shuluster" title="Code">💻</a></td> | ||
<td align="center"><a href="https://vytenis.kuciauskas.lt"><img src="https://avatars.githubusercontent.com/u/468006?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vytenis</b></sub></a><br /><a href="https://github.com/drwpow/openapi-typescript/commits?author=FDiskas" title="Code">💻</a></td> | ||
</tr> | ||
@@ -247,0 +249,0 @@ </table> |
@@ -5,3 +5,4 @@ import path from "path"; | ||
import parserTypescript from "prettier/parser-typescript"; | ||
import load, { resolveSchema } from "./load"; | ||
import { URL } from "url"; | ||
import load, { resolveSchema, VIRTUAL_JSON_URL } from "./load"; | ||
import { swaggerVersion } from "./utils"; | ||
@@ -39,20 +40,27 @@ import { transformAll } from "./transform/index"; | ||
let external: Record<string, Record<string, any>> = {}; | ||
const allSchemas: Record<string, Record<string, any>> = {}; | ||
if (typeof schema === "string") { | ||
const schemaURL = resolveSchema(schema); | ||
if (options.silent === false) console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`)); | ||
const schemas: Record<string, Record<string, any>> = {}; | ||
await load(schemaURL, { | ||
...ctx, | ||
schemas, | ||
schemas: allSchemas, | ||
rootURL: schemaURL, // as it crawls schemas recursively, it needs to know which is the root to resolve everything relative to | ||
}); | ||
for (const k of Object.keys(schemas)) { | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === schemaURL.href) { | ||
rootSchema = schemas[k]; | ||
rootSchema = allSchemas[k]; | ||
} else { | ||
external[k] = schemas[k]; | ||
external[k] = allSchemas[k]; | ||
} | ||
} | ||
} else { | ||
rootSchema = schema; | ||
await load(schema, { ...ctx, schemas: allSchemas, rootURL: new URL(VIRTUAL_JSON_URL) }); | ||
for (const k of Object.keys(allSchemas)) { | ||
if (k === VIRTUAL_JSON_URL) { | ||
rootSchema = allSchemas[k]; | ||
} else { | ||
external[k] = allSchemas[k]; | ||
} | ||
} | ||
} | ||
@@ -59,0 +67,0 @@ |
107
src/load.ts
@@ -5,2 +5,3 @@ import fs from "fs"; | ||
import fetch, { Headers } from "node-fetch"; | ||
import slash from "slash"; | ||
import mime from "mime"; | ||
@@ -12,3 +13,6 @@ import yaml from "js-yaml"; | ||
type PartialSchema = Record<string, any>; // 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 }; | ||
export const VIRTUAL_JSON_URL = `file:///_json`; // fake URL reserved for dynamic JSON | ||
function parseSchema(schema: any, type: "YAML" | "JSON") { | ||
@@ -41,3 +45,5 @@ if (type === "YAML") { | ||
// option 2: local | ||
const localPath = path.isAbsolute(url) ? new URL("", `file://${url}`) : new URL(url, `file://${process.cwd()}/`); // if absolute path is provided use that; otherwise search cwd\ | ||
const localPath = path.isAbsolute(url) | ||
? new URL("", `file://${slash(url)}`) | ||
: new URL(url, `file://${slash(process.cwd())}/`); // if absolute path is provided use that; otherwise search cwd\ | ||
if (!fs.existsSync(localPath)) { | ||
@@ -53,3 +59,3 @@ throw new Error(`Could not locate ${url}`); | ||
rootURL: URL; | ||
schemas: { [url: string]: PartialSchema }; | ||
schemas: SchemaMap; | ||
} | ||
@@ -61,42 +67,57 @@ | ||
/** Load a schema from local path or remote URL */ | ||
export default async function load(schemaURL: URL, options: LoadOptions): Promise<{ [url: string]: PartialSchema }> { | ||
if (urlCache.has(schemaURL.href)) return options.schemas; // exit early if this has already been scanned | ||
urlCache.add(schemaURL.href); // add URL to cache | ||
export default async function load( | ||
schema: URL | PartialSchema, | ||
options: LoadOptions | ||
): Promise<{ [url: string]: PartialSchema }> { | ||
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; | ||
const schemas = options.schemas; | ||
let contents = ""; | ||
let contentType = ""; | ||
if (isFile(schemaURL)) { | ||
// load local | ||
contents = await fs.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime.getType(schemaURL.href) || ""; | ||
} else { | ||
// load remote | ||
const headers = new Headers(); | ||
if (options.auth) headers.set("Authorization", options.auth); | ||
const res = await fetch(schemaURL.href, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
// scenario 1: load schema from dynamic JSON | ||
if (isJSON) { | ||
schemas[schemaID] = schema; | ||
} | ||
// scenario 2: fetch schema from URL (local or remote) | ||
else { | ||
if (urlCache.has(schemaID)) return options.schemas; // exit early if this has already been scanned | ||
urlCache.add(schemaID); // add URL to cache | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = | ||
contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
} else if (isJSON) { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
} else { | ||
// if contentType is unknown, guess | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "JSON"); | ||
} catch (err1) { | ||
let contents = ""; | ||
let contentType = ""; | ||
const schemaURL = schema as URL; // helps TypeScript | ||
if (isFile(schemaURL)) { | ||
// load local | ||
contents = await fs.promises.readFile(schemaURL, "utf8"); | ||
contentType = mime.getType(schemaID) || ""; | ||
} else { | ||
// load remote | ||
const headers = new Headers(); | ||
headers.set("User-Agent", "openapi-typescript"); | ||
if (options.auth) headers.set("Authorization", options.auth); | ||
const res = await fetch(schemaID, { method: "GET", headers }); | ||
contentType = res.headers.get("Content-Type") || ""; | ||
contents = await res.text(); | ||
} | ||
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml"; | ||
const isJSON = | ||
contentType === "application/json" || | ||
contentType === "application/json5" || | ||
contentType === "application/openapi+json"; | ||
if (isYAML) { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} else if (isJSON) { | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} else { | ||
// if contentType is unknown, guess | ||
try { | ||
schemas[schemaURL.href] = parseSchema(contents, "YAML"); | ||
} catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); // give up: unknown type | ||
schemas[schemaID] = parseSchema(contents, "JSON"); | ||
} catch (err1) { | ||
try { | ||
schemas[schemaID] = parseSchema(contents, "YAML"); | ||
} catch (err2) { | ||
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); // give up: unknown type | ||
} | ||
} | ||
@@ -108,3 +129,3 @@ } | ||
const refPromises: Promise<any>[] = []; | ||
schemas[schemaURL.href] = JSON.parse(JSON.stringify(schemas[schemaURL.href]), (k, v) => { | ||
schemas[schemaID] = JSON.parse(JSON.stringify(schemas[schemaID]), (k, v) => { | ||
if (k !== "$ref" || typeof v !== "string") return v; | ||
@@ -115,4 +136,10 @@ | ||
// load $refs (only if new) and merge subschemas with top-level schema | ||
const nextURL = | ||
refURL.startsWith("http://") || refURL.startsWith("https://") ? new URL(refURL) : new URL(refURL, schemaURL); | ||
const isRemoteURL = refURL.startsWith("http://") || refURL.startsWith("https://"); | ||
// if this is dynamic JSON, we have no idea how to resolve relative URLs, so throw here | ||
if (isJSON && !isRemoteURL) { | ||
throw new Error(`Can’t load URL "${refURL}" from dynamic JSON. Load this schema from a URL instead.`); | ||
} | ||
const nextURL = isRemoteURL ? new URL(refURL) : new URL(slash(refURL), schema as URL); | ||
refPromises.push( | ||
@@ -132,3 +159,3 @@ load(nextURL, options).then((subschemas) => { | ||
// transform $refs once, at the root schema, after all have been scanned & downloaded (much easier to do here when we have the context) | ||
if (schemaURL.href === options.rootURL.href) { | ||
if (schemaID === options.rootURL.href) { | ||
for (const subschemaURL of Object.keys(schemas)) { | ||
@@ -135,0 +162,0 @@ // transform $refs in schema |
@@ -40,2 +40,15 @@ import { GlobalContext } from "../types"; | ||
/** make sure all required fields exist **/ | ||
export function addRequiredProps(properties: Record<string, any>, required: Set<string>): string[] { | ||
const missingRequired = [...required].filter((r: string) => !(r in properties)); | ||
if (missingRequired.length == 0) { | ||
return []; | ||
} | ||
let output = ""; | ||
for (const r of missingRequired) { | ||
output += `${r}: unknown;\n`; | ||
} | ||
return [`{\n${output}}`]; | ||
} | ||
/** transform anyOf */ | ||
@@ -103,3 +116,3 @@ export function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string { | ||
const isAnyOfOrOneOfOrAllOf = "anyOf" in node || "oneOf" in node || "allOf" in node; | ||
const missingRequired = addRequiredProps(node.properties || {}, node.required || []); | ||
// if empty object, then return generic map type | ||
@@ -111,3 +124,5 @@ if ( | ||
) { | ||
output += `{ ${readonly}[key: string]: any }`; | ||
const emptyObj = `{ ${readonly}[key: string]: unknown }`; | ||
output += tsIntersectionOf([emptyObj, ...missingRequired]); | ||
break; | ||
@@ -150,2 +165,3 @@ } | ||
...(properties ? [`{\n${properties}\n}`] : []), // then properties (line breaks are important!) | ||
...missingRequired, // add required that are missing from properties | ||
...(additionalProperties ? [additionalProperties] : []), // then additional properties | ||
@@ -152,0 +168,0 @@ ]); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
232735
3430
256
9
+ Addedslash@^3.0.0
+ Addedslash@3.0.0(transitive)