@workos/oagen-emitters 0.2.1 → 0.4.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/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14549 -3385
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +328 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +45 -12
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +84 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +179 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +4 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +128 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +3 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +527 -397
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +69 -19
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +179 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +279 -0
- package/src/php/resources.ts +636 -0
- package/src/php/tests.ts +609 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +152 -0
- package/src/python/client.ts +345 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +189 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +472 -0
- package/src/shared/naming-utils.ts +154 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +70 -0
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +92 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +315 -84
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +95 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +682 -0
- package/test/php/tests.test.ts +185 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
package/src/go/models.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Model, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { mapTypeRef } from './type-map.js';
|
|
3
|
+
import { className, fieldName } from './naming.js';
|
|
4
|
+
import { lowerFirstForDoc, fieldDocComment, articleFor } from '../shared/naming-utils.js';
|
|
5
|
+
|
|
6
|
+
// Import and re-export shared model detection utilities
|
|
7
|
+
import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
|
|
8
|
+
export { isListWrapperModel, isListMetadataModel };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate Go struct definitions from IR Models.
|
|
12
|
+
* All models go into a single models.go file for the flat package.
|
|
13
|
+
*/
|
|
14
|
+
export function generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
15
|
+
if (models.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
const files: GeneratedFile[] = [];
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
|
|
20
|
+
lines.push(`package ${ctx.namespace}`);
|
|
21
|
+
lines.push('');
|
|
22
|
+
|
|
23
|
+
// Build structural hash for deduplication
|
|
24
|
+
const modelHashMap = new Map<string, string>();
|
|
25
|
+
const hashGroups = new Map<string, string[]>();
|
|
26
|
+
for (const model of models) {
|
|
27
|
+
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
28
|
+
const hash = structuralHash(model);
|
|
29
|
+
modelHashMap.set(model.name, hash);
|
|
30
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
31
|
+
hashGroups.get(hash)!.push(model.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pick canonical for each duplicate group.
|
|
35
|
+
// Empty structs (hash '') are now properly populated by oneOf flattening,
|
|
36
|
+
// so we still skip aliasing them to avoid aliasing truly empty structs.
|
|
37
|
+
const aliasOf = new Map<string, string>();
|
|
38
|
+
for (const [hash, names] of hashGroups) {
|
|
39
|
+
if (names.length <= 1) continue;
|
|
40
|
+
if (hash === '') continue;
|
|
41
|
+
const sorted = [...names].sort();
|
|
42
|
+
const canonical = sorted[0];
|
|
43
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
44
|
+
aliasOf.set(sorted[i], canonical);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const batchedAliases = new Set<string>();
|
|
49
|
+
for (const model of models) {
|
|
50
|
+
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
51
|
+
|
|
52
|
+
const structName = className(model.name);
|
|
53
|
+
|
|
54
|
+
// If this model is a dedup alias, emit a type alias.
|
|
55
|
+
// For large alias groups (5+), use a compact batch declaration.
|
|
56
|
+
const canonicalName = aliasOf.get(model.name);
|
|
57
|
+
if (canonicalName) {
|
|
58
|
+
// Check if this alias is part of a batch that was already emitted
|
|
59
|
+
if (batchedAliases.has(model.name)) continue;
|
|
60
|
+
|
|
61
|
+
const canonicalStruct = className(canonicalName);
|
|
62
|
+
const hash = modelHashMap.get(model.name)!;
|
|
63
|
+
const groupNames = hashGroups.get(hash) ?? [];
|
|
64
|
+
const aliases = groupNames.filter((n) => aliasOf.has(n));
|
|
65
|
+
|
|
66
|
+
if (aliases.length >= 5) {
|
|
67
|
+
// Batch emit all aliases for this group at once
|
|
68
|
+
for (const aliasName of aliases) {
|
|
69
|
+
batchedAliases.add(aliasName);
|
|
70
|
+
}
|
|
71
|
+
lines.push(`// The following types are structurally identical to ${canonicalStruct}.`);
|
|
72
|
+
lines.push('type (');
|
|
73
|
+
for (const aliasName of aliases) {
|
|
74
|
+
lines.push(`\t${className(aliasName)} = ${canonicalStruct}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(')');
|
|
77
|
+
lines.push('');
|
|
78
|
+
} else {
|
|
79
|
+
lines.push(`// ${structName} is an alias for ${canonicalStruct}.`);
|
|
80
|
+
lines.push(`type ${structName} = ${canonicalStruct}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Emit struct
|
|
87
|
+
if (model.description) {
|
|
88
|
+
const descLines = model.description.split('\n').filter((l) => l.trim());
|
|
89
|
+
lines.push(`// ${structName} ${lowerFirst(descLines[0])}`);
|
|
90
|
+
for (let i = 1; i < descLines.length; i++) {
|
|
91
|
+
lines.push(`// ${descLines[i].trim()}`);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
const humanized = humanize(model.name);
|
|
95
|
+
lines.push(`// ${structName} represents ${articleFor(humanized)} ${humanized}.`);
|
|
96
|
+
}
|
|
97
|
+
lines.push(`type ${structName} struct {`);
|
|
98
|
+
|
|
99
|
+
// Deduplicate fields by Go field name
|
|
100
|
+
const seenFieldNames = new Set<string>();
|
|
101
|
+
for (const field of model.fields) {
|
|
102
|
+
const goFieldName = fieldName(field.name);
|
|
103
|
+
if (seenFieldNames.has(goFieldName)) continue;
|
|
104
|
+
seenFieldNames.add(goFieldName);
|
|
105
|
+
|
|
106
|
+
const isOptional = !field.required;
|
|
107
|
+
const goType = isOptional ? makeOptional(mapTypeRef(field.type)) : mapTypeRef(field.type);
|
|
108
|
+
|
|
109
|
+
const jsonTag = field.required ? `json:"${field.name}"` : `json:"${field.name},omitempty"`;
|
|
110
|
+
|
|
111
|
+
if (field.description) {
|
|
112
|
+
const fdLines = field.description.split('\n').filter((l) => l.trim());
|
|
113
|
+
lines.push(`\t// ${fieldDocComment(goFieldName, fdLines[0])}`);
|
|
114
|
+
for (let i = 1; i < fdLines.length; i++) {
|
|
115
|
+
lines.push(`\t// ${fdLines[i].trim()}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (field.deprecated) {
|
|
119
|
+
if (field.description) lines.push(`\t//`);
|
|
120
|
+
const deprecationReason = extractDeprecationReason(field.description);
|
|
121
|
+
lines.push(`\t// Deprecated: ${deprecationReason}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push(`\t${goFieldName} ${goType} \`${jsonTag}\``);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lines.push('}');
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Emit shared PaginationParams struct for list operations to embed
|
|
131
|
+
lines.push('// PaginationParams contains common pagination parameters for list operations.');
|
|
132
|
+
lines.push('type PaginationParams struct {');
|
|
133
|
+
lines.push('\t// Before is a cursor for reverse pagination.');
|
|
134
|
+
lines.push('\tBefore *string `url:"before,omitempty" json:"-"`');
|
|
135
|
+
lines.push('\t// After is a cursor for forward pagination.');
|
|
136
|
+
lines.push('\tAfter *string `url:"after,omitempty" json:"-"`');
|
|
137
|
+
lines.push('\t// Limit is the maximum number of items to return per page.');
|
|
138
|
+
lines.push('\tLimit *int `url:"limit,omitempty" json:"-"`');
|
|
139
|
+
lines.push('\t// Order is the sort order for results (asc or desc).');
|
|
140
|
+
lines.push('\tOrder *string `url:"order,omitempty" json:"-"`');
|
|
141
|
+
lines.push('}');
|
|
142
|
+
lines.push('');
|
|
143
|
+
|
|
144
|
+
files.push({
|
|
145
|
+
path: 'models.go',
|
|
146
|
+
content: lines.join('\n'),
|
|
147
|
+
overwriteExisting: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Make a Go type optional (pointer) if it isn't already.
|
|
155
|
+
*/
|
|
156
|
+
function makeOptional(goType: string): string {
|
|
157
|
+
if (goType.startsWith('*') || goType.startsWith('[]') || goType.startsWith('map[')) {
|
|
158
|
+
return goType;
|
|
159
|
+
}
|
|
160
|
+
return `*${goType}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function structuralHash(model: Model): string {
|
|
164
|
+
return model.fields
|
|
165
|
+
.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`)
|
|
166
|
+
.sort()
|
|
167
|
+
.join('|');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Known acronyms to preserve as single tokens during humanization. */
|
|
171
|
+
const HUMANIZE_ACRONYMS: [RegExp, string][] = [
|
|
172
|
+
[/OAuth/g, 'OAUTH_ACRN'],
|
|
173
|
+
[/URN/g, 'URN_ACRN'],
|
|
174
|
+
[/IETF/g, 'IETF_ACRN'],
|
|
175
|
+
[/API/g, 'API_ACRN'],
|
|
176
|
+
[/SSO/g, 'SSO_ACRN'],
|
|
177
|
+
[/PKCE/g, 'PKCE_ACRN'],
|
|
178
|
+
[/JWT/g, 'JWT_ACRN'],
|
|
179
|
+
[/MFA/g, 'MFA_ACRN'],
|
|
180
|
+
[/TOTP/g, 'TOTP_ACRN'],
|
|
181
|
+
[/SAML/g, 'SAML_ACRN'],
|
|
182
|
+
[/SCIM/g, 'SCIM_ACRN'],
|
|
183
|
+
[/OIDC/g, 'OIDC_ACRN'],
|
|
184
|
+
[/CORS/g, 'CORS_ACRN'],
|
|
185
|
+
[/RBAC/g, 'RBAC_ACRN'],
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const HUMANIZE_RESTORE: [RegExp, string][] = [
|
|
189
|
+
[/oauth_acrn/g, 'OAuth'],
|
|
190
|
+
[/urn_acrn/g, 'URN'],
|
|
191
|
+
[/ietf_acrn/g, 'IETF'],
|
|
192
|
+
[/api_acrn/g, 'API'],
|
|
193
|
+
[/sso_acrn/g, 'SSO'],
|
|
194
|
+
[/pkce_acrn/g, 'PKCE'],
|
|
195
|
+
[/jwt_acrn/g, 'JWT'],
|
|
196
|
+
[/mfa_acrn/g, 'MFA'],
|
|
197
|
+
[/totp_acrn/g, 'TOTP'],
|
|
198
|
+
[/saml_acrn/g, 'SAML'],
|
|
199
|
+
[/scim_acrn/g, 'SCIM'],
|
|
200
|
+
[/oidc_acrn/g, 'OIDC'],
|
|
201
|
+
[/cors_acrn/g, 'CORS'],
|
|
202
|
+
[/rbac_acrn/g, 'RBAC'],
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
function humanize(name: string): string {
|
|
206
|
+
// Replace known acronyms with placeholders before splitting
|
|
207
|
+
let s = name;
|
|
208
|
+
for (const [pattern, replacement] of HUMANIZE_ACRONYMS) {
|
|
209
|
+
s = s.replace(pattern, replacement);
|
|
210
|
+
}
|
|
211
|
+
// Split camelCase/PascalCase into words
|
|
212
|
+
let result = s.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
213
|
+
result = result.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
214
|
+
result = result.toLowerCase();
|
|
215
|
+
// Restore acronyms
|
|
216
|
+
for (const [pattern, replacement] of HUMANIZE_RESTORE) {
|
|
217
|
+
result = result.replace(pattern, replacement);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function lowerFirst(s: string): string {
|
|
223
|
+
return lowerFirstForDoc(s);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract a deprecation reason from a field description.
|
|
228
|
+
* Looks for patterns like "Use X instead", "Replaced by Y", etc.
|
|
229
|
+
* Falls back to a generic message if no migration guidance is found.
|
|
230
|
+
*/
|
|
231
|
+
function extractDeprecationReason(description?: string): string {
|
|
232
|
+
if (!description) return 'this field is deprecated.';
|
|
233
|
+
|
|
234
|
+
// Common patterns: "Use X instead", "Replaced by X", "Deprecated in favor of X"
|
|
235
|
+
const patterns = [
|
|
236
|
+
/\b(use\s+\S+(?:\s+\S+){0,3}\s+instead)\b/i,
|
|
237
|
+
/\b(replaced\s+by\s+\S+(?:\s+\S+){0,3})\b/i,
|
|
238
|
+
/\b(deprecated\s+in\s+favor\s+of\s+\S+(?:\s+\S+){0,3})\b/i,
|
|
239
|
+
/\b(prefer\s+\S+(?:\s+\S+){0,3})\b/i,
|
|
240
|
+
/\b(migrate\s+to\s+\S+(?:\s+\S+){0,3})\b/i,
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
for (const pattern of patterns) {
|
|
244
|
+
const match = description.match(pattern);
|
|
245
|
+
if (match) {
|
|
246
|
+
let reason = match[1].trim();
|
|
247
|
+
// Ensure it ends with a period
|
|
248
|
+
if (!reason.endsWith('.')) reason += '.';
|
|
249
|
+
return reason;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return 'this field is deprecated.';
|
|
254
|
+
}
|
package/src/go/naming.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Operation, Service, EmitterContext } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase, toSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
|
|
4
|
+
import { stripUrnPrefix, applyAcronymFixes } from '../shared/naming-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Go-specific acronym extensions beyond the shared base set.
|
|
8
|
+
* Go convention requires ALL_CAPS for well-known initialisms.
|
|
9
|
+
*/
|
|
10
|
+
const GO_EXTRA_ACRONYM_FIXES: [RegExp, string][] = [
|
|
11
|
+
[/Jwks(?=[A-Z]|$)/g, 'JWKS'],
|
|
12
|
+
[/Totp(?=[A-Z]|$)/g, 'TOTP'],
|
|
13
|
+
[/Api(?=[A-Z]|$)/g, 'API'],
|
|
14
|
+
[/Urls(?=[A-Z]|$)/g, 'URLs'],
|
|
15
|
+
[/Url(?=[A-Z]|$)/g, 'URL'],
|
|
16
|
+
[/Uris(?=[A-Z]|$)/g, 'URIs'],
|
|
17
|
+
[/Uri(?=[A-Z]|$)/g, 'URI'],
|
|
18
|
+
[/Http(?=[A-Z]|$)/g, 'HTTP'],
|
|
19
|
+
[/Uuid(?=[A-Z]|$)/g, 'UUID'],
|
|
20
|
+
[/Json(?=[A-Z]|$)/g, 'JSON'],
|
|
21
|
+
[/Html(?=[A-Z]|$)/g, 'HTML'],
|
|
22
|
+
[/Ip(?=[A-Z]|$)/g, 'IP'],
|
|
23
|
+
[/Pkce/g, 'PKCE'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fix trailing "Id" to "ID" in Go convention.
|
|
28
|
+
* Must be applied after PascalCase and other acronym fixes.
|
|
29
|
+
*/
|
|
30
|
+
function fixTrailingId(s: string): string {
|
|
31
|
+
// Fix "Ids" → "IDs" first (plural), then "Id" → "ID" (singular)
|
|
32
|
+
let result = s.replace(/Ids(?=[A-Z]|$)/g, 'IDs');
|
|
33
|
+
result = result.replace(/Id(?=[A-Z]|$)/g, 'ID');
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Apply all Go acronym conventions to a PascalCase string. */
|
|
38
|
+
function applyAcronyms(s: string): string {
|
|
39
|
+
let result = applyAcronymFixes(s, GO_EXTRA_ACRONYM_FIXES);
|
|
40
|
+
result = fixTrailingId(result);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** PascalCase type/struct name with Go acronym conventions. */
|
|
45
|
+
export function className(name: string): string {
|
|
46
|
+
return applyAcronyms(toPascalCase(stripUrnPrefix(name)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** snake_case file name (without extension). */
|
|
50
|
+
export function fileName(name: string): string {
|
|
51
|
+
return toSnakeCase(stripUrnPrefix(name));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** PascalCase exported method name with Go acronym conventions. */
|
|
55
|
+
export function methodName(name: string): string {
|
|
56
|
+
return applyAcronyms(toPascalCase(name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** PascalCase exported field name with Go acronym conventions. */
|
|
60
|
+
export function fieldName(name: string): string {
|
|
61
|
+
return applyAcronyms(toPascalCase(name));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** snake_case module/directory name. */
|
|
65
|
+
export function moduleName(name: string): string {
|
|
66
|
+
return toSnakeCase(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** snake_case property name for service accessors on the client. */
|
|
70
|
+
export function servicePropertyName(name: string): string {
|
|
71
|
+
return unexportedName(className(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Lower-camel identifier with Go acronym conventions preserved. */
|
|
75
|
+
export function unexportedName(name: string): string {
|
|
76
|
+
const exported = className(name);
|
|
77
|
+
if (!exported) return exported;
|
|
78
|
+
|
|
79
|
+
const initialism = exported.match(/^[A-Z]+(?=[A-Z][a-z]|[0-9]|$)/)?.[0];
|
|
80
|
+
if (initialism) {
|
|
81
|
+
return initialism.toLowerCase() + exported.slice(initialism.length);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return exported.charAt(0).toLowerCase() + exported.slice(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Resolve the effective service name using resolved operations. */
|
|
88
|
+
export function resolveServiceName(service: Service, ctx: EmitterContext): string {
|
|
89
|
+
return resolveClassName(service, ctx);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Build a map from IR service name to resolved service name. */
|
|
93
|
+
export function buildServiceNameMap(services: Service[], ctx: EmitterContext): Map<string, string> {
|
|
94
|
+
const map = new Map<string, string>();
|
|
95
|
+
for (const service of services) {
|
|
96
|
+
map.set(service.name, resolveServiceName(service, ctx));
|
|
97
|
+
}
|
|
98
|
+
return map;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Resolve the output directory for a service. */
|
|
102
|
+
export function resolveServiceDir(resolvedServiceName: string): string {
|
|
103
|
+
return moduleName(resolvedServiceName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Resolve the SDK method name for an operation, using resolved operations first. */
|
|
107
|
+
export function resolveMethodName(op: Operation, _service: Service, ctx: EmitterContext): string {
|
|
108
|
+
const lookup = buildResolvedLookup(ctx);
|
|
109
|
+
const resolved = lookupMethodName(op, lookup);
|
|
110
|
+
if (resolved) return trimMountedResourceFromMethod(methodName(resolved), resolveClassName(_service, ctx));
|
|
111
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
112
|
+
const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
|
|
113
|
+
if (existing) return trimMountedResourceFromMethod(methodName(existing.methodName), resolveClassName(_service, ctx));
|
|
114
|
+
return trimMountedResourceFromMethod(methodName(op.name), resolveClassName(_service, ctx));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolve the SDK class name for a service using resolved operations' mountOn. */
|
|
118
|
+
export function resolveClassName(service: Service, ctx: EmitterContext): string {
|
|
119
|
+
for (const r of ctx.resolvedOperations ?? []) {
|
|
120
|
+
if (r.service.name === service.name) return className(r.mountOn);
|
|
121
|
+
}
|
|
122
|
+
if (ctx.overlayLookup?.methodByOperation) {
|
|
123
|
+
for (const op of service.operations) {
|
|
124
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
125
|
+
const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
|
|
126
|
+
if (existing) return className(existing.className);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return className(service.name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Build a map from IR service name to mount-target directory name. */
|
|
133
|
+
export function buildMountDirMap(ctx: EmitterContext): Map<string, string> {
|
|
134
|
+
const map = new Map<string, string>();
|
|
135
|
+
for (const service of ctx.spec.services) {
|
|
136
|
+
const target = getMountTarget(service, ctx);
|
|
137
|
+
map.set(service.name, moduleName(target));
|
|
138
|
+
}
|
|
139
|
+
return map;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function splitPascalWords(name: string): string[] {
|
|
143
|
+
return name.match(/[A-Z]+(?:[a-z]+|(?=[A-Z]|$))|[A-Z]?[a-z]+|[0-9]+/g) ?? [name];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function singularize(word: string): string {
|
|
147
|
+
if (word.endsWith('ies') && word.length > 3) {
|
|
148
|
+
return `${word.slice(0, -3)}y`;
|
|
149
|
+
}
|
|
150
|
+
if (word.endsWith('s') && !word.endsWith('ss')) {
|
|
151
|
+
return word.slice(0, -1);
|
|
152
|
+
}
|
|
153
|
+
return word;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function wordsMatch(left: string, right: string): boolean {
|
|
157
|
+
return singularize(left.toLowerCase()) === singularize(right.toLowerCase());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function trimMountedResourceFromMethod(method: string, mountName: string): string {
|
|
161
|
+
const methodWords = splitPascalWords(method);
|
|
162
|
+
if (methodWords.length < 2) return method;
|
|
163
|
+
|
|
164
|
+
const mountWords = splitPascalWords(className(mountName));
|
|
165
|
+
if (mountWords.length === 0) return method;
|
|
166
|
+
|
|
167
|
+
let matched = 0;
|
|
168
|
+
while (
|
|
169
|
+
matched < mountWords.length &&
|
|
170
|
+
matched + 1 < methodWords.length &&
|
|
171
|
+
wordsMatch(methodWords[matched + 1], mountWords[matched])
|
|
172
|
+
) {
|
|
173
|
+
matched++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (matched === 0) return method;
|
|
177
|
+
|
|
178
|
+
return [methodWords[0], ...methodWords.slice(matched + 1)].join('');
|
|
179
|
+
}
|