@workos/oagen-emitters 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +10 -34
- package/src/dotnet/index.ts +6 -4
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +10 -5
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/src/go/resources.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
getOpInferFromClient,
|
|
19
19
|
buildHiddenParams,
|
|
20
20
|
hasHiddenParams,
|
|
21
|
+
collectGroupedParamNames,
|
|
22
|
+
collectBodyFieldTypes,
|
|
21
23
|
} from '../shared/resolved-ops.js';
|
|
22
24
|
import { lowerFirstForDoc, fieldDocComment } from '../shared/naming-utils.js';
|
|
23
25
|
import { generateWrapperMethods } from './wrappers.js';
|
|
@@ -78,15 +80,32 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
78
80
|
// Determine which imports are needed
|
|
79
81
|
const needsFmt = operations.some((op) => op.pathParams.length > 0);
|
|
80
82
|
const needsNetUrl = operations.some((op) => {
|
|
83
|
+
if (op.pathParams.length > 0) return true;
|
|
81
84
|
const resolved = lookupResolved(op, resolvedLookup);
|
|
82
|
-
|
|
85
|
+
if (resolved?.urlBuilder) return true;
|
|
86
|
+
if (resolved && hasHiddenParams(resolved) && op.httpMethod.toLowerCase() === 'get') return true;
|
|
87
|
+
if ((op.parameterGroups?.length ?? 0) > 0) return true;
|
|
88
|
+
return false;
|
|
83
89
|
});
|
|
84
90
|
const needsStrings = needsStringsImport(operations, resolvedLookup);
|
|
91
|
+
const needsJson = operations.some((op) => hasBodyGroups(op));
|
|
92
|
+
// context is needed only for methods that make HTTP calls. URL-builder ops
|
|
93
|
+
// don't take ctx, so a file that contains *only* URL builders would have
|
|
94
|
+
// an unused import.
|
|
95
|
+
const needsContext = operations.some((op) => {
|
|
96
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
97
|
+
return !resolved?.urlBuilder;
|
|
98
|
+
});
|
|
85
99
|
|
|
86
100
|
lines.push(`package ${ctx.namespace}`);
|
|
87
101
|
lines.push('');
|
|
88
102
|
lines.push('import (');
|
|
89
|
-
|
|
103
|
+
if (needsContext) {
|
|
104
|
+
lines.push('\t"context"');
|
|
105
|
+
}
|
|
106
|
+
if (needsJson) {
|
|
107
|
+
lines.push('\t"encoding/json"');
|
|
108
|
+
}
|
|
90
109
|
if (needsFmt) {
|
|
91
110
|
lines.push('\t"fmt"');
|
|
92
111
|
}
|
|
@@ -106,6 +125,23 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
106
125
|
lines.push('}');
|
|
107
126
|
lines.push('');
|
|
108
127
|
|
|
128
|
+
// Pre-collect all parameter groups across operations and emit deduplicated
|
|
129
|
+
// type definitions. A group with the same name may appear in both query-param
|
|
130
|
+
// and body-param contexts; the interface then carries both applyToQuery and
|
|
131
|
+
// applyToBody methods.
|
|
132
|
+
const groupTypes = collectFileGroups(mountName, operations);
|
|
133
|
+
if (groupTypes.length > 0) {
|
|
134
|
+
// Collect body field types from all operations so variant structs use the
|
|
135
|
+
// correct IR types (the parser's group-level types can fall back to string).
|
|
136
|
+
const mergedBodyFieldTypes = new Map<string, import('@workos/oagen').TypeRef>();
|
|
137
|
+
for (const op of operations) {
|
|
138
|
+
for (const [k, v] of collectBodyFieldTypes(op, ctx.spec.models)) {
|
|
139
|
+
mergedBodyFieldTypes.set(k, v);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
lines.push(emitCollectedGroupTypes(mountName, groupTypes, mergedBodyFieldTypes));
|
|
143
|
+
}
|
|
144
|
+
|
|
109
145
|
// Generate params structs and methods for each operation.
|
|
110
146
|
// Deduplicate by method name -- multiple IR operations can resolve to the same
|
|
111
147
|
// Go method name when mounted from different IR services.
|
|
@@ -118,24 +154,28 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
118
154
|
emittedMethods.add(method);
|
|
119
155
|
|
|
120
156
|
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
157
|
+
const hasWrappers = (resolvedOp?.wrappers?.length ?? 0) > 0;
|
|
158
|
+
|
|
159
|
+
// When wrappers exist (union-body operations like Authenticate), only
|
|
160
|
+
// the typed per-variant wrapper methods are emitted. The parent method
|
|
161
|
+
// would otherwise carry `Body interface{}` and defeat type safety.
|
|
162
|
+
if (!hasWrappers) {
|
|
163
|
+
const paramsStruct = generateParamsStruct(mountName, method, op, plan, ctx, resolvedOp);
|
|
164
|
+
if (paramsStruct) {
|
|
165
|
+
lines.push(paramsStruct);
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (paramsStruct) {
|
|
125
|
-
lines.push(paramsStruct);
|
|
169
|
+
const methodCode = generateMethod(serviceType, mountName, method, op, plan, ctx, resolvedOp);
|
|
170
|
+
lines.push(methodCode);
|
|
126
171
|
lines.push('');
|
|
127
172
|
}
|
|
128
173
|
|
|
129
|
-
// Generate method
|
|
130
|
-
const methodCode = generateMethod(serviceType, mountName, method, op, plan, ctx, resolvedOp);
|
|
131
|
-
lines.push(methodCode);
|
|
132
|
-
lines.push('');
|
|
133
|
-
|
|
134
174
|
// Generate union split wrapper methods (e.g., AuthenticateWithPassword)
|
|
135
|
-
if (
|
|
136
|
-
const wrapperLines = generateWrapperMethods(serviceType, resolvedOp, ctx);
|
|
175
|
+
if (hasWrappers && resolvedOp) {
|
|
176
|
+
const wrapperLines = generateWrapperMethods(serviceType, mountName, resolvedOp, ctx);
|
|
137
177
|
lines.push(...wrapperLines);
|
|
138
|
-
for (const w of resolvedOp.wrappers) {
|
|
178
|
+
for (const w of resolvedOp.wrappers!) {
|
|
139
179
|
emittedMethods.add(methodName(w.name));
|
|
140
180
|
}
|
|
141
181
|
}
|
|
@@ -160,6 +200,165 @@ export function paramsStructName(mountName: string, method: string): string {
|
|
|
160
200
|
return `${prefix}${method}Params`;
|
|
161
201
|
}
|
|
162
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Unexported struct name used as the typed JSON body for a non-wrapper
|
|
205
|
+
* operation that has hidden params (defaults / inferFromClient). Mirrors
|
|
206
|
+
* the wrapper convention (`<methodLowerCamel>Body`).
|
|
207
|
+
*/
|
|
208
|
+
function hiddenParamsBodyStructName(method: string): string {
|
|
209
|
+
return `${unexportedName(method)}Body`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Mutually-exclusive parameter group support
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/** Check whether an operation has any body-level parameter groups. */
|
|
217
|
+
function hasBodyGroups(op: Operation): boolean {
|
|
218
|
+
return (op.parameterGroups ?? []).some((g) => isBodyGroup(g, op));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check whether a parameter group targets the request body (rather than query params).
|
|
223
|
+
* Body groups' parameter names don't appear in op.queryParams — they come from
|
|
224
|
+
* the body model's oneOf variants.
|
|
225
|
+
*/
|
|
226
|
+
function isBodyGroup(group: import('@workos/oagen').ParameterGroup, op: Operation): boolean {
|
|
227
|
+
const queryNames = new Set(op.queryParams.map((qp) => qp.name));
|
|
228
|
+
// If none of the group's variant params appear in queryParams, it's a body group
|
|
229
|
+
return group.variants.every((v) => v.parameters.every((p) => !queryNames.has(p.name)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Interface type name for a parameter group (e.g. AuthorizationParentResource). */
|
|
233
|
+
function groupInterfaceName(mountName: string, groupName: string): string {
|
|
234
|
+
return `${className(mountName)}${fieldName(groupName)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Variant struct type name (e.g. AuthorizationParentResourceRefByID). */
|
|
238
|
+
function groupVariantTypeName(mountName: string, groupName: string, variantName: string): string {
|
|
239
|
+
return `${groupInterfaceName(mountName, groupName)}${fieldName(variantName)}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Derive a short field name for a parameter within a variant struct.
|
|
244
|
+
* Strips the group name prefix when present to avoid stuttering
|
|
245
|
+
* (e.g. parent_resource_id in group parent_resource -> ID).
|
|
246
|
+
*/
|
|
247
|
+
function deriveVariantFieldName(paramName: string, groupName: string): string {
|
|
248
|
+
const prefix = groupName + '_';
|
|
249
|
+
const stripped = paramName.startsWith(prefix) ? paramName.slice(prefix.length) : paramName;
|
|
250
|
+
return fieldName(stripped);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Collected group metadata, merged across all operations in a file. */
|
|
254
|
+
interface CollectedGroup {
|
|
255
|
+
name: string;
|
|
256
|
+
needsQuery: boolean;
|
|
257
|
+
needsBody: boolean;
|
|
258
|
+
/** Use the first variant set encountered (they should be identical). */
|
|
259
|
+
variants: import('@workos/oagen').ParameterGroupVariant[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Pre-collect all parameter groups across operations in a mount group,
|
|
264
|
+
* deduplicating by group name and merging query/body usage flags.
|
|
265
|
+
*/
|
|
266
|
+
function collectFileGroups(mountName: string, operations: Operation[]): CollectedGroup[] {
|
|
267
|
+
const byName = new Map<string, CollectedGroup>();
|
|
268
|
+
|
|
269
|
+
for (const op of operations) {
|
|
270
|
+
for (const group of op.parameterGroups ?? []) {
|
|
271
|
+
const existing = byName.get(group.name);
|
|
272
|
+
const isBody = isBodyGroup(group, op);
|
|
273
|
+
if (existing) {
|
|
274
|
+
if (isBody) existing.needsBody = true;
|
|
275
|
+
else existing.needsQuery = true;
|
|
276
|
+
} else {
|
|
277
|
+
byName.set(group.name, {
|
|
278
|
+
name: group.name,
|
|
279
|
+
needsQuery: !isBody,
|
|
280
|
+
needsBody: isBody,
|
|
281
|
+
variants: group.variants,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return [...byName.values()];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Emit deduplicated Go interface and variant struct definitions for all
|
|
292
|
+
* collected parameter groups in a file. Groups used in query contexts get
|
|
293
|
+
* applyToQuery; body contexts get applyToBody; groups used in both get both.
|
|
294
|
+
*/
|
|
295
|
+
function emitCollectedGroupTypes(
|
|
296
|
+
mountName: string,
|
|
297
|
+
groups: CollectedGroup[],
|
|
298
|
+
bodyFieldTypes: Map<string, import('@workos/oagen').TypeRef>,
|
|
299
|
+
): string {
|
|
300
|
+
const lines: string[] = [];
|
|
301
|
+
|
|
302
|
+
for (const group of groups) {
|
|
303
|
+
const ifaceName = groupInterfaceName(mountName, group.name);
|
|
304
|
+
const markerMethod = `is${ifaceName}`;
|
|
305
|
+
|
|
306
|
+
const variantNames = group.variants.map((v) => groupVariantTypeName(mountName, group.name, v.name));
|
|
307
|
+
lines.push(`// ${ifaceName} is one of:`);
|
|
308
|
+
for (const vn of variantNames) {
|
|
309
|
+
lines.push(`// - ${vn}`);
|
|
310
|
+
}
|
|
311
|
+
lines.push(`type ${ifaceName} interface {`);
|
|
312
|
+
lines.push(`\t${markerMethod}()`);
|
|
313
|
+
if (group.needsQuery) {
|
|
314
|
+
lines.push('\tapplyToQuery(url.Values)');
|
|
315
|
+
}
|
|
316
|
+
if (group.needsBody) {
|
|
317
|
+
lines.push('\tapplyToBody(map[string]any)');
|
|
318
|
+
}
|
|
319
|
+
lines.push('}');
|
|
320
|
+
lines.push('');
|
|
321
|
+
|
|
322
|
+
for (const variant of group.variants) {
|
|
323
|
+
const typeName = groupVariantTypeName(mountName, group.name, variant.name);
|
|
324
|
+
|
|
325
|
+
lines.push(`type ${typeName} struct {`);
|
|
326
|
+
for (const param of variant.parameters) {
|
|
327
|
+
const goField = deriveVariantFieldName(param.name, group.name);
|
|
328
|
+
const effectiveType = bodyFieldTypes.get(param.name) ?? param.type;
|
|
329
|
+
const goType = mapTypeRefValue(effectiveType);
|
|
330
|
+
lines.push(`\t${goField} ${goType}`);
|
|
331
|
+
}
|
|
332
|
+
lines.push('}');
|
|
333
|
+
lines.push('');
|
|
334
|
+
|
|
335
|
+
lines.push(`func (p ${typeName}) ${markerMethod}() {}`);
|
|
336
|
+
|
|
337
|
+
if (group.needsQuery) {
|
|
338
|
+
lines.push(`func (p ${typeName}) applyToQuery(v url.Values) {`);
|
|
339
|
+
for (const param of variant.parameters) {
|
|
340
|
+
const goField = deriveVariantFieldName(param.name, group.name);
|
|
341
|
+
lines.push(`\tv.Set("${param.name}", ${formatQueryValue(`p.${goField}`, param.type)})`);
|
|
342
|
+
}
|
|
343
|
+
lines.push('}');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (group.needsBody) {
|
|
347
|
+
lines.push(`func (p ${typeName}) applyToBody(m map[string]any) {`);
|
|
348
|
+
for (const param of variant.parameters) {
|
|
349
|
+
const goField = deriveVariantFieldName(param.name, group.name);
|
|
350
|
+
lines.push(`\tm["${param.name}"] = p.${goField}`);
|
|
351
|
+
}
|
|
352
|
+
lines.push('}');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
lines.push('');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return lines.join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
163
362
|
function generateParamsStruct(
|
|
164
363
|
mountName: string,
|
|
165
364
|
method: string,
|
|
@@ -170,22 +369,24 @@ function generateParamsStruct(
|
|
|
170
369
|
): string | null {
|
|
171
370
|
// Build set of hidden param names (defaults + inferFromClient)
|
|
172
371
|
const hidden = buildHiddenParams(resolvedOp);
|
|
372
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
373
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
173
374
|
|
|
174
|
-
const hasQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
375
|
+
const hasQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0;
|
|
175
376
|
const hasBody = plan.hasBody && op.requestBody;
|
|
176
377
|
|
|
177
|
-
// Check if body has any visible fields after filtering
|
|
378
|
+
// Check if body has any visible fields after filtering (excluding grouped fields)
|
|
178
379
|
let hasVisibleBodyFields = false;
|
|
179
380
|
if (hasBody && op.requestBody?.kind === 'model') {
|
|
180
381
|
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
181
382
|
if (bodyModel) {
|
|
182
|
-
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
383
|
+
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name) && !groupedParams.has(f.name));
|
|
183
384
|
}
|
|
184
385
|
} else if (hasBody) {
|
|
185
386
|
hasVisibleBodyFields = true; // non-model body always visible
|
|
186
387
|
}
|
|
187
388
|
|
|
188
|
-
if (!hasQueryParams && !hasVisibleBodyFields) return null;
|
|
389
|
+
if (!hasQueryParams && !hasVisibleBodyFields && !hasGroups) return null;
|
|
189
390
|
|
|
190
391
|
const lines: string[] = [];
|
|
191
392
|
const structName = paramsStructName(mountName, method);
|
|
@@ -202,6 +403,7 @@ function generateParamsStruct(
|
|
|
202
403
|
if (bodyModel) {
|
|
203
404
|
for (const field of bodyModel.fields) {
|
|
204
405
|
if (hidden.has(field.name)) continue;
|
|
406
|
+
if (groupedParams.has(field.name)) continue;
|
|
205
407
|
const goField = fieldName(field.name);
|
|
206
408
|
if (emittedFields.has(goField)) continue;
|
|
207
409
|
emittedFields.add(goField);
|
|
@@ -233,7 +435,7 @@ function generateParamsStruct(
|
|
|
233
435
|
// Check if this is a list operation with standard pagination fields.
|
|
234
436
|
// If so, embed PaginationParams and skip those fields individually.
|
|
235
437
|
const PAGINATION_FIELDS = new Set(['before', 'after', 'limit', 'order']);
|
|
236
|
-
const visibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name));
|
|
438
|
+
const visibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name));
|
|
237
439
|
const hasPaginationFields = ['before', 'after', 'limit'].every((name) =>
|
|
238
440
|
visibleQueryParams.some((qp) => qp.name === name),
|
|
239
441
|
);
|
|
@@ -241,9 +443,10 @@ function generateParamsStruct(
|
|
|
241
443
|
lines.push('\tPaginationParams');
|
|
242
444
|
}
|
|
243
445
|
|
|
244
|
-
// Query params (skip any already emitted from body fields, hidden params, and pagination fields)
|
|
446
|
+
// Query params (skip any already emitted from body fields, hidden params, grouped params, and pagination fields)
|
|
245
447
|
for (const param of op.queryParams) {
|
|
246
448
|
if (hidden.has(param.name)) continue;
|
|
449
|
+
if (groupedParams.has(param.name)) continue;
|
|
247
450
|
if (hasPaginationFields && PAGINATION_FIELDS.has(param.name)) continue;
|
|
248
451
|
const goField = fieldName(param.name);
|
|
249
452
|
if (emittedFields.has(goField)) continue;
|
|
@@ -272,7 +475,52 @@ function generateParamsStruct(
|
|
|
272
475
|
lines.push(`\t${goField} ${goType} \`${urlTag} ${jsonTag}\``);
|
|
273
476
|
}
|
|
274
477
|
|
|
478
|
+
// Parameter group fields (sum-type interfaces, serialized via applyToQuery)
|
|
479
|
+
for (const group of op.parameterGroups ?? []) {
|
|
480
|
+
const goField = fieldName(group.name);
|
|
481
|
+
const goType = groupInterfaceName(mountName, group.name);
|
|
482
|
+
if (group.optional) {
|
|
483
|
+
lines.push(`\t// ${goField} optionally identifies the ${group.name.replace(/_/g, ' ')}.`);
|
|
484
|
+
} else {
|
|
485
|
+
lines.push(`\t// ${goField} identifies the ${group.name.replace(/_/g, ' ')} (required).`);
|
|
486
|
+
}
|
|
487
|
+
lines.push(`\t${goField} ${goType} \`url:"-" json:"-"\``);
|
|
488
|
+
}
|
|
489
|
+
|
|
275
490
|
lines.push('}');
|
|
491
|
+
|
|
492
|
+
// Generate MarshalJSON for params structs that have body-level groups.
|
|
493
|
+
// The method uses a type alias to marshal non-group fields, then merges
|
|
494
|
+
// the active group variant's fields into the JSON map.
|
|
495
|
+
const bodyGroupList = (op.parameterGroups ?? []).filter((g) => isBodyGroup(g, op));
|
|
496
|
+
if (bodyGroupList.length > 0) {
|
|
497
|
+
lines.push('');
|
|
498
|
+
lines.push(`// MarshalJSON implements json.Marshaler for ${structName}.`);
|
|
499
|
+
lines.push(`func (p ${structName}) MarshalJSON() ([]byte, error) {`);
|
|
500
|
+
lines.push(`\ttype Alias ${structName}`);
|
|
501
|
+
lines.push('\tdata, err := json.Marshal(Alias(p))');
|
|
502
|
+
lines.push('\tif err != nil {');
|
|
503
|
+
lines.push('\t\treturn nil, err');
|
|
504
|
+
lines.push('\t}');
|
|
505
|
+
// Check if any group is non-nil; if not, return early
|
|
506
|
+
const allNilCheck = bodyGroupList.map((g) => `p.${fieldName(g.name)} == nil`).join(' && ');
|
|
507
|
+
lines.push(`\tif ${allNilCheck} {`);
|
|
508
|
+
lines.push('\t\treturn data, nil');
|
|
509
|
+
lines.push('\t}');
|
|
510
|
+
lines.push('\tvar m map[string]any');
|
|
511
|
+
lines.push('\tif err := json.Unmarshal(data, &m); err != nil {');
|
|
512
|
+
lines.push('\t\treturn nil, err');
|
|
513
|
+
lines.push('\t}');
|
|
514
|
+
for (const group of bodyGroupList) {
|
|
515
|
+
const goField = fieldName(group.name);
|
|
516
|
+
lines.push(`\tif p.${goField} != nil {`);
|
|
517
|
+
lines.push(`\t\tp.${goField}.applyToBody(m)`);
|
|
518
|
+
lines.push('\t}');
|
|
519
|
+
}
|
|
520
|
+
lines.push('\treturn json.Marshal(m)');
|
|
521
|
+
lines.push('}');
|
|
522
|
+
}
|
|
523
|
+
|
|
276
524
|
return lines.join('\n');
|
|
277
525
|
}
|
|
278
526
|
|
|
@@ -290,31 +538,44 @@ function generateMethod(
|
|
|
290
538
|
const isDelete = plan.isDelete;
|
|
291
539
|
const hasBody = plan.hasBody && op.requestBody;
|
|
292
540
|
const hidden = buildHiddenParams(resolvedOp);
|
|
541
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
293
542
|
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
294
543
|
|
|
295
|
-
// Check if body has visible fields after filtering hidden params
|
|
544
|
+
// Check if body has visible fields after filtering hidden params and grouped params
|
|
545
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
296
546
|
let hasVisibleBodyFields = false;
|
|
297
547
|
if (hasBody && op.requestBody?.kind === 'model') {
|
|
298
548
|
const bodyModel = _ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
299
549
|
if (bodyModel) {
|
|
300
|
-
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
550
|
+
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name) && !groupedParams.has(f.name));
|
|
301
551
|
}
|
|
302
552
|
} else if (hasBody) {
|
|
303
553
|
hasVisibleBodyFields = true;
|
|
304
554
|
}
|
|
305
555
|
|
|
306
|
-
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams;
|
|
556
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
307
557
|
const paramsType = hasParams ? `*${paramsStructName(mountName, method)}` : null;
|
|
308
558
|
const bodyArg = hasBody && hasParams ? bodyArgument(op) : 'nil';
|
|
309
559
|
const hasHidden = hasHiddenParams(resolvedOp);
|
|
310
560
|
const isGet = op.httpMethod.toLowerCase() === 'get';
|
|
561
|
+
const isUrlBuilder = resolvedOp?.urlBuilder ?? false;
|
|
311
562
|
|
|
312
563
|
// Detect if response is a raw array (not paginated)
|
|
313
564
|
const isArrayResponse = !isPaginated && op.response?.kind === 'array';
|
|
314
565
|
|
|
566
|
+
// Emit a typed body struct *before* the method for non-GET ops with hidden
|
|
567
|
+
// params (defaults / inferFromClient). The struct gives consumers a
|
|
568
|
+
// deterministic wire format and avoids `map[string]interface{}` literals.
|
|
569
|
+
if (hasHidden && !isGet && hasBody) {
|
|
570
|
+
emitHiddenParamsBodyStruct(lines, method, op, _ctx, resolvedOp!);
|
|
571
|
+
lines.push('');
|
|
572
|
+
}
|
|
573
|
+
|
|
315
574
|
// Return type
|
|
316
575
|
let returnType: string;
|
|
317
|
-
if (
|
|
576
|
+
if (isUrlBuilder) {
|
|
577
|
+
returnType = 'string';
|
|
578
|
+
} else if (isPaginated && op.pagination) {
|
|
318
579
|
const itemType = resolveIteratorItemType(op.pagination.itemType, _ctx);
|
|
319
580
|
returnType = `*Iterator[${itemType}]`;
|
|
320
581
|
} else if (isDelete) {
|
|
@@ -333,7 +594,7 @@ function generateMethod(
|
|
|
333
594
|
// Build godoc -- wrap multi-line descriptions in // comments
|
|
334
595
|
if (op.description) {
|
|
335
596
|
const descLines = op.description.split('\n').filter((l) => l.trim());
|
|
336
|
-
lines.push(`// ${method
|
|
597
|
+
lines.push(`// ${godocSummary(method, descLines[0])}`);
|
|
337
598
|
for (let i = 1; i < descLines.length; i++) {
|
|
338
599
|
lines.push(`// ${descLines[i].trim()}`);
|
|
339
600
|
}
|
|
@@ -349,8 +610,8 @@ function generateMethod(
|
|
|
349
610
|
lines.push(`// Deprecated: this operation is deprecated.`);
|
|
350
611
|
}
|
|
351
612
|
|
|
352
|
-
// Method signature
|
|
353
|
-
const params: string[] = ['ctx context.Context'];
|
|
613
|
+
// Method signature — URL builders don't take ctx (no I/O) and return a string.
|
|
614
|
+
const params: string[] = isUrlBuilder ? [] : ['ctx context.Context'];
|
|
354
615
|
// Path params as positional args (sorted by template order)
|
|
355
616
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
356
617
|
params.push(`${lowerFirst(fieldName(p.name))} ${mapTypeRefValue(p.type)}`);
|
|
@@ -360,20 +621,23 @@ function generateMethod(
|
|
|
360
621
|
}
|
|
361
622
|
params.push('opts ...RequestOption');
|
|
362
623
|
|
|
363
|
-
|
|
364
|
-
lines.push(`func (s *${serviceType}) ${method}(${params.join(', ')}) ${returnType} {`);
|
|
365
|
-
} else if (isDelete || !plan.responseModelName) {
|
|
366
|
-
lines.push(`func (s *${serviceType}) ${method}(${params.join(', ')}) ${returnType} {`);
|
|
367
|
-
} else {
|
|
368
|
-
lines.push(`func (s *${serviceType}) ${method}(${params.join(', ')}) ${returnType} {`);
|
|
369
|
-
}
|
|
624
|
+
lines.push(`func (s *${serviceType}) ${method}(${params.join(', ')}) ${returnType} {`);
|
|
370
625
|
|
|
371
626
|
// Build path
|
|
372
627
|
const pathExpr = buildPathExpr(op);
|
|
373
628
|
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
if (
|
|
629
|
+
// URL-builder ops construct the URL client-side and return it without
|
|
630
|
+
// performing any HTTP I/O.
|
|
631
|
+
if (isUrlBuilder) {
|
|
632
|
+
emitUrlBuilderMethod(lines, op, pathExpr, resolvedOp!, paramsType);
|
|
633
|
+
lines.push('}');
|
|
634
|
+
return lines.join('\n');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// For GET operations with hidden params or parameter groups, build query
|
|
638
|
+
// via url.Values so we can inject defaults, inferred values, and/or call
|
|
639
|
+
// applyToQuery on grouped parameter variants.
|
|
640
|
+
if ((hasHidden || hasGroups) && isGet) {
|
|
377
641
|
emitGetWithHiddenParams(
|
|
378
642
|
lines,
|
|
379
643
|
op,
|
|
@@ -387,7 +651,7 @@ function generateMethod(
|
|
|
387
651
|
isArrayResponse,
|
|
388
652
|
);
|
|
389
653
|
} else if (hasHidden && !isGet && hasBody) {
|
|
390
|
-
// For non-GET operations with hidden params, build a body
|
|
654
|
+
// For non-GET operations with hidden params, build a typed body struct
|
|
391
655
|
emitBodyWithHiddenParams(
|
|
392
656
|
lines,
|
|
393
657
|
op,
|
|
@@ -399,13 +663,14 @@ function generateMethod(
|
|
|
399
663
|
isPaginated,
|
|
400
664
|
isDelete,
|
|
401
665
|
isArrayResponse,
|
|
666
|
+
method,
|
|
402
667
|
);
|
|
403
668
|
} else if (isPaginated && op.pagination) {
|
|
404
669
|
const itemType = resolveIteratorItemType(op.pagination.itemType, _ctx);
|
|
405
670
|
const dataPath = op.pagination.dataPath ? `"${op.pagination.dataPath}"` : `"data"`;
|
|
406
671
|
const cursorParam = '"after"';
|
|
407
672
|
lines.push(
|
|
408
|
-
`\treturn newIterator[${itemType}](ctx, s.client, "${op.httpMethod.toUpperCase()}", ${pathExpr}, ${hasVisibleQueryParams ? 'params' : 'nil'}, ${cursorParam}, ${dataPath}, opts)`,
|
|
673
|
+
`\treturn newIterator[${itemType}](ctx, s.client, "${op.httpMethod.toUpperCase()}", ${pathExpr}, ${hasVisibleQueryParams ? 'params' : 'nil'}, ${cursorParam}, ${dataPath}, opts, ${paginationDefaultsLiteral(op)})`,
|
|
409
674
|
);
|
|
410
675
|
} else if (isDelete) {
|
|
411
676
|
lines.push(
|
|
@@ -499,9 +764,11 @@ function emitGetWithHiddenParams(
|
|
|
499
764
|
lines.push('\t}');
|
|
500
765
|
}
|
|
501
766
|
|
|
502
|
-
// Add user-provided query params from the struct
|
|
767
|
+
// Add user-provided query params from the struct (excluding grouped params
|
|
768
|
+
// which are serialized via their variant's applyToQuery method)
|
|
503
769
|
if (paramsType) {
|
|
504
|
-
const
|
|
770
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
771
|
+
const visibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParamNames.has(qp.name));
|
|
505
772
|
for (const param of visibleQueryParams) {
|
|
506
773
|
const goField = fieldName(param.name);
|
|
507
774
|
const isMap = param.type.kind === 'map';
|
|
@@ -529,6 +796,14 @@ function emitGetWithHiddenParams(
|
|
|
529
796
|
lines.push('\t}');
|
|
530
797
|
}
|
|
531
798
|
}
|
|
799
|
+
|
|
800
|
+
// Apply parameter group variants to the query via applyToQuery
|
|
801
|
+
for (const group of op.parameterGroups ?? []) {
|
|
802
|
+
const goField = fieldName(group.name);
|
|
803
|
+
lines.push(`\tif params.${goField} != nil {`);
|
|
804
|
+
lines.push(`\t\tparams.${goField}.applyToQuery(query)`);
|
|
805
|
+
lines.push('\t}');
|
|
806
|
+
}
|
|
532
807
|
}
|
|
533
808
|
|
|
534
809
|
// Make the request with query as the 4th arg
|
|
@@ -537,7 +812,7 @@ function emitGetWithHiddenParams(
|
|
|
537
812
|
const dataPath = op.pagination.dataPath ? `"${op.pagination.dataPath}"` : `"data"`;
|
|
538
813
|
const cursorParam = '"after"';
|
|
539
814
|
lines.push(
|
|
540
|
-
`\treturn newIterator[${itemType}](ctx, s.client, "GET", ${pathExpr}, query, ${cursorParam}, ${dataPath}, opts)`,
|
|
815
|
+
`\treturn newIterator[${itemType}](ctx, s.client, "GET", ${pathExpr}, query, ${cursorParam}, ${dataPath}, opts, ${paginationDefaultsLiteral(op)})`,
|
|
541
816
|
);
|
|
542
817
|
} else if (isDelete) {
|
|
543
818
|
lines.push(`\t_, err := s.client.request(ctx, "GET", ${pathExpr}, query, nil, nil, opts)`);
|
|
@@ -566,62 +841,193 @@ function emitGetWithHiddenParams(
|
|
|
566
841
|
}
|
|
567
842
|
|
|
568
843
|
/**
|
|
569
|
-
* Emit method body for
|
|
570
|
-
* Builds a
|
|
844
|
+
* Emit method body for URL-builder operations (OAuth redirect endpoints).
|
|
845
|
+
* Builds a url.Values from defaults + inferred + user-provided params, then
|
|
846
|
+
* returns `s.client.buildURL(path, query, opts)` without performing any I/O.
|
|
571
847
|
*/
|
|
572
|
-
function
|
|
848
|
+
function emitUrlBuilderMethod(
|
|
573
849
|
lines: string[],
|
|
574
850
|
op: Operation,
|
|
575
851
|
pathExpr: string,
|
|
576
|
-
plan: OperationPlan,
|
|
577
|
-
ctx: EmitterContext,
|
|
578
852
|
resolvedOp: ResolvedOperation,
|
|
579
853
|
paramsType: string | null,
|
|
580
|
-
_isPaginated: boolean,
|
|
581
|
-
isDelete: boolean,
|
|
582
|
-
isArrayResponse: boolean,
|
|
583
854
|
): void {
|
|
584
855
|
const hidden = buildHiddenParams(resolvedOp);
|
|
585
856
|
|
|
586
|
-
|
|
587
|
-
lines.push('\tbody := map[string]interface{}{');
|
|
857
|
+
lines.push('\tquery := url.Values{}');
|
|
588
858
|
|
|
589
|
-
// Inject constant defaults
|
|
859
|
+
// Inject constant defaults (e.g., response_type=code)
|
|
590
860
|
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
591
|
-
lines.push(`\
|
|
861
|
+
lines.push(`\tquery.Set("${key}", ${goLiteralForQuery(value as string | number | boolean)})`);
|
|
592
862
|
}
|
|
593
863
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// Inject inferred fields from client config
|
|
864
|
+
// Inject inferred fields from client config (e.g., client_id)
|
|
597
865
|
for (const field of getOpInferFromClient(resolvedOp)) {
|
|
598
866
|
const expr = clientFieldExpression(field);
|
|
599
867
|
lines.push(`\tif ${expr} != "" {`);
|
|
600
|
-
lines.push(`\t\
|
|
868
|
+
lines.push(`\t\tquery.Set("${field}", ${expr})`);
|
|
601
869
|
lines.push('\t}');
|
|
602
870
|
}
|
|
603
871
|
|
|
604
|
-
// Add user-provided
|
|
605
|
-
if (paramsType
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (
|
|
612
|
-
lines.push(`\
|
|
872
|
+
// Add user-provided query params from the struct
|
|
873
|
+
if (paramsType) {
|
|
874
|
+
const visibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name));
|
|
875
|
+
for (const param of visibleQueryParams) {
|
|
876
|
+
const goField = fieldName(param.name);
|
|
877
|
+
const isMap = param.type.kind === 'map';
|
|
878
|
+
if (isMap) {
|
|
879
|
+
if (param.required) {
|
|
880
|
+
lines.push(`\tfor k, v := range params.${goField} {`);
|
|
881
|
+
lines.push(`\t\tquery.Set(fmt.Sprintf("${param.name}[%s]", k), fmt.Sprintf("%v", v))`);
|
|
882
|
+
lines.push('\t}');
|
|
613
883
|
} else {
|
|
614
|
-
// Slices and maps are reference types in Go — nil-able without pointer wrapping
|
|
615
|
-
const isRefType = field.type.kind === 'array' || field.type.kind === 'map';
|
|
616
|
-
const valueExpr = isRefType ? `params.${goField}` : `*params.${goField}`;
|
|
617
884
|
lines.push(`\tif params.${goField} != nil {`);
|
|
618
|
-
lines.push(`\t\
|
|
885
|
+
lines.push(`\t\tfor k, v := range params.${goField} {`);
|
|
886
|
+
lines.push(`\t\t\tquery.Set(fmt.Sprintf("${param.name}[%s]", k), fmt.Sprintf("%v", v))`);
|
|
887
|
+
lines.push('\t\t}');
|
|
619
888
|
lines.push('\t}');
|
|
620
889
|
}
|
|
890
|
+
} else if (param.required) {
|
|
891
|
+
lines.push(`\tquery.Set("${param.name}", ${formatQueryValue(`params.${goField}`, param.type)})`);
|
|
892
|
+
} else {
|
|
893
|
+
const isRefType = param.type.kind === 'array';
|
|
894
|
+
const valueExpr = isRefType ? `params.${goField}` : `*params.${goField}`;
|
|
895
|
+
lines.push(`\tif params.${goField} != nil {`);
|
|
896
|
+
lines.push(`\t\tquery.Set("${param.name}", ${formatQueryValue(valueExpr, param.type)})`);
|
|
897
|
+
lines.push('\t}');
|
|
621
898
|
}
|
|
622
899
|
}
|
|
623
900
|
}
|
|
624
901
|
|
|
902
|
+
lines.push(`\treturn s.client.buildURL(${pathExpr}, query, opts)`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Emit a private typed body struct for a non-GET operation that has hidden
|
|
907
|
+
* params. Field order matches how `emitBodyWithHiddenParams` constructs the
|
|
908
|
+
* body at the call site: defaults → required exposed → inferred → optional
|
|
909
|
+
* exposed. Mirrors `emitWrapperBodyStruct` in `wrappers.ts`.
|
|
910
|
+
*/
|
|
911
|
+
function emitHiddenParamsBodyStruct(
|
|
912
|
+
lines: string[],
|
|
913
|
+
method: string,
|
|
914
|
+
op: Operation,
|
|
915
|
+
ctx: EmitterContext,
|
|
916
|
+
resolvedOp: ResolvedOperation,
|
|
917
|
+
): void {
|
|
918
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
919
|
+
const structName = hiddenParamsBodyStructName(method);
|
|
920
|
+
|
|
921
|
+
const bodyModel =
|
|
922
|
+
op.requestBody?.kind === 'model'
|
|
923
|
+
? ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name)
|
|
924
|
+
: undefined;
|
|
925
|
+
|
|
926
|
+
lines.push(`// ${structName} is the JSON request body for ${method}.`);
|
|
927
|
+
lines.push(`type ${structName} struct {`);
|
|
928
|
+
|
|
929
|
+
// Constant defaults (always sent — no omitempty so the wire format is deterministic)
|
|
930
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
931
|
+
const goField = fieldName(key);
|
|
932
|
+
const goType = typeof value === 'boolean' ? 'bool' : typeof value === 'number' ? 'int' : 'string';
|
|
933
|
+
lines.push(`\t${goField} ${goType} \`json:"${key}"\``);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Required exposed body fields
|
|
937
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
938
|
+
if (bodyModel) {
|
|
939
|
+
for (const field of bodyModel.fields) {
|
|
940
|
+
if (hidden.has(field.name)) continue;
|
|
941
|
+
if (groupedParamNames.has(field.name)) continue;
|
|
942
|
+
if (!field.required) continue;
|
|
943
|
+
const goField = fieldName(field.name);
|
|
944
|
+
const goType = mapTypeRef(field.type);
|
|
945
|
+
lines.push(`\t${goField} ${goType} \`json:"${field.name}"\``);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Inferred fields from client config (omitempty drops empty strings)
|
|
950
|
+
for (const inferred of getOpInferFromClient(resolvedOp)) {
|
|
951
|
+
const goField = fieldName(inferred);
|
|
952
|
+
lines.push(`\t${goField} string \`json:"${inferred},omitempty"\``);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Optional exposed body fields (pointer/slice/map + omitempty)
|
|
956
|
+
if (bodyModel) {
|
|
957
|
+
for (const field of bodyModel.fields) {
|
|
958
|
+
if (hidden.has(field.name)) continue;
|
|
959
|
+
if (groupedParamNames.has(field.name)) continue;
|
|
960
|
+
if (field.required) continue;
|
|
961
|
+
const goField = fieldName(field.name);
|
|
962
|
+
const goType = makeOptional(mapTypeRef(field.type));
|
|
963
|
+
lines.push(`\t${goField} ${goType} \`json:"${field.name},omitempty"\``);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
lines.push('}');
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Emit method body for non-GET operations that have hidden params (defaults
|
|
972
|
+
* / inferFromClient). Builds a typed body struct (declared by
|
|
973
|
+
* `emitHiddenParamsBodyStruct`) so the wire format is deterministic and
|
|
974
|
+
* statically typed.
|
|
975
|
+
*/
|
|
976
|
+
function emitBodyWithHiddenParams(
|
|
977
|
+
lines: string[],
|
|
978
|
+
op: Operation,
|
|
979
|
+
pathExpr: string,
|
|
980
|
+
plan: OperationPlan,
|
|
981
|
+
ctx: EmitterContext,
|
|
982
|
+
resolvedOp: ResolvedOperation,
|
|
983
|
+
paramsType: string | null,
|
|
984
|
+
_isPaginated: boolean,
|
|
985
|
+
isDelete: boolean,
|
|
986
|
+
isArrayResponse: boolean,
|
|
987
|
+
method: string,
|
|
988
|
+
): void {
|
|
989
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
990
|
+
const bodyType = hiddenParamsBodyStructName(method);
|
|
991
|
+
|
|
992
|
+
const bodyModel =
|
|
993
|
+
op.requestBody?.kind === 'model'
|
|
994
|
+
? ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name)
|
|
995
|
+
: undefined;
|
|
996
|
+
|
|
997
|
+
// Build typed body struct literal — defaults + required user fields first
|
|
998
|
+
lines.push(`\tbody := ${bodyType}{`);
|
|
999
|
+
|
|
1000
|
+
for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
|
|
1001
|
+
lines.push(`\t\t${fieldName(key)}: ${goLiteral(value as string | number | boolean)},`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (paramsType && bodyModel) {
|
|
1005
|
+
for (const field of bodyModel.fields) {
|
|
1006
|
+
if (hidden.has(field.name)) continue;
|
|
1007
|
+
if (!field.required) continue;
|
|
1008
|
+
const goField = fieldName(field.name);
|
|
1009
|
+
lines.push(`\t\t${goField}: params.${goField},`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
lines.push('\t}');
|
|
1014
|
+
|
|
1015
|
+
// Inferred fields from client config — omitempty drops empty values
|
|
1016
|
+
for (const inferred of getOpInferFromClient(resolvedOp)) {
|
|
1017
|
+
const goField = fieldName(inferred);
|
|
1018
|
+
lines.push(`\tbody.${goField} = ${clientFieldExpression(inferred)}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Optional exposed body fields — copy through; omitempty drops nil/empty
|
|
1022
|
+
if (paramsType && bodyModel) {
|
|
1023
|
+
for (const field of bodyModel.fields) {
|
|
1024
|
+
if (hidden.has(field.name)) continue;
|
|
1025
|
+
if (field.required) continue;
|
|
1026
|
+
const goField = fieldName(field.name);
|
|
1027
|
+
lines.push(`\tbody.${goField} = params.${goField}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
625
1031
|
// Determine query arg (visible query params from struct)
|
|
626
1032
|
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
627
1033
|
const queryArg = hasVisibleQueryParams ? 'params' : 'nil';
|
|
@@ -725,7 +1131,9 @@ function buildPathExpr(op: Operation): string {
|
|
|
725
1131
|
const args: string[] = [];
|
|
726
1132
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
727
1133
|
fmtStr = fmtStr.replace(`{${p.name}}`, '%s');
|
|
728
|
-
|
|
1134
|
+
const varName = lowerFirst(fieldName(p.name));
|
|
1135
|
+
const needsCast = p.type.kind !== 'primitive' || p.type.type !== 'string';
|
|
1136
|
+
args.push(needsCast ? `url.PathEscape(string(${varName}))` : `url.PathEscape(${varName})`);
|
|
729
1137
|
}
|
|
730
1138
|
return `fmt.Sprintf("${fmtStr}", ${args.join(', ')})`;
|
|
731
1139
|
}
|
|
@@ -812,6 +1220,59 @@ function lowerFirstDesc(s: string): string {
|
|
|
812
1220
|
return lowerFirstForDoc(s);
|
|
813
1221
|
}
|
|
814
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* Build the Go literal for the pagination defaults map passed as the last
|
|
1225
|
+
* argument to newIterator. Collects `default` values from pagination-related
|
|
1226
|
+
* query params (limit, order) and returns either `nil` or
|
|
1227
|
+
* `map[string]string{"limit": "10", "order": "desc"}`.
|
|
1228
|
+
*/
|
|
1229
|
+
function paginationDefaultsLiteral(op: Operation): string {
|
|
1230
|
+
const PAGINATION_DEFAULTS = ['limit', 'order'];
|
|
1231
|
+
const entries: string[] = [];
|
|
1232
|
+
for (const name of PAGINATION_DEFAULTS) {
|
|
1233
|
+
const param = op.queryParams.find((qp) => qp.name === name);
|
|
1234
|
+
if (param?.default != null) {
|
|
1235
|
+
entries.push(`"${name}": "${param.default}"`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (entries.length === 0) return 'nil';
|
|
1239
|
+
return `map[string]string{${entries.join(', ')}}`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Build the godoc summary line for a method. When every PascalCase word
|
|
1244
|
+
* in the method name matches the leading words of the summary
|
|
1245
|
+
* (case-insensitive), those words are stripped to avoid stutter:
|
|
1246
|
+
*
|
|
1247
|
+
* godocSummary("Check", "Check authorization") → "Check authorization"
|
|
1248
|
+
* godocSummary("Delete", "Delete an API key") → "Delete an API key"
|
|
1249
|
+
* godocSummary("VerifyEmail", "Verify email") → "VerifyEmail"
|
|
1250
|
+
* godocSummary("GetJWKS", "Get JWKS") → "GetJWKS"
|
|
1251
|
+
*
|
|
1252
|
+
* When the summary words diverge from the method name, nothing is stripped:
|
|
1253
|
+
*
|
|
1254
|
+
* godocSummary("AssignRole", "Assign a role")
|
|
1255
|
+
* → "AssignRole assign a role"
|
|
1256
|
+
* godocSummary("ListOrganizationMembershipResources", "List resources for organization membership")
|
|
1257
|
+
* → "ListOrganizationMembershipResources list resources for organization membership"
|
|
1258
|
+
*/
|
|
1259
|
+
function godocSummary(method: string, summary: string): string {
|
|
1260
|
+
const methodWords = method.match(/[A-Z]+(?:[a-z]+|(?=[A-Z]|$))|[A-Z]?[a-z]+|[0-9]+/g) ?? [method];
|
|
1261
|
+
const summaryWords = summary.split(/\s+/);
|
|
1262
|
+
|
|
1263
|
+
// Check whether all method words match the leading summary words.
|
|
1264
|
+
if (methodWords.length <= summaryWords.length) {
|
|
1265
|
+
const allMatch = methodWords.every((mw, i) => mw.toLowerCase() === summaryWords[i].toLowerCase());
|
|
1266
|
+
if (allMatch) {
|
|
1267
|
+
const rest = summaryWords.slice(methodWords.length).join(' ');
|
|
1268
|
+
if (rest) return `${method} ${lowerFirstDesc(rest)}`;
|
|
1269
|
+
return method;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return `${method} ${lowerFirstDesc(summary)}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
815
1276
|
function singularizePascal(name: string): string {
|
|
816
1277
|
if (name.endsWith('ies')) {
|
|
817
1278
|
return `${name.slice(0, -3)}y`;
|
|
@@ -823,5 +1284,5 @@ function singularizePascal(name: string): string {
|
|
|
823
1284
|
}
|
|
824
1285
|
|
|
825
1286
|
function serviceTypeName(name: string): string {
|
|
826
|
-
return `${
|
|
1287
|
+
return `${className(singularizePascal(name))}Service`;
|
|
827
1288
|
}
|