Skip to content

Commit e173ccf

Browse files
authored
Fix remote schema maps (#1212)
1 parent 15f28df commit e173ccf

File tree

8 files changed

+496
-40
lines changed

8 files changed

+496
-40
lines changed

.changeset/gold-bugs-chew.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Fix bug with remote schema $refs

packages/openapi-typescript/examples/digital-ocean-api.ts

+418-9
Large diffs are not rendered by default.

packages/openapi-typescript/src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ async function openapiTS(schema: string | URL | OpenAPI3 | Readable, options: Op
174174
subschemaOutput = transformResponseObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
175175
break;
176176
}
177+
case "SchemaMap": {
178+
subschemaOutput += "{\n";
179+
indentLv++;
180+
for (const [name, schemaObject] of getEntries(subschema.schema!)) {
181+
const c = getSchemaObjectComment(schemaObject, indentLv);
182+
if (c) subschemaOutput += indent(c, indentLv);
183+
subschemaOutput += indent(`${escObjKey(name)}: ${transformSchemaObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv);
184+
}
185+
indentLv--;
186+
subschemaOutput += indent("};", indentLv);
187+
break;
188+
}
177189
case "SchemaObject": {
178190
subschemaOutput = transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
179191
break;

packages/openapi-typescript/src/load.ts

+45-31
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ export default async function load(schema: URL | Subschema | Readable, options:
217217

218218
// hints help external partial schemas pick up where the root left off (for external complete/valid schemas, skip this)
219219
const isRemoteFullSchema = ref.path[0] === "paths" || ref.path[0] === "components"; // if the initial ref is "paths" or "components" this must be a full schema
220-
const hint = isRemoteFullSchema ? "OpenAPI3" : getHint([...nodePath, ...ref.path], options.hint);
220+
const hintPath: string[] = [...(nodePath as string[])];
221+
if (ref.filename) hintPath.push(ref.filename);
222+
hintPath.push(...ref.path);
223+
const hint = isRemoteFullSchema ? "OpenAPI3" : getHint({ path: hintPath, external: !!ref.filename, startFrom: options.hint });
221224

222225
// if root schema is remote and this is a relative reference, treat as remote
223226
if (schema instanceof URL) {
@@ -306,105 +309,116 @@ function relativePath(src: URL, dest: URL): string {
306309
return dest.href;
307310
}
308311

312+
export interface GetHintOptions {
313+
path: string[];
314+
external: boolean;
315+
startFrom?: Subschema["hint"];
316+
}
317+
309318
/** given a path array (an array of indices), what type of object is this? */
310-
export function getHint(path: (string | number)[], startFrom?: Subschema["hint"]): Subschema["hint"] | undefined {
319+
export function getHint({ path, external, startFrom }: GetHintOptions): Subschema["hint"] | undefined {
311320
if (startFrom && startFrom !== "OpenAPI3") {
312321
switch (startFrom) {
313322
case "OperationObject":
314-
return getHintFromOperationObject(path);
323+
return getHintFromOperationObject(path, external);
315324
case "RequestBodyObject":
316-
return getHintFromRequestBodyObject(path);
325+
return getHintFromRequestBodyObject(path, external);
317326
case "ResponseObject":
318-
return getHintFromResponseObject(path);
327+
return getHintFromResponseObject(path, external);
319328
default:
320329
return startFrom;
321330
}
322331
}
323332
switch (path[0] as keyof OpenAPI3) {
324333
case "paths":
325-
return getHintFromPathItemObject(path.slice(2)); // skip URL at [1]
334+
return getHintFromPathItemObject(path.slice(2), external); // skip URL at [1]
326335
case "components":
327-
return getHintFromComponentsObject(path.slice(1));
336+
return getHintFromComponentsObject(path.slice(1), external);
328337
}
329338
return undefined;
330339
}
331-
function getHintFromComponentsObject(path: (string | number)[]): Subschema["hint"] | undefined {
340+
function getHintFromComponentsObject(path: (string | number)[], external: boolean): Subschema["hint"] | undefined {
332341
switch (path[0] as keyof ComponentsObject) {
333342
case "schemas":
334343
case "headers":
335-
return getHintFromSchemaObject(path.slice(2));
344+
return getHintFromSchemaObject(path.slice(2), external);
336345
case "parameters":
337-
return getHintFromParameterObject(path.slice(2));
346+
return getHintFromParameterObject(path.slice(2), external);
338347
case "responses":
339-
return getHintFromResponseObject(path.slice(2));
348+
return getHintFromResponseObject(path.slice(2), external);
340349
case "requestBodies":
341-
return getHintFromRequestBodyObject(path.slice(2));
350+
return getHintFromRequestBodyObject(path.slice(2), external);
342351
case "pathItems":
343-
return getHintFromPathItemObject(path.slice(2));
352+
return getHintFromPathItemObject(path.slice(2), external);
344353
}
345354
return "SchemaObject";
346355
}
347-
function getHintFromMediaTypeObject(path: (string | number)[]): Subschema["hint"] {
356+
function getHintFromMediaTypeObject(path: (string | number)[], external: boolean): Subschema["hint"] {
348357
switch (path[0]) {
349358
case "schema":
350-
return getHintFromSchemaObject(path.slice(1));
359+
return getHintFromSchemaObject(path.slice(1), external);
351360
}
352361
return "MediaTypeObject";
353362
}
354-
function getHintFromOperationObject(path: (string | number)[]): Subschema["hint"] {
363+
function getHintFromOperationObject(path: (string | number)[], external: boolean): Subschema["hint"] {
355364
switch (path[0] as keyof OperationObject) {
356365
case "parameters":
357366
return "ParameterObject[]";
358367
case "requestBody":
359-
return getHintFromRequestBodyObject(path.slice(1));
368+
return getHintFromRequestBodyObject(path.slice(1), external);
360369
case "responses":
361-
return getHintFromResponseObject(path.slice(2)); // skip the response code at [1]
370+
return getHintFromResponseObject(path.slice(2), external); // skip the response code at [1]
362371
}
363372
return "OperationObject";
364373
}
365-
function getHintFromParameterObject(path: (string | number)[]): Subschema["hint"] {
374+
function getHintFromParameterObject(path: (string | number)[], external: boolean): Subschema["hint"] {
366375
switch (path[0]) {
367376
case "content":
368-
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
377+
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
369378
case "schema":
370-
return getHintFromSchemaObject(path.slice(1));
379+
return getHintFromSchemaObject(path.slice(1), external);
371380
}
372381
return "ParameterObject";
373382
}
374-
function getHintFromPathItemObject(path: (string | number)[]): Subschema["hint"] | undefined {
383+
function getHintFromPathItemObject(path: (string | number)[], external: boolean): Subschema["hint"] | undefined {
375384
switch (path[0] as keyof PathItemObject) {
376385
case "parameters": {
377386
if (typeof path[1] === "number") {
378387
return "ParameterObject[]";
379388
}
380-
return getHintFromParameterObject(path.slice(1));
389+
return getHintFromParameterObject(path.slice(1), external);
381390
}
382391
default:
383-
return getHintFromOperationObject(path.slice(1));
392+
return getHintFromOperationObject(path.slice(1), external);
384393
}
385394
}
386-
function getHintFromRequestBodyObject(path: (string | number)[]): Subschema["hint"] {
395+
function getHintFromRequestBodyObject(path: (string | number)[], external: boolean): Subschema["hint"] {
387396
switch (path[0] as keyof RequestBodyObject) {
388397
case "content":
389-
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
398+
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
390399
}
391400
return "RequestBodyObject";
392401
}
393-
function getHintFromResponseObject(path: (string | number)[]): Subschema["hint"] {
402+
function getHintFromResponseObject(path: (string | number)[], external: boolean): Subschema["hint"] {
394403
switch (path[0] as keyof ResponseObject) {
395404
case "headers":
396-
return getHintFromSchemaObject(path.slice(2)); // skip name at [1]
405+
return getHintFromSchemaObject(path.slice(2), external); // skip name at [1]
397406
case "content":
398-
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
407+
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
399408
}
400409
return "ResponseObject";
401410
}
402-
function getHintFromSchemaObject(path: (string | number)[]): Subschema["hint"] {
411+
function getHintFromSchemaObject(path: (string | number)[], external: boolean): Subschema["hint"] {
403412
switch (path[0]) {
404413
case "allOf":
405414
case "anyOf":
406415
case "oneOf":
407-
return getHintFromSchemaObject(path.slice(2)); // skip array index at [1]
416+
return getHintFromSchemaObject(path.slice(2), external); // skip array index at [1]
417+
}
418+
// if this is external, and the path is [filename, key], then the external schema is probably a SchemaMap
419+
if (path.length === 2 && external) {
420+
return "SchemaMap";
408421
}
422+
// otherwise, path length of 1 means partial schema is likely a SchemaObject (or it’s unknown, in which case assume SchemaObject)
409423
return "SchemaObject";
410424
}

packages/openapi-typescript/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ export type Subschema =
639639
}
640640
| { hint: "RequestBodyObject"; schema: RequestBodyObject }
641641
| { hint: "ResponseObject"; schema: ResponseObject }
642+
| { hint: "SchemaMap"; schema: NonNullable<ComponentsObject["schemas"]> }
642643
| { hint: "SchemaObject"; schema: SchemaObject };
643644

644645
/** Context passed to all submodules */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
PartialType:
2+
type: object
3+
properties:
4+
foo:
5+
type: string
6+
required:
7+
- foo

packages/openapi-typescript/test/fixtures/remote-ref-test.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ components:
1616
schemas:
1717
RemoteType:
1818
$ref: "./remote-ref-test-2.yaml#/components/schemas/SchemaType"
19+
RemotePartialType:
20+
$ref: '_schema-test-partial.yaml#/PartialType'

packages/openapi-typescript/test/index.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export type webhooks = Record<string, never>;
309309
export interface components {
310310
schemas: {
311311
RemoteType: external["remote-ref-test-2.yaml"]["components"]["schemas"]["SchemaType"];
312+
RemotePartialType: external["_schema-test-partial.yaml"]["PartialType"];
312313
};
313314
responses: never;
314315
parameters: never;
@@ -318,6 +319,11 @@ export interface components {
318319
}
319320
320321
export interface external {
322+
"_schema-test-partial.yaml": {
323+
PartialType: {
324+
foo: string;
325+
};
326+
};
321327
"remote-ref-test-2.yaml": {
322328
paths: {
323329
"/": {

0 commit comments

Comments
 (0)