@workos/oagen-emitters 0.14.4 → 0.15.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-BGVaMGqe.mjs → plugin-CO4RFgAW.mjs} +959 -251
- package/dist/plugin-CO4RFgAW.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- package/renovate.json +1 -61
- package/src/go/client.ts +1 -1
- package/src/go/enums.ts +77 -0
- package/src/kotlin/enums.ts +11 -4
- package/src/node/client.ts +119 -2
- package/src/node/discriminated-models.ts +8 -0
- package/src/node/field-plan.ts +64 -8
- package/src/node/index.ts +59 -3
- package/src/node/models.ts +73 -30
- package/src/node/naming.ts +14 -1
- package/src/node/node-overrides.ts +4 -37
- package/src/node/options.ts +29 -1
- package/src/node/resources.ts +533 -83
- package/src/node/tests.ts +108 -7
- package/src/php/fixtures.ts +4 -1
- package/src/php/models.ts +3 -1
- package/src/php/resources.ts +40 -11
- package/src/php/tests.ts +22 -12
- package/src/python/client.ts +0 -8
- package/src/python/enums.ts +41 -15
- package/src/python/fixtures.ts +23 -7
- package/src/python/models.ts +26 -5
- package/src/python/resources.ts +71 -3
- package/src/python/tests.ts +70 -12
- package/src/python/wrappers.ts +25 -4
- package/src/ruby/client.ts +0 -1
- package/src/rust/resources.ts +10 -7
- package/src/shared/non-spec-services.ts +0 -5
- package/test/go/enums.test.ts +24 -0
- package/test/node/resources.test.ts +11 -1
- package/test/node/tests.test.ts +3 -3
- package/test/php/client.test.ts +0 -1
- package/test/php/resources.test.ts +50 -0
- package/test/rust/resources.test.ts +9 -0
- package/dist/plugin-BGVaMGqe.mjs.map +0 -1
package/src/python/resources.ts
CHANGED
|
@@ -363,9 +363,16 @@ function emitMethodSignature(
|
|
|
363
363
|
: className(resolvedItem);
|
|
364
364
|
returnType = `${pageType}[${itemTypeName}]`;
|
|
365
365
|
} else if (isArrayResponse) {
|
|
366
|
-
|
|
366
|
+
const arrayItemModel = ctx.spec.models.find((m) => m.name === plan.responseModelName);
|
|
367
|
+
const arrayItemTypeName = (arrayItemModel as any)?.discriminator
|
|
368
|
+
? className(plan.responseModelName!) + 'Variant'
|
|
369
|
+
: className(plan.responseModelName!);
|
|
370
|
+
returnType = `List[${arrayItemTypeName}]`;
|
|
367
371
|
} else if (plan.responseModelName) {
|
|
368
|
-
|
|
372
|
+
const singleResponseModel = ctx.spec.models.find((m) => m.name === plan.responseModelName);
|
|
373
|
+
returnType = (singleResponseModel as any)?.discriminator
|
|
374
|
+
? className(plan.responseModelName) + 'Variant'
|
|
375
|
+
: className(plan.responseModelName);
|
|
369
376
|
} else {
|
|
370
377
|
returnType = 'None';
|
|
371
378
|
}
|
|
@@ -772,6 +779,10 @@ function emitMethodBody(
|
|
|
772
779
|
lines.push(' )');
|
|
773
780
|
} else if (plan.hasBody && op.requestBody) {
|
|
774
781
|
const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
|
|
782
|
+
const bodyResponseModelObj = plan.responseModelName
|
|
783
|
+
? ctx.spec.models.find((m) => m.name === plan.responseModelName)
|
|
784
|
+
: null;
|
|
785
|
+
const isBodyResponseDiscriminator = !!(bodyResponseModelObj as any)?.discriminator;
|
|
775
786
|
// Build body dict
|
|
776
787
|
const bodyGroupedParams = collectGroupedParamNames(op);
|
|
777
788
|
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
@@ -847,6 +858,26 @@ function emitMethodBody(
|
|
|
847
858
|
lines.push(' request_options=request_options,');
|
|
848
859
|
lines.push(' )');
|
|
849
860
|
lines.push(` return [${itemModel}.from_dict(cast(Dict[str, Any], item)) for item in raw]`);
|
|
861
|
+
} else if (isBodyResponseDiscriminator && responseModel !== 'None') {
|
|
862
|
+
const variantType = responseModel + 'Variant';
|
|
863
|
+
lines.push(` return cast(`);
|
|
864
|
+
lines.push(` ${variantType},`);
|
|
865
|
+
lines.push(` ${awaitPrefix}self._client.request(`);
|
|
866
|
+
lines.push(` method="${httpMethod}",`);
|
|
867
|
+
lines.push(` path=${pathStr},`);
|
|
868
|
+
lines.push(` body=${bodyVarName},`);
|
|
869
|
+
if (bodyHasParams) {
|
|
870
|
+
lines.push(' params=params,');
|
|
871
|
+
}
|
|
872
|
+
lines.push(
|
|
873
|
+
` model=${responseModel}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
|
|
874
|
+
);
|
|
875
|
+
if (plan.isIdempotentPost) {
|
|
876
|
+
lines.push(' idempotency_key=idempotency_key,');
|
|
877
|
+
}
|
|
878
|
+
lines.push(' request_options=request_options,');
|
|
879
|
+
lines.push(' )');
|
|
880
|
+
lines.push(' )');
|
|
850
881
|
} else {
|
|
851
882
|
const bodyReturnPrefix = responseModel !== 'None' ? 'return ' : '';
|
|
852
883
|
lines.push(` ${bodyReturnPrefix}${awaitPrefix}self._client.request(`);
|
|
@@ -868,6 +899,10 @@ function emitMethodBody(
|
|
|
868
899
|
} else {
|
|
869
900
|
// GET or similar with query params
|
|
870
901
|
const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
|
|
902
|
+
const getResponseModelObj = plan.responseModelName
|
|
903
|
+
? ctx.spec.models.find((m) => m.name === plan.responseModelName)
|
|
904
|
+
: null;
|
|
905
|
+
const isGetResponseDiscriminator = !!(getResponseModelObj as any)?.discriminator;
|
|
871
906
|
const getGroupedParams = collectGroupedParamNames(op);
|
|
872
907
|
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
873
908
|
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name) && !getGroupedParams.has(p.name));
|
|
@@ -937,6 +972,22 @@ function emitMethodBody(
|
|
|
937
972
|
lines.push(' request_options=request_options,');
|
|
938
973
|
lines.push(' )');
|
|
939
974
|
lines.push(` return [${itemModel}.from_dict(cast(Dict[str, Any], item)) for item in raw]`);
|
|
975
|
+
} else if (isGetResponseDiscriminator && responseModel !== 'None') {
|
|
976
|
+
const variantType = responseModel + 'Variant';
|
|
977
|
+
lines.push(` return cast(`);
|
|
978
|
+
lines.push(` ${variantType},`);
|
|
979
|
+
lines.push(` ${awaitPrefix}self._client.request(`);
|
|
980
|
+
lines.push(` method="${httpMethod}",`);
|
|
981
|
+
lines.push(` path=${pathStr},`);
|
|
982
|
+
if (emittedParams) {
|
|
983
|
+
lines.push(' params=params,');
|
|
984
|
+
}
|
|
985
|
+
lines.push(
|
|
986
|
+
` model=${responseModel}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
|
|
987
|
+
);
|
|
988
|
+
lines.push(' request_options=request_options,');
|
|
989
|
+
lines.push(' )');
|
|
990
|
+
lines.push(' )');
|
|
940
991
|
} else {
|
|
941
992
|
const returnPrefix = responseModel !== 'None' ? 'return ' : '';
|
|
942
993
|
lines.push(` ${returnPrefix}${awaitPrefix}self._client.request(`);
|
|
@@ -1061,6 +1112,13 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1061
1112
|
if (!listWrapperNames.has(plan.responseModelName) || !plan.isPaginated) {
|
|
1062
1113
|
modelImports.add(plan.responseModelName);
|
|
1063
1114
|
}
|
|
1115
|
+
// For discriminator (dispatcher) models also import the variant union type alias
|
|
1116
|
+
if (!plan.isPaginated) {
|
|
1117
|
+
const responseModelObj = ctx.spec.models.find((m) => m.name === plan.responseModelName);
|
|
1118
|
+
if ((responseModelObj as any)?.discriminator) {
|
|
1119
|
+
modelImports.add(plan.responseModelName + 'Variant');
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1064
1122
|
}
|
|
1065
1123
|
if (op.requestBody?.kind === 'model') {
|
|
1066
1124
|
const requestBodyRef = op.requestBody;
|
|
@@ -1161,13 +1219,23 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1161
1219
|
// Deduplicate: skip cross-service imports for models already available locally
|
|
1162
1220
|
const localSet = new Set(localModels);
|
|
1163
1221
|
|
|
1222
|
+
// Build a mapping from variant type aliases to their dispatcher's file name
|
|
1223
|
+
// (e.g. ConnectApplicationVariant -> connect_application, not connect_application_variant)
|
|
1224
|
+
const variantToFile = new Map<string, string>();
|
|
1225
|
+
for (const model of ctx.spec.models) {
|
|
1226
|
+
if ((model as any).discriminator) {
|
|
1227
|
+
variantToFile.set(model.name + 'Variant', fileName(model.name));
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1164
1231
|
if (localModels.length > 0) {
|
|
1165
1232
|
lines.push(`from .models import ${localModels.map((n) => className(n)).join(', ')}`);
|
|
1166
1233
|
}
|
|
1167
1234
|
for (const [csDir, names] of [...crossServiceModels].sort()) {
|
|
1168
1235
|
const unique = names.filter((n) => !localSet.has(n));
|
|
1169
1236
|
for (const n of unique) {
|
|
1170
|
-
|
|
1237
|
+
const filePath = variantToFile.get(n) ?? fileName(n);
|
|
1238
|
+
lines.push(`from ${ctx.namespace}.${dirToModule(csDir)}.models.${filePath} import ${className(n)}`);
|
|
1171
1239
|
}
|
|
1172
1240
|
}
|
|
1173
1241
|
|
package/src/python/tests.ts
CHANGED
|
@@ -167,7 +167,27 @@ function generateServiceTest(
|
|
|
167
167
|
const enumImports = new Set<string>();
|
|
168
168
|
for (const op of service.operations) {
|
|
169
169
|
const plan = planOperation(op);
|
|
170
|
-
if (plan.responseModelName)
|
|
170
|
+
if (plan.responseModelName) {
|
|
171
|
+
modelImports.add(plan.responseModelName);
|
|
172
|
+
// For non-paginated discriminated union responses, import the resolved variant class
|
|
173
|
+
if (!plan.isPaginated) {
|
|
174
|
+
const resolvedVariantClass = resolvePaginatedItemClass(plan.responseModelName, spec);
|
|
175
|
+
if (resolvedVariantClass && resolvedVariantClass !== className(plan.responseModelName)) {
|
|
176
|
+
const responseModel = spec.models.find((m) => m.name === plan.responseModelName);
|
|
177
|
+
const disc =
|
|
178
|
+
responseModel &&
|
|
179
|
+
((responseModel as any).discriminator as { property: string; mapping: Record<string, string> } | undefined);
|
|
180
|
+
if (disc) {
|
|
181
|
+
for (const variantName of Object.values(disc.mapping)) {
|
|
182
|
+
if (className(variantName) === resolvedVariantClass) {
|
|
183
|
+
modelImports.add(variantName);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
171
191
|
if (op.pagination?.itemType.kind === 'model') {
|
|
172
192
|
modelImports.add(op.pagination.itemType.name);
|
|
173
193
|
// Unwrap list wrapper to find the inner item model (may be a discriminated union)
|
|
@@ -220,11 +240,13 @@ function generateServiceTest(
|
|
|
220
240
|
}
|
|
221
241
|
}
|
|
222
242
|
|
|
223
|
-
// Filter out list wrapper models
|
|
243
|
+
// Filter out list wrapper models, but keep non-paginated response wrappers
|
|
244
|
+
const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
|
|
224
245
|
const actualImports = [...modelImports].filter((name) => {
|
|
225
246
|
const model = spec.models.find((m) => m.name === name);
|
|
226
247
|
if (!model) return true;
|
|
227
|
-
|
|
248
|
+
if (isListWrapperModel(model) && !nonPaginatedRefs.has(name)) return false;
|
|
249
|
+
return true;
|
|
228
250
|
});
|
|
229
251
|
|
|
230
252
|
// Group imports by their actual service directory (models may live in different services)
|
|
@@ -414,16 +436,33 @@ function generateServiceTest(
|
|
|
414
436
|
const fixtureName = `${fileName(modelName)}.json`;
|
|
415
437
|
const modelClass = className(modelName);
|
|
416
438
|
|
|
439
|
+
// For discriminated union responses, resolve to the concrete variant class
|
|
440
|
+
const resolvedClass = resolvePaginatedItemClass(modelName, spec) ?? modelClass;
|
|
441
|
+
// Pick assertable fields from the resolved variant, not the dispatcher
|
|
442
|
+
const resolvedModelName =
|
|
443
|
+
resolvedClass !== modelClass
|
|
444
|
+
? (() => {
|
|
445
|
+
const responseModel = spec.models.find((m) => m.name === modelName);
|
|
446
|
+
const disc = (responseModel as any)?.discriminator as { mapping: Record<string, string> } | undefined;
|
|
447
|
+
if (disc) {
|
|
448
|
+
for (const variantName of Object.values(disc.mapping)) {
|
|
449
|
+
if (className(variantName) === resolvedClass) return variantName;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return modelName;
|
|
453
|
+
})()
|
|
454
|
+
: modelName;
|
|
455
|
+
|
|
417
456
|
lines.push(` def test_${method}(self, workos, httpx_mock):`);
|
|
418
457
|
lines.push(` httpx_mock.add_response(`);
|
|
419
458
|
lines.push(` json=load_fixture("${fixtureName}"),`);
|
|
420
459
|
lines.push(' )');
|
|
421
460
|
const args = buildTestArgs(op, spec, hiddenParams);
|
|
422
461
|
lines.push(` result = workos.${propName}.${method}(${args})`);
|
|
423
|
-
lines.push(` assert isinstance(result, ${
|
|
462
|
+
lines.push(` assert isinstance(result, ${resolvedClass})`);
|
|
424
463
|
|
|
425
464
|
// Field-value assertions: verify at least 2 scalar fields from fixture
|
|
426
|
-
const assertFields = pickAssertableFields(
|
|
465
|
+
const assertFields = pickAssertableFields(resolvedModelName, spec);
|
|
427
466
|
for (const af of assertFields) {
|
|
428
467
|
const op_ = af.isBool ? 'is' : '==';
|
|
429
468
|
lines.push(` assert result.${af.field} ${op_} ${af.value}`);
|
|
@@ -706,12 +745,29 @@ function generateServiceTest(
|
|
|
706
745
|
const modelName = plan.responseModelName;
|
|
707
746
|
const fixtureName = `${fileName(modelName)}.json`;
|
|
708
747
|
const modelClass = className(modelName);
|
|
748
|
+
|
|
749
|
+
// For discriminated union responses, resolve to the concrete variant class
|
|
750
|
+
const asyncResolvedClass = resolvePaginatedItemClass(modelName, spec) ?? modelClass;
|
|
751
|
+
const asyncResolvedModelName =
|
|
752
|
+
asyncResolvedClass !== modelClass
|
|
753
|
+
? (() => {
|
|
754
|
+
const responseModel = spec.models.find((m) => m.name === modelName);
|
|
755
|
+
const disc = (responseModel as any)?.discriminator as { mapping: Record<string, string> } | undefined;
|
|
756
|
+
if (disc) {
|
|
757
|
+
for (const variantName of Object.values(disc.mapping)) {
|
|
758
|
+
if (className(variantName) === asyncResolvedClass) return variantName;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return modelName;
|
|
762
|
+
})()
|
|
763
|
+
: modelName;
|
|
764
|
+
|
|
709
765
|
pushAsyncTestDef(lines, ` async def test_${method}(self, async_workos, httpx_mock):`);
|
|
710
766
|
lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
|
|
711
767
|
lines.push(` result = await async_workos.${propName}.${method}(${asyncArgs})`);
|
|
712
|
-
lines.push(` assert isinstance(result, ${
|
|
768
|
+
lines.push(` assert isinstance(result, ${asyncResolvedClass})`);
|
|
713
769
|
// Field-value assertions
|
|
714
|
-
const assertFields = pickAssertableFields(
|
|
770
|
+
const assertFields = pickAssertableFields(asyncResolvedModelName, spec);
|
|
715
771
|
for (const af of assertFields) {
|
|
716
772
|
const op_ = af.isBool ? 'is' : '==';
|
|
717
773
|
lines.push(` assert result.${af.field} ${op_} ${af.value}`);
|
|
@@ -889,7 +945,9 @@ function emitWrapperTests(
|
|
|
889
945
|
for (const wrapper of r.wrappers) {
|
|
890
946
|
const method = wrapper.name;
|
|
891
947
|
const wrapperParams = resolveWrapperParams(wrapper, ctx);
|
|
892
|
-
const
|
|
948
|
+
const resolvedResponseClass = wrapper.responseModelName
|
|
949
|
+
? (resolvePaginatedItemClass(wrapper.responseModelName, spec) ?? className(wrapper.responseModelName))
|
|
950
|
+
: null;
|
|
893
951
|
const fixtureName = wrapper.responseModelName ? `${fileName(wrapper.responseModelName)}.json` : null;
|
|
894
952
|
|
|
895
953
|
// Build test args for required wrapper params
|
|
@@ -908,8 +966,8 @@ function emitWrapperTests(
|
|
|
908
966
|
if (fixtureName) {
|
|
909
967
|
lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
|
|
910
968
|
lines.push(` result = await async_workos.${propName}.${method}(${args})`);
|
|
911
|
-
if (
|
|
912
|
-
lines.push(` assert isinstance(result, ${
|
|
969
|
+
if (resolvedResponseClass) {
|
|
970
|
+
lines.push(` assert isinstance(result, ${resolvedResponseClass})`);
|
|
913
971
|
}
|
|
914
972
|
} else {
|
|
915
973
|
lines.push(' httpx_mock.add_response(json={})');
|
|
@@ -920,8 +978,8 @@ function emitWrapperTests(
|
|
|
920
978
|
if (fixtureName) {
|
|
921
979
|
lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
|
|
922
980
|
lines.push(` result = workos.${propName}.${method}(${args})`);
|
|
923
|
-
if (
|
|
924
|
-
lines.push(` assert isinstance(result, ${
|
|
981
|
+
if (resolvedResponseClass) {
|
|
982
|
+
lines.push(` assert isinstance(result, ${resolvedResponseClass})`);
|
|
925
983
|
}
|
|
926
984
|
} else {
|
|
927
985
|
lines.push(' httpx_mock.add_response(json={})');
|
package/src/python/wrappers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
|
|
1
|
+
import type { EmitterContext, ResolvedOperation, ResolvedWrapper, Model } from '@workos/oagen';
|
|
2
2
|
import { toSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { className, fieldName } from './naming.js';
|
|
4
4
|
import { resolveWrapperParams, formatWrapperDescription } from '../shared/wrapper-utils.js';
|
|
@@ -75,8 +75,15 @@ function emitWrapperMethod(
|
|
|
75
75
|
|
|
76
76
|
lines.push(' request_options: Optional[RequestOptions] = None,');
|
|
77
77
|
|
|
78
|
-
// Return type
|
|
79
|
-
const
|
|
78
|
+
// Return type — use Variant type for discriminated union responses
|
|
79
|
+
const isDiscriminatorResponse = wrapper.responseModelName
|
|
80
|
+
? !!(ctx.spec.models.find((m: Model) => m.name === wrapper.responseModelName) as any)?.discriminator
|
|
81
|
+
: false;
|
|
82
|
+
const responseType = wrapper.responseModelName
|
|
83
|
+
? isDiscriminatorResponse
|
|
84
|
+
? className(wrapper.responseModelName) + 'Variant'
|
|
85
|
+
: className(wrapper.responseModelName)
|
|
86
|
+
: 'None';
|
|
80
87
|
|
|
81
88
|
lines.push(` ) -> ${responseType}:`);
|
|
82
89
|
|
|
@@ -122,7 +129,21 @@ function emitWrapperMethod(
|
|
|
122
129
|
const awaitPrefix = isAsync ? 'await ' : '';
|
|
123
130
|
lines.push('');
|
|
124
131
|
|
|
125
|
-
if (wrapper.responseModelName) {
|
|
132
|
+
if (wrapper.responseModelName && isDiscriminatorResponse) {
|
|
133
|
+
const variantType = className(wrapper.responseModelName) + 'Variant';
|
|
134
|
+
lines.push(` return cast(`);
|
|
135
|
+
lines.push(` ${variantType},`);
|
|
136
|
+
lines.push(` ${awaitPrefix}self._client.request(`);
|
|
137
|
+
lines.push(` method="${op.httpMethod.toUpperCase()}",`);
|
|
138
|
+
lines.push(` path=${pathExpr},`);
|
|
139
|
+
lines.push(' body=body,');
|
|
140
|
+
lines.push(
|
|
141
|
+
` model=${className(wrapper.responseModelName)}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
|
|
142
|
+
);
|
|
143
|
+
lines.push(' request_options=request_options,');
|
|
144
|
+
lines.push(' )');
|
|
145
|
+
lines.push(' )');
|
|
146
|
+
} else if (wrapper.responseModelName) {
|
|
126
147
|
lines.push(` return ${awaitPrefix}self._client.request(`);
|
|
127
148
|
lines.push(` method="${op.httpMethod.toUpperCase()}",`);
|
|
128
149
|
lines.push(` path=${pathExpr},`);
|
package/src/ruby/client.ts
CHANGED
|
@@ -27,7 +27,6 @@ import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
|
27
27
|
*/
|
|
28
28
|
const NON_SPEC_ACCESSORS: Record<string, { prop: string; className: string; ctorArg: 'self' | '' }> = {
|
|
29
29
|
passwordless: { prop: 'passwordless', className: 'Passwordless', ctorArg: 'self' },
|
|
30
|
-
vault: { prop: 'vault', className: 'Vault', ctorArg: 'self' },
|
|
31
30
|
actions: { prop: 'actions', className: 'Actions', ctorArg: 'self' },
|
|
32
31
|
session_manager: { prop: 'session_manager', className: 'SessionManager', ctorArg: 'self' },
|
|
33
32
|
pkce: { prop: 'pkce', className: 'PKCE', ctorArg: '' },
|
package/src/rust/resources.ts
CHANGED
|
@@ -355,6 +355,7 @@ function renderParamsStruct(
|
|
|
355
355
|
): string {
|
|
356
356
|
const bodyRequired = isBodyRequired(op);
|
|
357
357
|
const hidden = new Set<string>([...Object.keys(resolved.defaults ?? {}), ...(resolved.inferFromClient ?? [])]);
|
|
358
|
+
const materializeSpecDefaults = !resolved.urlBuilder;
|
|
358
359
|
|
|
359
360
|
// Names of params that belong to a parameter group; these are folded into
|
|
360
361
|
// the enum field and must be omitted from the flat params struct.
|
|
@@ -412,9 +413,14 @@ function renderParamsStruct(
|
|
|
412
413
|
});
|
|
413
414
|
if (!p.required && !rust.startsWith('Option<')) rust = makeOptional(rust);
|
|
414
415
|
rust = applySecretRedaction(rust, p.name);
|
|
415
|
-
// Spec-level
|
|
416
|
-
// `Default::default()` and `new(…)`
|
|
417
|
-
|
|
416
|
+
// Spec-level defaults on HTTP params are materialized so
|
|
417
|
+
// `Default::default()` and `new(…)` produce the documented value. URL
|
|
418
|
+
// builders keep optional query params omitted unless the caller supplies
|
|
419
|
+
// them, because the query string is the public redirect target.
|
|
420
|
+
const defaultExpr =
|
|
421
|
+
materializeSpecDefaults && p.default != null
|
|
422
|
+
? rustDefaultExpr(p.default, p.type, rust.startsWith('Option<'), ctx)
|
|
423
|
+
: null;
|
|
418
424
|
// Field-level documentation derived from the spec.
|
|
419
425
|
const desc = p.description?.trim();
|
|
420
426
|
if (desc) {
|
|
@@ -1317,10 +1323,7 @@ function renderResourcesBarrel(exports: { module: string; struct: string }[]): s
|
|
|
1317
1323
|
unique.sort((a, b) => a.module.localeCompare(b.module));
|
|
1318
1324
|
|
|
1319
1325
|
const lines: string[] = [];
|
|
1320
|
-
|
|
1321
|
-
// `pub mod resources::organization_membership` would collide with the
|
|
1322
|
-
// same-named module re-exported via `pub use models::*` in lib.rs.
|
|
1323
|
-
for (const { module } of unique) lines.push(`mod ${module};`);
|
|
1326
|
+
for (const { module } of unique) lines.push(`pub mod ${module};`);
|
|
1324
1327
|
lines.push('');
|
|
1325
1328
|
for (const { module, struct } of unique) lines.push(`pub use ${module}::${struct};`);
|
|
1326
1329
|
return lines.join('\n') + '\n';
|
|
@@ -42,11 +42,6 @@ export const NON_SPEC_SERVICES: readonly NonSpecService[] = [
|
|
|
42
42
|
description: 'Passwordless (magic-link) session endpoints, not yet in the OpenAPI spec.',
|
|
43
43
|
hasClientAccessor: true,
|
|
44
44
|
},
|
|
45
|
-
{
|
|
46
|
-
id: 'vault',
|
|
47
|
-
description: 'Vault KV storage, key operations, and client-side AES-GCM encrypt/decrypt.',
|
|
48
|
-
hasClientAccessor: true,
|
|
49
|
-
},
|
|
50
45
|
{
|
|
51
46
|
id: 'webhook_verification',
|
|
52
47
|
description: 'Webhook signature verification and event deserialization (H01/H02).',
|
package/test/go/enums.test.ts
CHANGED
|
@@ -129,4 +129,28 @@ describe('go/enums', () => {
|
|
|
129
129
|
const content = files[0].content;
|
|
130
130
|
expect(content).toContain('type SSOConnectionType string');
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
it('generates an events compatibility package from webhook event enum values', () => {
|
|
134
|
+
const enums: Enum[] = [
|
|
135
|
+
{
|
|
136
|
+
name: 'CreateWebhookEndpointEvents',
|
|
137
|
+
values: [
|
|
138
|
+
{ name: 'USER_CREATED', value: 'user.created' },
|
|
139
|
+
{ name: 'API_KEY_CREATED', value: 'api_key.created' },
|
|
140
|
+
{ name: 'DSYNC_USER_CREATED', value: 'dsync.user.created' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const files = generateEnums(enums, ctx);
|
|
146
|
+
const compat = files.find((file) => file.path === 'pkg/events/events.go');
|
|
147
|
+
|
|
148
|
+
expect(compat).toBeDefined();
|
|
149
|
+
expect(compat!.content).toContain('package events');
|
|
150
|
+
expect(compat!.content).toContain('type Event string');
|
|
151
|
+
expect(compat!.content).toContain('UserCreated = "user.created"');
|
|
152
|
+
expect(compat!.content).toContain('APIKeyCreated = "api_key.created"');
|
|
153
|
+
expect(compat!.content).toContain('DsyncUserCreated = "dsync.user.created"');
|
|
154
|
+
expect(compat!.content).not.toContain('CreateWebhookEndpointEventsUserCreated');
|
|
155
|
+
});
|
|
132
156
|
});
|
|
@@ -101,7 +101,9 @@ describe('generateResources', () => {
|
|
|
101
101
|
expect(resourceFile).toBeDefined();
|
|
102
102
|
expect(resourceFile!.content).toContain('export class Organizations');
|
|
103
103
|
expect(resourceFile!.content).toContain('constructor(private readonly workos: WorkOS)');
|
|
104
|
-
expect(resourceFile!.content).toContain(
|
|
104
|
+
expect(resourceFile!.content).toContain(
|
|
105
|
+
'async getOrganization(options: GetOrganizationOptions): Promise<Organization>',
|
|
106
|
+
);
|
|
105
107
|
expect(resourceFile!.content).toContain('deserializeOrganization(data)');
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -154,6 +156,14 @@ describe('generateResources', () => {
|
|
|
154
156
|
const ctxWithResolved: EmitterContext = {
|
|
155
157
|
...ctx,
|
|
156
158
|
spec,
|
|
159
|
+
emitterOptions: {
|
|
160
|
+
operationOverrides: {
|
|
161
|
+
'GET /user_management/organization_memberships/{omId}/groups': {
|
|
162
|
+
methodName: 'list_groups_for_organization_membership',
|
|
163
|
+
mountOn: 'UserManagement',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
157
167
|
resolvedOperations: [
|
|
158
168
|
{
|
|
159
169
|
operation,
|
package/test/node/tests.test.ts
CHANGED
|
@@ -86,7 +86,7 @@ describe('node test generation ownership', () => {
|
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('
|
|
89
|
+
it('regenerates over hand-written tests and fixtures for owned services', () => {
|
|
90
90
|
const tmpRoot = createTrackedSdkRoot(true);
|
|
91
91
|
try {
|
|
92
92
|
const result = nodeEmitter.generateTests!(spec, {
|
|
@@ -95,8 +95,8 @@ describe('node test generation ownership', () => {
|
|
|
95
95
|
emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
|
|
96
96
|
} as EmitterContext);
|
|
97
97
|
|
|
98
|
-
expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(
|
|
99
|
-
expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(
|
|
98
|
+
expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(true);
|
|
99
|
+
expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(true);
|
|
100
100
|
} finally {
|
|
101
101
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
102
102
|
}
|
package/test/php/client.test.ts
CHANGED
|
@@ -86,7 +86,6 @@ describe('generateClient', () => {
|
|
|
86
86
|
const result = generateClient(emptySpec, ctx);
|
|
87
87
|
|
|
88
88
|
expect(result[0].content).toContain('public function passwordless(): Passwordless');
|
|
89
|
-
expect(result[0].content).toContain('public function vault(): Vault');
|
|
90
89
|
expect(result[0].content).toContain('public function webhookVerification(): WebhookVerification');
|
|
91
90
|
expect(result[0].content).toContain('public function actions(): Actions');
|
|
92
91
|
expect(result[0].content).toContain('public function sessionManager(): SessionManager');
|
|
@@ -217,6 +217,56 @@ describe('generateResources', () => {
|
|
|
217
217
|
expect(content).not.toMatch(/PaginationOrder::Desc/);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it('does not materialize optional query defaults for URL builders', () => {
|
|
221
|
+
const screenHintEnum = {
|
|
222
|
+
name: 'UserManagementAuthenticationScreenHint',
|
|
223
|
+
values: [
|
|
224
|
+
{ name: 'sign_in', value: 'sign-in' },
|
|
225
|
+
{ name: 'sign_up', value: 'sign-up' },
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
const op = {
|
|
229
|
+
name: 'getAuthorizationUrl',
|
|
230
|
+
httpMethod: 'get' as const,
|
|
231
|
+
path: '/user_management/authorize',
|
|
232
|
+
pathParams: [],
|
|
233
|
+
queryParams: [
|
|
234
|
+
{ name: 'redirect_uri', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
|
|
235
|
+
{
|
|
236
|
+
name: 'screen_hint',
|
|
237
|
+
type: { kind: 'enum' as const, name: 'UserManagementAuthenticationScreenHint' },
|
|
238
|
+
required: false,
|
|
239
|
+
default: 'sign-in',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
headerParams: [],
|
|
243
|
+
response: { kind: 'primitive' as const, type: 'unknown' as const },
|
|
244
|
+
errors: [],
|
|
245
|
+
injectIdempotencyKey: false,
|
|
246
|
+
};
|
|
247
|
+
const service: Service = { name: 'UserManagement', operations: [op] };
|
|
248
|
+
const spec: ApiSpec = { ...emptySpec, enums: [screenHintEnum], services: [service] };
|
|
249
|
+
const content = generateResources([service], {
|
|
250
|
+
...ctx,
|
|
251
|
+
spec,
|
|
252
|
+
resolvedOperations: [
|
|
253
|
+
{
|
|
254
|
+
operation: op,
|
|
255
|
+
service,
|
|
256
|
+
methodName: 'get_authorization_url',
|
|
257
|
+
mountOn: 'UserManagement',
|
|
258
|
+
defaults: { response_type: 'code' },
|
|
259
|
+
inferFromClient: ['client_id'],
|
|
260
|
+
urlBuilder: true,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
})[0].content;
|
|
264
|
+
|
|
265
|
+
expect(content).toContain('?\\WorkOS\\Resource\\UserManagementAuthenticationScreenHint $screenHint = null');
|
|
266
|
+
expect(content).toContain("'screen_hint' => $screenHint?->value");
|
|
267
|
+
expect(content).not.toContain('UserManagementAuthenticationScreenHint::SignIn');
|
|
268
|
+
});
|
|
269
|
+
|
|
220
270
|
it('generates paginated list method', () => {
|
|
221
271
|
const result = generateResources(services, ctx);
|
|
222
272
|
|
|
@@ -319,6 +319,12 @@ describe('rust/resources', () => {
|
|
|
319
319
|
type: { kind: 'primitive', type: 'string' },
|
|
320
320
|
required: true,
|
|
321
321
|
},
|
|
322
|
+
{
|
|
323
|
+
name: 'screen_hint',
|
|
324
|
+
type: { kind: 'enum', name: 'UserManagementAuthenticationScreenHint' },
|
|
325
|
+
required: false,
|
|
326
|
+
default: 'sign-in',
|
|
327
|
+
},
|
|
322
328
|
{
|
|
323
329
|
name: 'state',
|
|
324
330
|
type: { kind: 'primitive', type: 'string' },
|
|
@@ -347,6 +353,9 @@ describe('rust/resources', () => {
|
|
|
347
353
|
expect(f.content).toContain('pub fn get_authorization_url(');
|
|
348
354
|
expect(f.content).toContain('-> Result<String, Error>');
|
|
349
355
|
expect(f.content).toContain('let qs = crate::query::encode_query');
|
|
356
|
+
expect(f.content).toContain('pub screen_hint: Option<UserManagementAuthenticationScreenHint>,');
|
|
357
|
+
expect(f.content).toContain('screen_hint: Default::default(),');
|
|
358
|
+
expect(f.content).not.toContain('screen_hint: Some(UserManagementAuthenticationScreenHint::SignIn)');
|
|
350
359
|
expect(f.content).not.toContain('get_authorization_url_with_options');
|
|
351
360
|
expect(f.content).not.toContain('request_with_query_opts');
|
|
352
361
|
});
|