Skip to content

Commit 4a3260a

Browse files
authored
Fix JSON loader (#689)
#638
1 parent ba4251c commit 4a3260a

File tree

4 files changed

+114
-49
lines changed

4 files changed

+114
-49
lines changed

src/index.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import path from "path";
22
import { bold, yellow } from "kleur";
33
import prettier from "prettier";
44
import parserTypescript from "prettier/parser-typescript";
5-
import load, { resolveSchema } from "./load";
5+
import { URL } from "url";
6+
import load, { resolveSchema, VIRTUAL_JSON_URL } from "./load";
67
import { swaggerVersion } from "./utils";
78
import { transformAll } from "./transform/index";
89
import { GlobalContext, OpenAPI2, OpenAPI3, SchemaObject, SwaggerToTSOptions } from "./types";
@@ -35,24 +36,31 @@ export default async function openapiTS(
3536
// 1. load schema
3637
let rootSchema: Record<string, any> = {};
3738
let external: Record<string, Record<string, any>> = {};
39+
const allSchemas: Record<string, Record<string, any>> = {};
3840
if (typeof schema === "string") {
3941
const schemaURL = resolveSchema(schema);
4042
if (options.silent === false) console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`));
41-
const schemas: Record<string, Record<string, any>> = {};
4243
await load(schemaURL, {
4344
...ctx,
44-
schemas,
45+
schemas: allSchemas,
4546
rootURL: schemaURL, // as it crawls schemas recursively, it needs to know which is the root to resolve everything relative to
4647
});
47-
for (const k of Object.keys(schemas)) {
48+
for (const k of Object.keys(allSchemas)) {
4849
if (k === schemaURL.href) {
49-
rootSchema = schemas[k];
50+
rootSchema = allSchemas[k];
5051
} else {
51-
external[k] = schemas[k];
52+
external[k] = allSchemas[k];
5253
}
5354
}
5455
} else {
55-
rootSchema = schema;
56+
await load(schema, { ...ctx, schemas: allSchemas, rootURL: new URL(VIRTUAL_JSON_URL) });
57+
for (const k of Object.keys(allSchemas)) {
58+
if (k === VIRTUAL_JSON_URL) {
59+
rootSchema = allSchemas[k];
60+
} else {
61+
external[k] = allSchemas[k];
62+
}
63+
}
5664
}
5765

5866
// 2. generate raw output

src/load.ts

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { GlobalContext } from "./types";
99
import { parseRef } from "./utils";
1010

1111
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
12+
type SchemaMap = { [url: string]: PartialSchema };
13+
14+
export const VIRTUAL_JSON_URL = `file:///_json`; // fake URL reserved for dynamic JSON
1215

1316
function parseSchema(schema: any, type: "YAML" | "JSON") {
1417
if (type === "YAML") {
@@ -50,70 +53,88 @@ export function resolveSchema(url: string): URL {
5053

5154
interface LoadOptions extends GlobalContext {
5255
rootURL: URL;
53-
schemas: { [url: string]: PartialSchema };
56+
schemas: SchemaMap;
5457
}
5558

5659
// temporary cache for load()
5760
let urlCache = new Set<string>(); // URL cache (prevent URLs from being loaded over and over)
5861

5962
/** Load a schema from local path or remote URL */
60-
export default async function load(schemaURL: URL, options: LoadOptions): Promise<{ [url: string]: PartialSchema }> {
61-
if (urlCache.has(schemaURL.href)) return options.schemas; // exit early if this has already been scanned
62-
urlCache.add(schemaURL.href); // add URL to cache
63+
export default async function load(
64+
schema: URL | PartialSchema,
65+
options: LoadOptions
66+
): Promise<{ [url: string]: PartialSchema }> {
67+
const isJSON = schema instanceof URL === false; // if this is dynamically-passed-in JSON, we’ll have to change a few things
68+
let schemaID = isJSON ? new URL(VIRTUAL_JSON_URL).href : schema.href;
6369

6470
const schemas = options.schemas;
6571

66-
let contents = "";
67-
let contentType = "";
68-
69-
if (isFile(schemaURL)) {
70-
// load local
71-
contents = await fs.promises.readFile(schemaURL, "utf8");
72-
contentType = mime.getType(schemaURL.href) || "";
73-
} else {
74-
// load remote
75-
const headers = new Headers();
76-
headers.set("User-Agent", "openapi-typescript");
77-
if (options.auth) headers.set("Authorization", options.auth);
78-
const res = await fetch(schemaURL.href, { method: "GET", headers });
79-
contentType = res.headers.get("Content-Type") || "";
80-
contents = await res.text();
72+
// scenario 1: load schema from dynamic JSON
73+
if (isJSON) {
74+
schemas[schemaID] = schema;
8175
}
76+
// scenario 2: fetch schema from URL (local or remote)
77+
else {
78+
if (urlCache.has(schemaID)) return options.schemas; // exit early if this has already been scanned
79+
urlCache.add(schemaID); // add URL to cache
80+
81+
let contents = "";
82+
let contentType = "";
83+
const schemaURL = schema as URL; // helps TypeScript
84+
85+
if (isFile(schemaURL)) {
86+
// load local
87+
contents = await fs.promises.readFile(schemaURL, "utf8");
88+
contentType = mime.getType(schemaID) || "";
89+
} else {
90+
// load remote
91+
const headers = new Headers();
92+
headers.set("User-Agent", "openapi-typescript");
93+
if (options.auth) headers.set("Authorization", options.auth);
94+
const res = await fetch(schemaID, { method: "GET", headers });
95+
contentType = res.headers.get("Content-Type") || "";
96+
contents = await res.text();
97+
}
8298

83-
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml";
84-
const isJSON =
85-
contentType === "application/json" ||
86-
contentType === "application/json5" ||
87-
contentType === "application/openapi+json";
88-
if (isYAML) {
89-
schemas[schemaURL.href] = parseSchema(contents, "YAML");
90-
} else if (isJSON) {
91-
schemas[schemaURL.href] = parseSchema(contents, "JSON");
92-
} else {
93-
// if contentType is unknown, guess
94-
try {
95-
schemas[schemaURL.href] = parseSchema(contents, "JSON");
96-
} catch (err1) {
99+
const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml";
100+
const isJSON =
101+
contentType === "application/json" ||
102+
contentType === "application/json5" ||
103+
contentType === "application/openapi+json";
104+
if (isYAML) {
105+
schemas[schemaID] = parseSchema(contents, "YAML");
106+
} else if (isJSON) {
107+
schemas[schemaID] = parseSchema(contents, "JSON");
108+
} else {
109+
// if contentType is unknown, guess
97110
try {
98-
schemas[schemaURL.href] = parseSchema(contents, "YAML");
99-
} catch (err2) {
100-
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); // give up: unknown type
111+
schemas[schemaID] = parseSchema(contents, "JSON");
112+
} catch (err1) {
113+
try {
114+
schemas[schemaID] = parseSchema(contents, "YAML");
115+
} catch (err2) {
116+
throw new Error(`Unknown format${contentType ? `: "${contentType}"` : ""}. Only YAML or JSON supported.`); // give up: unknown type
117+
}
101118
}
102119
}
103120
}
104121

105122
// scan $refs, but don’t transform (load everything in parallel)
106123
const refPromises: Promise<any>[] = [];
107-
schemas[schemaURL.href] = JSON.parse(JSON.stringify(schemas[schemaURL.href]), (k, v) => {
124+
schemas[schemaID] = JSON.parse(JSON.stringify(schemas[schemaID]), (k, v) => {
108125
if (k !== "$ref" || typeof v !== "string") return v;
109126

110127
const { url: refURL } = parseRef(v);
111128
if (refURL) {
112129
// load $refs (only if new) and merge subschemas with top-level schema
113-
const nextURL =
114-
refURL.startsWith("http://") || refURL.startsWith("https://")
115-
? new URL(refURL)
116-
: new URL(slash(refURL), schemaURL);
130+
const isRemoteURL = refURL.startsWith("http://") || refURL.startsWith("https://");
131+
132+
// if this is dynamic JSON, we have no idea how to resolve relative URLs, so throw here
133+
if (isJSON && !isRemoteURL) {
134+
throw new Error(`Can’t load URL "${refURL}" from dynamic JSON. Load this schema from a URL instead.`);
135+
}
136+
137+
const nextURL = isRemoteURL ? new URL(refURL) : new URL(slash(refURL), schema as URL);
117138
refPromises.push(
118139
load(nextURL, options).then((subschemas) => {
119140
for (const subschemaURL of Object.keys(subschemas)) {
@@ -128,7 +149,7 @@ export default async function load(schemaURL: URL, options: LoadOptions): Promis
128149
await Promise.all(refPromises);
129150

130151
// transform $refs once, at the root schema, after all have been scanned & downloaded (much easier to do here when we have the context)
131-
if (schemaURL.href === options.rootURL.href) {
152+
if (schemaID === options.rootURL.href) {
132153
for (const subschemaURL of Object.keys(schemas)) {
133154
// transform $refs in schema
134155
schemas[subschemaURL] = JSON.parse(JSON.stringify(schemas[subschemaURL]), (k, v) => {

tests/v2/index.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { execSync } from "child_process";
22
import fs from "fs";
33
import path from "path";
4+
import yaml from "js-yaml";
45
import { sanitizeLB } from "../test-utils";
6+
import openapiTS from "../../src/index";
57

68
const cmd = `node ../../bin/cli.js`;
79
const schemas = fs.readdirSync(path.join(__dirname, "specs"));
@@ -49,3 +51,19 @@ describe("cli", () => {
4951
expect(generated).toBe(sanitizeLB(expected));
5052
});
5153
});
54+
55+
describe("json", () => {
56+
schemas.forEach((schema) => {
57+
it(`reads ${schema} from JSON`, async () => {
58+
const [schemaYAML, expected] = await Promise.all([
59+
fs.promises.readFile(path.join(__dirname, "specs", schema), "utf8"),
60+
fs.promises.readFile(path.join(__dirname, "expected", schema.replace(".yaml", ".ts")), "utf8"),
61+
]);
62+
const schemaJSON = yaml.load(schemaYAML) as any;
63+
const generated = await openapiTS(schemaJSON, {
64+
prettierConfig: path.join(__dirname, "..", "..", ".prettierrc"),
65+
});
66+
expect(generated).toBe(sanitizeLB(expected));
67+
});
68+
});
69+
});

tests/v3/index.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { execSync } from "child_process";
22
import fs from "fs";
33
import path from "path";
4+
import yaml from "js-yaml";
45
import { sanitizeLB } from "../test-utils";
6+
import openapiTS from "../../src/index";
57

68
const cmd = `node ../../bin/cli.js`;
79
const schemas = fs.readdirSync(path.join(__dirname, "specs"));
@@ -60,3 +62,19 @@ describe("cli", () => {
6062
expect(generated).toBe(sanitizeLB(expected));
6163
});
6264
});
65+
66+
describe("json", () => {
67+
schemas.forEach((schema) => {
68+
it(`reads ${schema} from JSON`, async () => {
69+
const [schemaYAML, expected] = await Promise.all([
70+
fs.promises.readFile(path.join(__dirname, "specs", schema), "utf8"),
71+
fs.promises.readFile(path.join(__dirname, "expected", schema.replace(".yaml", ".ts")), "utf8"),
72+
]);
73+
const schemaJSON = yaml.load(schemaYAML) as any;
74+
const generated = await openapiTS(schemaJSON, {
75+
prettierConfig: path.join(__dirname, "..", "..", ".prettierrc"),
76+
});
77+
expect(generated).toBe(sanitizeLB(expected));
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)