Skip to content

Commit 7989228

Browse files
author
Florian Lutze
committed
feat(sdk): add new config to generate client based on nested operation ids
1 parent 937a874 commit 7989228

File tree

7 files changed

+192
-35
lines changed

7 files changed

+192
-35
lines changed

packages/openapi-ts/src/plugins/@angular/common/companions/angularHttpRequestsCompanionPluginHandler.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ const generateAngularClassRequests = ({
8484

8585
for (const entry of classes.values()) {
8686
entry.path.forEach((currentClassName, index) => {
87-
if (!requestClasses.has(currentClassName)) {
88-
requestClasses.set(currentClassName, {
89-
className: currentClassName,
87+
if (!requestClasses.has(currentClassName.className)) {
88+
requestClasses.set(currentClassName.className, {
89+
className: currentClassName.className,
9090
classes: new Set(),
9191
methods: new Set(),
9292
nodes: [],
@@ -96,17 +96,17 @@ const generateAngularClassRequests = ({
9696

9797
const parentClassName = entry.path[index - 1];
9898
if (parentClassName && parentClassName !== currentClassName) {
99-
const parentClass = requestClasses.get(parentClassName)!;
100-
parentClass.classes.add(currentClassName);
101-
requestClasses.set(parentClassName, parentClass);
99+
const parentClass = requestClasses.get(parentClassName.className)!;
100+
parentClass.classes.add(currentClassName.className);
101+
requestClasses.set(parentClassName.className, parentClass);
102102
}
103103

104104
const isLast = entry.path.length === index + 1;
105105
if (!isLast) {
106106
return;
107107
}
108108

109-
const currentClass = requestClasses.get(currentClassName)!;
109+
const currentClass = requestClasses.get(currentClassName.className)!;
110110

111111
// Generate the request method name with "Request" suffix
112112
const requestMethodName =
@@ -133,7 +133,7 @@ const generateAngularClassRequests = ({
133133
}
134134

135135
currentClass.methods.add(requestMethodName);
136-
requestClasses.set(currentClassName, currentClass);
136+
requestClasses.set(currentClassName.className, currentClass);
137137
});
138138
}
139139
});

packages/openapi-ts/src/plugins/@angular/common/companions/angularHttpResourceCompanionPluginHandler.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ const generateAngularClassServices = ({
9191

9292
for (const entry of classes.values()) {
9393
entry.path.forEach((currentClassName, index) => {
94-
if (!serviceClasses.has(currentClassName)) {
95-
serviceClasses.set(currentClassName, {
96-
className: currentClassName,
94+
if (!serviceClasses.has(currentClassName.className)) {
95+
serviceClasses.set(currentClassName.className, {
96+
className: currentClassName.className,
9797
classes: new Set(),
9898
methods: new Set(),
9999
nodes: [],
@@ -103,17 +103,17 @@ const generateAngularClassServices = ({
103103

104104
const parentClassName = entry.path[index - 1];
105105
if (parentClassName && parentClassName !== currentClassName) {
106-
const parentClass = serviceClasses.get(parentClassName)!;
107-
parentClass.classes.add(currentClassName);
108-
serviceClasses.set(parentClassName, parentClass);
106+
const parentClass = serviceClasses.get(parentClassName.className)!;
107+
parentClass.classes.add(currentClassName.className);
108+
serviceClasses.set(parentClassName.className, parentClass);
109109
}
110110

111111
const isLast = entry.path.length === index + 1;
112112
if (!isLast) {
113113
return;
114114
}
115115

116-
const currentClass = serviceClasses.get(currentClassName)!;
116+
const currentClass = serviceClasses.get(currentClassName.className)!;
117117

118118
// Generate the resource method name
119119
const resourceMethodName =
@@ -141,7 +141,7 @@ const generateAngularClassServices = ({
141141
}
142142

143143
currentClass.methods.add(resourceMethodName);
144-
serviceClasses.set(currentClassName, currentClass);
144+
serviceClasses.set(currentClassName.className, currentClass);
145145
});
146146
}
147147
});
@@ -295,7 +295,7 @@ const generateResourceCallExpression = ({
295295
expression: methodAccess,
296296
name: stringCase({
297297
case: 'camelCase',
298-
value: className,
298+
value: className.className,
299299
}),
300300
});
301301
}

packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = {
1111
classStructure: 'auto',
1212
client: true,
1313
exportFromIndex: true,
14+
groupByOperationId: false,
1415
instance: false,
1516
operationId: true,
1617
params_EXPERIMENTAL: 'default',

packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ interface ClassNameEntry {
3636
/**
3737
* JSONPath-like array to class location.
3838
*/
39-
path: ReadonlyArray<string>;
39+
path: ReadonlyArray<PathEntry>;
40+
}
41+
42+
interface PathEntry {
43+
className: string;
44+
propertyName: string;
4045
}
4146

4247
const operationClassName = ({
@@ -83,7 +88,96 @@ const getOperationMethodName = ({
8388
/**
8489
* Returns a list of classes where this operation appears in the generated SDK.
8590
*/
86-
export const operationClasses = ({
91+
export const operationClassesNestedByOperationId = ({
92+
context,
93+
operation,
94+
plugin,
95+
}: {
96+
context: IR.Context;
97+
operation: IR.OperationObject;
98+
plugin: {
99+
config: Pick<
100+
HeyApiSdkPlugin['Instance']['config'],
101+
'asClass' | 'classStructure' | 'instance'
102+
>;
103+
};
104+
}): Map<string, ClassNameEntry> => {
105+
const classNames = new Map<string, ClassNameEntry>();
106+
107+
let methodName: string | undefined;
108+
let classCandidates: Array<string> = [];
109+
110+
if (!operation.operationId) {
111+
throw new Error(
112+
'Operation ID is required when nestByOperationId is true. Missing in operation: ' +
113+
operation.path,
114+
);
115+
}
116+
117+
classCandidates = operation.operationId?.split(/[./]/).filter(Boolean) ?? [];
118+
if (classCandidates.length > 1) {
119+
// Pop the method candidate from the class candidates to not have it in the path
120+
const methodCandidate = classCandidates.pop()!;
121+
methodName = stringCase({
122+
case: 'camelCase',
123+
value: sanitizeNamespaceIdentifier(methodCandidate),
124+
});
125+
}
126+
127+
// classCandidates = ["v1", "tenants", "providers"];
128+
let previousClassName = '';
129+
const rootClasses = classCandidates.map((value) => {
130+
const currentClassName =
131+
previousClassName +
132+
stringCase({
133+
case: 'PascalCase',
134+
value,
135+
});
136+
previousClassName = currentClassName;
137+
return currentClassName;
138+
});
139+
140+
const className =
141+
rootClasses.length > 0 ? rootClasses[rootClasses.length - 1]! : undefined;
142+
143+
for (const rootClass of rootClasses) {
144+
const finalClassName = operationClassName({
145+
context,
146+
value: className || rootClass,
147+
});
148+
149+
const path: PathEntry[] = [];
150+
rootClasses.forEach((className, index) => {
151+
const propertyName = stringCase({
152+
case: 'camelCase',
153+
value: transformClassName({
154+
config: context.config,
155+
name: classCandidates[index] ?? '',
156+
}),
157+
});
158+
path.push({
159+
className: operationClassName({
160+
context,
161+
value: className,
162+
}),
163+
propertyName,
164+
});
165+
});
166+
167+
classNames.set(rootClass, {
168+
className: finalClassName,
169+
methodName: methodName || getOperationMethodName({ operation, plugin }),
170+
path,
171+
});
172+
}
173+
174+
return classNames;
175+
};
176+
177+
/**
178+
* Returns a list of classes where this operation appears in the generated SDK.
179+
*/
180+
const operationClassesDefault = ({
87181
context,
88182
operation,
89183
plugin,
@@ -140,18 +234,55 @@ export const operationClasses = ({
140234
classNames.set(rootClass, {
141235
className: finalClassName,
142236
methodName: methodName || getOperationMethodName({ operation, plugin }),
143-
path: path.map((value) =>
144-
operationClassName({
237+
path: path.map((value) => ({
238+
className: operationClassName({
145239
context,
146240
value,
147241
}),
148-
),
242+
propertyName: transformClassName({
243+
config: context.config,
244+
name: value,
245+
}),
246+
})),
149247
});
150248
}
151249

152250
return classNames;
153251
};
154252

253+
/**
254+
* Returns a list of classes where this operation appears in the generated SDK.
255+
*/
256+
export const operationClasses = ({
257+
context,
258+
operation,
259+
plugin,
260+
}: {
261+
context: IR.Context;
262+
operation: IR.OperationObject;
263+
plugin: {
264+
config: Pick<
265+
HeyApiSdkPlugin['Instance']['config'],
266+
'asClass' | 'classStructure' | 'instance' | 'groupByOperationId'
267+
>;
268+
};
269+
}): Map<string, ClassNameEntry> => {
270+
// Use nested operationId class generator above
271+
if (plugin.config.groupByOperationId) {
272+
return operationClassesNestedByOperationId({
273+
context,
274+
operation,
275+
plugin,
276+
});
277+
}
278+
279+
return operationClassesDefault({
280+
context,
281+
operation,
282+
plugin,
283+
});
284+
};
285+
155286
export const operationOptionsType = ({
156287
file,
157288
operation,

packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ interface SdkClassEntry {
9393
/**
9494
* Child classes located inside this class.
9595
*/
96-
classes: Set<string>;
96+
classes: Set<{ className: string; propertyName: string }>;
9797
/**
9898
* Track unique added method nodes.
9999
*/
@@ -152,9 +152,9 @@ const generateClassSdk = ({
152152

153153
for (const entry of classes.values()) {
154154
entry.path.forEach((currentClassName, index) => {
155-
if (!sdkClasses.has(currentClassName)) {
156-
sdkClasses.set(currentClassName, {
157-
className: currentClassName,
155+
if (!sdkClasses.has(currentClassName.className)) {
156+
sdkClasses.set(currentClassName.className, {
157+
className: currentClassName.className,
158158
classes: new Set(),
159159
methods: new Set(),
160160
nodes: [],
@@ -164,9 +164,12 @@ const generateClassSdk = ({
164164

165165
const parentClassName = entry.path[index - 1];
166166
if (parentClassName && parentClassName !== currentClassName) {
167-
const parentClass = sdkClasses.get(parentClassName)!;
168-
parentClass.classes.add(currentClassName);
169-
sdkClasses.set(parentClassName, parentClass);
167+
const parentClass = sdkClasses.get(parentClassName.className)!;
168+
parentClass.classes.add({
169+
className: currentClassName.className,
170+
propertyName: currentClassName.propertyName,
171+
});
172+
sdkClasses.set(parentClassName.className, parentClass);
170173
}
171174

172175
const isLast = entry.path.length === index + 1;
@@ -175,7 +178,7 @@ const generateClassSdk = ({
175178
return;
176179
}
177180

178-
const currentClass = sdkClasses.get(currentClassName)!;
181+
const currentClass = sdkClasses.get(currentClassName.className)!;
179182

180183
// avoid duplicate methods
181184
if (currentClass.methods.has(entry.methodName)) {
@@ -247,7 +250,7 @@ const generateClassSdk = ({
247250

248251
currentClass.methods.add(entry.methodName);
249252

250-
sdkClasses.set(currentClassName, currentClass);
253+
sdkClasses.set(currentClassName.className, currentClass);
251254
});
252255
}
253256
});
@@ -259,9 +262,19 @@ const generateClassSdk = ({
259262

260263
if (currentClass.classes.size) {
261264
for (const childClassName of currentClass.classes) {
262-
const childClass = sdkClasses.get(childClassName)!;
265+
const childClass = sdkClasses.get(childClassName.className)!;
263266
generateClass(childClass);
264267

268+
// Skip if the property already exists
269+
/** @ts-ignore */
270+
if (
271+
currentClass.nodes.find(
272+
(node) => node.name?.escapedText === childClassName.propertyName,
273+
)
274+
) {
275+
continue;
276+
}
277+
265278
currentClass.nodes.push(
266279
tsc.propertyDeclaration({
267280
initializer: plugin.config.instance
@@ -290,7 +303,7 @@ const generateClassSdk = ({
290303
modifier: plugin.config.instance ? undefined : 'static',
291304
name: stringCase({
292305
case: 'camelCase',
293-
value: childClass.className,
306+
value: childClassName.propertyName,
294307
}),
295308
}),
296309
);

packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export type UserConfig = Plugin.Name<'@hey-api/sdk'> & {
6363
* @default true
6464
*/
6565
exportFromIndex?: boolean;
66+
/**
67+
* Group operations by operationId?
68+
*
69+
* @default false
70+
*/
71+
groupByOperationId?: boolean;
6672
/**
6773
* Include only service classes with names matching regular expression
6874
*
@@ -238,6 +244,12 @@ export type Config = Plugin.Name<'@hey-api/sdk'> & {
238244
* @default true
239245
*/
240246
exportFromIndex: boolean;
247+
/**
248+
* Group operations by operationId?
249+
*
250+
* @default false
251+
*/
252+
groupByOperationId?: boolean;
241253
/**
242254
* Include only service classes with names matching regular expression
243255
*

0 commit comments

Comments
 (0)