Skip to content

Fix remote schema maps #1212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-bugs-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

Fix bug with remote schema $refs
427 changes: 418 additions & 9 deletions packages/openapi-typescript/examples/digital-ocean-api.ts

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ async function openapiTS(schema: string | URL | OpenAPI3 | Readable, options: Op
subschemaOutput = transformResponseObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
break;
}
case "SchemaMap": {
subschemaOutput += "{\n";
indentLv++;
for (const [name, schemaObject] of getEntries(subschema.schema!)) {
const c = getSchemaObjectComment(schemaObject, indentLv);
if (c) subschemaOutput += indent(c, indentLv);
subschemaOutput += indent(`${escObjKey(name)}: ${transformSchemaObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv);
}
indentLv--;
subschemaOutput += indent("};", indentLv);
break;
}
case "SchemaObject": {
subschemaOutput = transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
break;
Expand Down
76 changes: 45 additions & 31 deletions packages/openapi-typescript/src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ export default async function load(schema: URL | Subschema | Readable, options:

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

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

export interface GetHintOptions {
path: string[];
external: boolean;
startFrom?: Subschema["hint"];
}

/** given a path array (an array of indices), what type of object is this? */
export function getHint(path: (string | number)[], startFrom?: Subschema["hint"]): Subschema["hint"] | undefined {
export function getHint({ path, external, startFrom }: GetHintOptions): Subschema["hint"] | undefined {
if (startFrom && startFrom !== "OpenAPI3") {
switch (startFrom) {
case "OperationObject":
return getHintFromOperationObject(path);
return getHintFromOperationObject(path, external);
case "RequestBodyObject":
return getHintFromRequestBodyObject(path);
return getHintFromRequestBodyObject(path, external);
case "ResponseObject":
return getHintFromResponseObject(path);
return getHintFromResponseObject(path, external);
default:
return startFrom;
}
}
switch (path[0] as keyof OpenAPI3) {
case "paths":
return getHintFromPathItemObject(path.slice(2)); // skip URL at [1]
return getHintFromPathItemObject(path.slice(2), external); // skip URL at [1]
case "components":
return getHintFromComponentsObject(path.slice(1));
return getHintFromComponentsObject(path.slice(1), external);
}
return undefined;
}
function getHintFromComponentsObject(path: (string | number)[]): Subschema["hint"] | undefined {
function getHintFromComponentsObject(path: (string | number)[], external: boolean): Subschema["hint"] | undefined {
switch (path[0] as keyof ComponentsObject) {
case "schemas":
case "headers":
return getHintFromSchemaObject(path.slice(2));
return getHintFromSchemaObject(path.slice(2), external);
case "parameters":
return getHintFromParameterObject(path.slice(2));
return getHintFromParameterObject(path.slice(2), external);
case "responses":
return getHintFromResponseObject(path.slice(2));
return getHintFromResponseObject(path.slice(2), external);
case "requestBodies":
return getHintFromRequestBodyObject(path.slice(2));
return getHintFromRequestBodyObject(path.slice(2), external);
case "pathItems":
return getHintFromPathItemObject(path.slice(2));
return getHintFromPathItemObject(path.slice(2), external);
}
return "SchemaObject";
}
function getHintFromMediaTypeObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromMediaTypeObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0]) {
case "schema":
return getHintFromSchemaObject(path.slice(1));
return getHintFromSchemaObject(path.slice(1), external);
}
return "MediaTypeObject";
}
function getHintFromOperationObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromOperationObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0] as keyof OperationObject) {
case "parameters":
return "ParameterObject[]";
case "requestBody":
return getHintFromRequestBodyObject(path.slice(1));
return getHintFromRequestBodyObject(path.slice(1), external);
case "responses":
return getHintFromResponseObject(path.slice(2)); // skip the response code at [1]
return getHintFromResponseObject(path.slice(2), external); // skip the response code at [1]
}
return "OperationObject";
}
function getHintFromParameterObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromParameterObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0]) {
case "content":
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
case "schema":
return getHintFromSchemaObject(path.slice(1));
return getHintFromSchemaObject(path.slice(1), external);
}
return "ParameterObject";
}
function getHintFromPathItemObject(path: (string | number)[]): Subschema["hint"] | undefined {
function getHintFromPathItemObject(path: (string | number)[], external: boolean): Subschema["hint"] | undefined {
switch (path[0] as keyof PathItemObject) {
case "parameters": {
if (typeof path[1] === "number") {
return "ParameterObject[]";
}
return getHintFromParameterObject(path.slice(1));
return getHintFromParameterObject(path.slice(1), external);
}
default:
return getHintFromOperationObject(path.slice(1));
return getHintFromOperationObject(path.slice(1), external);
}
}
function getHintFromRequestBodyObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromRequestBodyObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0] as keyof RequestBodyObject) {
case "content":
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
}
return "RequestBodyObject";
}
function getHintFromResponseObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromResponseObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0] as keyof ResponseObject) {
case "headers":
return getHintFromSchemaObject(path.slice(2)); // skip name at [1]
return getHintFromSchemaObject(path.slice(2), external); // skip name at [1]
case "content":
return getHintFromMediaTypeObject(path.slice(2)); // skip content type at [1]
return getHintFromMediaTypeObject(path.slice(2), external); // skip content type at [1]
}
return "ResponseObject";
}
function getHintFromSchemaObject(path: (string | number)[]): Subschema["hint"] {
function getHintFromSchemaObject(path: (string | number)[], external: boolean): Subschema["hint"] {
switch (path[0]) {
case "allOf":
case "anyOf":
case "oneOf":
return getHintFromSchemaObject(path.slice(2)); // skip array index at [1]
return getHintFromSchemaObject(path.slice(2), external); // skip array index at [1]
}
// if this is external, and the path is [filename, key], then the external schema is probably a SchemaMap
if (path.length === 2 && external) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very wacky logic, but it seems to check out 🤷

return "SchemaMap";
}
// otherwise, path length of 1 means partial schema is likely a SchemaObject (or it’s unknown, in which case assume SchemaObject)
return "SchemaObject";
}
1 change: 1 addition & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ export type Subschema =
}
| { hint: "RequestBodyObject"; schema: RequestBodyObject }
| { hint: "ResponseObject"; schema: ResponseObject }
| { hint: "SchemaMap"; schema: NonNullable<ComponentsObject["schemas"]> }
| { hint: "SchemaObject"; schema: SchemaObject };

/** Context passed to all submodules */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
PartialType:
type: object
properties:
foo:
type: string
required:
- foo
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ components:
schemas:
RemoteType:
$ref: "./remote-ref-test-2.yaml#/components/schemas/SchemaType"
RemotePartialType:
$ref: '_schema-test-partial.yaml#/PartialType'
6 changes: 6 additions & 0 deletions packages/openapi-typescript/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ export type webhooks = Record<string, never>;
export interface components {
schemas: {
RemoteType: external["remote-ref-test-2.yaml"]["components"]["schemas"]["SchemaType"];
RemotePartialType: external["_schema-test-partial.yaml"]["PartialType"];
};
responses: never;
parameters: never;
Expand All @@ -318,6 +319,11 @@ export interface components {
}
export interface external {
"_schema-test-partial.yaml": {
PartialType: {
foo: string;
};
};
"remote-ref-test-2.yaml": {
paths: {
"/": {
Expand Down