Skip to content

Commit d3c7588

Browse files
committed
Support $ref into paths
1 parent f151f44 commit d3c7588

File tree

6 files changed

+117
-36
lines changed

6 files changed

+117
-36
lines changed

packages/openapi-typescript/src/lib/ts.ts

+71-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
2+
import { Referenced, OasRef} from "@redocly/openapi-core";
23
import ts, { type LiteralTypeNode, type TypeLiteralNode } from "typescript";
4+
import { ParameterObject, ReferenceObject } from "../types.js";
35

46
export const JS_PROPERTY_INDEX_RE = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
57
export const JS_ENUM_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+(.)?/g;
@@ -115,33 +117,81 @@ export function addJSDocComment(schemaObject: AnnotatedSchemaObject, node: ts.Pr
115117
}
116118
}
117119

118-
/** Convert OpenAPI ref into TS indexed access node (ex: `components["schemas"]["Foo"]`) */
119-
export function oapiRef(path: string): ts.TypeNode {
120+
function isOasRef<T>(obj: Referenced<T>): obj is OasRef {
121+
return Boolean((obj as OasRef).$ref);
122+
}
123+
type OapiRefResolved = ParameterObject | ReferenceObject;
124+
125+
function isParameterObject(obj: OapiRefResolved | undefined): obj is ParameterObject {
126+
if (!obj) {
127+
return false;
128+
}
129+
130+
return Boolean(!isOasRef(obj) && obj.in);
131+
}
132+
133+
function addIndexedAccess(node: ts.TypeReferenceNode | ts.IndexedAccessTypeNode, ...segments: readonly string[]) {
134+
return segments.reduce((acc, segment) => {
135+
return ts.factory.createIndexedAccessTypeNode(
136+
acc,
137+
ts.factory.createLiteralTypeNode(
138+
typeof segment === "number"
139+
? ts.factory.createNumericLiteral(segment)
140+
: ts.factory.createStringLiteral(segment),
141+
),
142+
);
143+
}, node);
144+
}
145+
146+
/**
147+
* Convert OpenAPI ref into TS indexed access node (ex: `components["schemas"]["Foo"]`)
148+
* `path` is a JSON Pointer to a location within an OpenAPI document.
149+
* Transform it into a TypeScript type reference into the generated types.
150+
*
151+
* In most cases the structures of the openapi-typescript generated types and the
152+
* JSON Pointer paths into the OpenAPI document are the same. However, in some cases
153+
* special transformations are necessary to account for the ways they differ.
154+
* * Object schemas
155+
* $refs into the `properties` of object schemas are valid, but openapi-typescript
156+
* flattens these objects, so we omit so the index into the schema skips ["properties"]
157+
* * Parameters
158+
* $refs into the `parameters` of paths are valid, but openapi-ts represents
159+
* them according to their type; path, query, header, etc… so in these cases we
160+
* must check the parameter definition to determine the how to index into
161+
* the openapi-typescript type.
162+
**/
163+
export function oapiRef(path: string, resolved?: OapiRefResolved): ts.TypeNode {
120164
const { pointer } = parseRef(path);
121165
if (pointer.length === 0) {
122166
throw new Error(`Error parsing $ref: ${path}. Is this a valid $ref?`);
123167
}
124-
let t: ts.TypeReferenceNode | ts.IndexedAccessTypeNode = ts.factory.createTypeReferenceNode(
125-
ts.factory.createIdentifier(String(pointer[0])),
168+
169+
const parametersObject = isParameterObject(resolved);
170+
171+
// Initial segments are handled in a fixed , then remaining segments are treated
172+
// according to heuristics based on the initial segments
173+
const initialSegment = pointer[0];
174+
const leadingSegments = pointer.slice(1, 3);
175+
const restSegments = pointer.slice(3);
176+
177+
const leadingType = addIndexedAccess(
178+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(String(initialSegment))),
179+
...leadingSegments,
126180
);
127-
if (pointer.length > 1) {
128-
for (let i = 1; i < pointer.length; i++) {
129-
// Skip `properties` items when in the middle of the pointer
130-
// See: https://github.com/openapi-ts/openapi-typescript/issues/1742
131-
if (i > 2 && i < pointer.length - 1 && pointer[i] === "properties") {
132-
continue;
133-
}
134-
t = ts.factory.createIndexedAccessTypeNode(
135-
t,
136-
ts.factory.createLiteralTypeNode(
137-
typeof pointer[i] === "number"
138-
? ts.factory.createNumericLiteral(pointer[i])
139-
: ts.factory.createStringLiteral(pointer[i] as string),
140-
),
141-
);
181+
182+
return restSegments.reduce<ts.TypeReferenceNode | ts.IndexedAccessTypeNode>((acc, segment, index, original) => {
183+
// Skip `properties` items when in the middle of the pointer
184+
// See: https://github.com/openapi-ts/openapi-typescript/issues/1742
185+
if (segment === "properties") {
186+
return acc;
142187
}
143-
}
144-
return t;
188+
189+
if (parametersObject && index === original.length - 1) {
190+
return addIndexedAccess(acc, resolved.in, resolved.name);
191+
}
192+
193+
return addIndexedAccess(acc, segment);
194+
}, leadingType);
145195
}
146196

147197
export interface AstToStringOptions {

packages/openapi-typescript/src/transform/parameters-array.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ export function transformParametersArray(
9090
if (paramIn !== "path" && !(resolved as ParameterObject).required) {
9191
optional = QUESTION_TOKEN;
9292
}
93+
9394
const subType =
9495
"$ref" in original
95-
? oapiRef(original.$ref)
96+
? oapiRef(original.$ref, resolved)
9697
: transformParameterObject(resolved as ParameterObject, {
9798
...options,
9899
path: createRef([options.path, "parameters", resolved.in, resolved.name]),

packages/openapi-typescript/test/fixtures/parameters-test.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ paths:
3535
type: string
3636
- $ref: "#/components/parameters/local_ref_b"
3737
- $ref: "./_parameters-test-partial.yaml#/remote_ref_b"
38+
/endpoint2:
39+
parameters:
40+
- $ref: "#/paths/~1endpoint/get/parameters/0"
3841
components:
3942
parameters:
4043
local_ref_a:

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

+19
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,25 @@ export type operations = Record<string, never>;`,
172172
patch?: never;
173173
trace?: never;
174174
};
175+
"/endpoint2": {
176+
parameters: {
177+
query?: never;
178+
header?: never;
179+
path: {
180+
/** @description This overrides parameters */
181+
local_param_a: paths["/endpoint"]["get"]["parameters"]["path"]["local_param_a"];
182+
};
183+
cookie?: never;
184+
};
185+
get?: never;
186+
put?: never;
187+
post?: never;
188+
delete?: never;
189+
options?: never;
190+
head?: never;
191+
patch?: never;
192+
trace?: never;
193+
};
175194
}
176195
export type webhooks = Record<string, never>;
177196
export interface components {

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,23 @@ describe("oapiRef", () => {
6565
expect(astToString(oapiRef("#/components/schemas/User")).trim()).toBe(`components["schemas"]["User"]`);
6666
});
6767

68-
test("removes inner `properties`", () => {
68+
test("`properties` of component schema `properties`", () => {
6969
expect(astToString(oapiRef("#/components/schemas/User/properties/username")).trim()).toBe(
7070
`components["schemas"]["User"]["username"]`,
7171
);
7272
});
7373

74-
test("leaves final `properties` intact", () => {
74+
test("component schema named `properties`", () => {
7575
expect(astToString(oapiRef("#/components/schemas/properties")).trim()).toBe(`components["schemas"]["properties"]`);
7676
});
77+
78+
test("reference into paths parameters" , () => {
79+
expect(astToString(oapiRef("#/paths/~1endpoint/get/parameters/0", {
80+
in: 'query',
81+
name: 'boop',
82+
required: true
83+
})).trim()).toBe('paths["/endpoint"]["get"]["parameters"]["query"]["boop"]')
84+
});
7785
});
7886

7987
describe("tsEnum", () => {

packages/openapi-typescript/test/transform/webhooks-object.test.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ describe("transformWebhooksObject", () => {
105105
schema: { type: "string" },
106106
required: true,
107107
},
108-
{ $ref: "#/components/parameters/query/utm_source" },
109-
{ $ref: "#/components/parameters/query/utm_email" },
110-
{ $ref: "#/components/parameters/query/utm_campaign" },
111-
{ $ref: "#/components/parameters/path/version" },
108+
{ $ref: "#/components/parameters/utm_source" },
109+
{ $ref: "#/components/parameters/utm_email" },
110+
{ $ref: "#/components/parameters/utm_campaign" },
111+
{ $ref: "#/components/parameters/version" },
112112
],
113113
},
114114
},
@@ -117,13 +117,13 @@ describe("transformWebhooksObject", () => {
117117
parameters: {
118118
query: {
119119
signature: string;
120-
utm_source?: components["parameters"]["query"]["utm_source"];
121-
utm_email?: components["parameters"]["query"]["utm_email"];
122-
utm_campaign?: components["parameters"]["query"]["utm_campaign"];
120+
utm_source?: components["parameters"]["utm_source"];
121+
utm_email?: components["parameters"]["utm_email"];
122+
utm_campaign?: components["parameters"]["utm_campaign"];
123123
};
124124
header?: never;
125125
path: {
126-
utm_campaign: components["parameters"]["path"]["version"];
126+
utm_campaign: components["parameters"]["version"];
127127
};
128128
cookie?: never;
129129
};
@@ -141,28 +141,28 @@ describe("transformWebhooksObject", () => {
141141
...DEFAULT_OPTIONS,
142142
resolve($ref) {
143143
switch ($ref) {
144-
case "#/components/parameters/query/utm_source": {
144+
case "#/components/parameters/utm_source": {
145145
return {
146146
in: "query",
147147
name: "utm_source",
148148
schema: { type: "string" },
149149
};
150150
}
151-
case "#/components/parameters/query/utm_email": {
151+
case "#/components/parameters/utm_email": {
152152
return {
153153
in: "query",
154154
name: "utm_email",
155155
schema: { type: "string" },
156156
};
157157
}
158-
case "#/components/parameters/query/utm_campaign": {
158+
case "#/components/parameters/utm_campaign": {
159159
return {
160160
in: "query",
161161
name: "utm_campaign",
162162
schema: { type: "string" },
163163
};
164164
}
165-
case "#/components/parameters/path/version": {
165+
case "#/components/parameters/version": {
166166
return {
167167
in: "path",
168168
name: "utm_campaign",

0 commit comments

Comments
 (0)