@workos/oagen-emitters 0.10.0 → 0.12.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 +28 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{plugin-H0KhxbN7.mjs → plugin-C408Wh-o.mjs} +2632 -717
- package/dist/plugin-C408Wh-o.mjs.map +1 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/docs/sdk-architecture/rust.md +323 -0
- package/package.json +2 -2
- package/src/go/models.ts +48 -3
- package/src/index.ts +1 -0
- package/src/php/models.ts +27 -3
- package/src/php/resources.ts +16 -16
- package/src/plugin.ts +2 -1
- package/src/python/enums.ts +11 -54
- package/src/python/models.ts +204 -219
- package/src/python/path-expression.ts +75 -26
- package/src/python/resources.ts +19 -44
- package/src/python/shared-schemas.ts +488 -0
- package/src/python/tests.ts +9 -7
- package/src/ruby/resources.ts +13 -1
- package/src/rust/client.ts +62 -0
- package/src/rust/enums.ts +201 -0
- package/src/rust/fixtures.ts +110 -0
- package/src/rust/index.ts +95 -0
- package/src/rust/manifest.ts +31 -0
- package/src/rust/models.ts +150 -0
- package/src/rust/naming.ts +131 -0
- package/src/rust/resources.ts +689 -0
- package/src/rust/secret.ts +59 -0
- package/src/rust/tests.ts +298 -0
- package/src/rust/type-map.ts +225 -0
- package/test/entrypoint.test.ts +1 -0
- package/test/go/models.test.ts +116 -1
- package/test/go/resources.test.ts +70 -0
- package/test/php/models.test.ts +77 -0
- package/test/php/resources.test.ts +95 -0
- package/test/plugin.test.ts +2 -1
- package/test/python/enums.test.ts +91 -0
- package/test/python/models.test.ts +225 -0
- package/test/python/resources.test.ts +47 -2
- package/test/ruby/resources.test.ts +58 -0
- package/test/rust/client.test.ts +62 -0
- package/test/rust/enums.test.ts +117 -0
- package/test/rust/manifest.test.ts +73 -0
- package/test/rust/models.test.ts +139 -0
- package/test/rust/resources.test.ts +245 -0
- package/test/rust/type-map.test.ts +83 -0
- package/dist/plugin-H0KhxbN7.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parsePathTemplate,
|
|
1
|
+
import { parsePathTemplate, type PathSegment } from '../shared/path-template.js';
|
|
2
2
|
import { fieldName } from './naming.js';
|
|
3
3
|
|
|
4
4
|
export interface PythonPathOptions {
|
|
@@ -7,46 +7,95 @@ export interface PythonPathOptions {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Build the Python
|
|
10
|
+
* Build the Python tuple expression that the SDK passes to the request layer.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* traversal vector open.
|
|
12
|
+
* The returned expression is a tuple of per-segment values. The request layer
|
|
13
|
+
* (`_BaseWorkOSClient._encode_path`) URL-encodes each element with `safe=""`
|
|
14
|
+
* before joining with "/", so a caller-supplied id containing "/" or "../"
|
|
15
|
+
* cannot escape its intended segment. This is the structural fix that lets
|
|
16
|
+
* the request layer make a real guarantee instead of inspecting an already-
|
|
17
|
+
* concatenated path string.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
19
|
+
* "/orgs" → `("orgs",)`
|
|
20
|
+
* "/orgs/{id}" → `("orgs", str(id))`
|
|
21
|
+
* "/orgs/{id}/users/{uid}" → `("orgs", str(id), "users", str(uid))`
|
|
22
|
+
* "/orgs/{id}" with id ∈ enums → `("orgs", str(enum_value(id)))`
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Mixed segments (e.g. literal text adjacent to a placeholder within a single
|
|
25
|
+
* path component) are emitted as a Python f-string element. Per-segment
|
|
26
|
+
* encoding is still applied to the whole element by the request layer; this
|
|
27
|
+
* is rare in WorkOS specs but is handled deterministically.
|
|
26
28
|
*/
|
|
27
29
|
export function buildPythonPathExpression(rawPath: string, options: PythonPathOptions = {}): string {
|
|
28
30
|
const segments = parsePathTemplate(rawPath, { stripLeadingSlash: true });
|
|
29
|
-
if (segments.length === 0) return '
|
|
30
|
-
if (!hasPathParams(segments)) {
|
|
31
|
-
const literal = (segments[0] as { value: string }).value;
|
|
32
|
-
return `"${escapePyDoubleQuoted(literal)}"`;
|
|
33
|
-
}
|
|
31
|
+
if (segments.length === 0) return '()';
|
|
34
32
|
|
|
35
|
-
const
|
|
36
|
-
|
|
33
|
+
const components = splitIntoComponents(segments);
|
|
34
|
+
const parts = components.map((c) => emitComponent(c, options.enumParams));
|
|
35
|
+
return parts.length === 1 ? `(${parts[0]!},)` : `(${parts.join(', ')})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Subpiece = { kind: 'literal'; value: string } | { kind: 'param'; name: string };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Split a parsed path template into one component per "/"-separated piece.
|
|
42
|
+
* Each component is a list of literal / param subpieces; multi-subpiece
|
|
43
|
+
* components occur only for mixed segments like `foo{id}bar`.
|
|
44
|
+
*/
|
|
45
|
+
function splitIntoComponents(segments: PathSegment[]): Subpiece[][] {
|
|
46
|
+
const components: Subpiece[][] = [[]];
|
|
37
47
|
for (const seg of segments) {
|
|
38
48
|
if (seg.kind === 'literal') {
|
|
39
|
-
|
|
49
|
+
const parts = seg.value.split('/');
|
|
50
|
+
const first = parts[0];
|
|
51
|
+
if (first !== undefined && first !== '') {
|
|
52
|
+
components[components.length - 1]!.push({ kind: 'literal', value: first });
|
|
53
|
+
}
|
|
54
|
+
for (let i = 1; i < parts.length; i++) {
|
|
55
|
+
components.push([]);
|
|
56
|
+
const part = parts[i];
|
|
57
|
+
if (part !== undefined && part !== '') {
|
|
58
|
+
components[components.length - 1]!.push({ kind: 'literal', value: part });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
40
61
|
} else {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
components[components.length - 1]!.push({ kind: 'param', name: seg.name });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Drop a trailing empty component if the path ended with a separator.
|
|
66
|
+
while (components.length > 1 && components[components.length - 1]!.length === 0) {
|
|
67
|
+
components.pop();
|
|
68
|
+
}
|
|
69
|
+
return components;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function emitComponent(component: Subpiece[], enumParams?: ReadonlySet<string>): string {
|
|
73
|
+
if (component.length === 1) {
|
|
74
|
+
const only = component[0]!;
|
|
75
|
+
if (only.kind === 'literal') return `"${escapePyDoubleQuoted(only.value)}"`;
|
|
76
|
+
const varName = fieldName(only.name);
|
|
77
|
+
const inner = enumParams?.has(only.name) ? `enum_value(${varName})` : varName;
|
|
78
|
+
return `str(${inner})`;
|
|
79
|
+
}
|
|
80
|
+
// Mixed component — fall back to an f-string. The request layer still
|
|
81
|
+
// URL-encodes the resulting element as a single segment.
|
|
82
|
+
let body = '';
|
|
83
|
+
for (const piece of component) {
|
|
84
|
+
if (piece.kind === 'literal') {
|
|
85
|
+
body += escapeFStringLiteral(piece.value);
|
|
86
|
+
} else {
|
|
87
|
+
const varName = fieldName(piece.name);
|
|
88
|
+
const inner = enumParams?.has(piece.name) ? `enum_value(${varName})` : varName;
|
|
89
|
+
body += `{${inner}}`;
|
|
44
90
|
}
|
|
45
91
|
}
|
|
46
92
|
return `f"${body}"`;
|
|
47
93
|
}
|
|
48
94
|
|
|
49
95
|
function escapePyDoubleQuoted(literal: string): string {
|
|
50
|
-
|
|
96
|
+
return literal.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function escapeFStringLiteral(literal: string): string {
|
|
51
100
|
return literal.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\{/g, '{{').replace(/\}/g, '}}');
|
|
52
101
|
}
|
package/src/python/resources.ts
CHANGED
|
@@ -12,14 +12,7 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
/** Extend Parameter with `explode` until @workos/oagen publishes the field. */
|
|
14
14
|
type ParameterExt = Parameter & { explode?: boolean };
|
|
15
|
-
import {
|
|
16
|
-
planOperation,
|
|
17
|
-
toPascalCase,
|
|
18
|
-
toSnakeCase,
|
|
19
|
-
collectModelRefs,
|
|
20
|
-
collectEnumRefs,
|
|
21
|
-
assignModelsToServices,
|
|
22
|
-
} from '@workos/oagen';
|
|
15
|
+
import { planOperation, toPascalCase, toSnakeCase, collectModelRefs, collectEnumRefs } from '@workos/oagen';
|
|
23
16
|
import { mapTypeRefUnquoted } from './type-map.js';
|
|
24
17
|
import {
|
|
25
18
|
className,
|
|
@@ -49,6 +42,7 @@ import {
|
|
|
49
42
|
clientFieldExpression,
|
|
50
43
|
} from './wrappers.js';
|
|
51
44
|
import { buildPythonPathExpression } from './path-expression.js';
|
|
45
|
+
import { computeSchemaPlacement } from './shared-schemas.js';
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
48
|
* Compute the Python parameter name for a body field, prefixing with `body_` if it
|
|
@@ -293,11 +287,20 @@ function emitMethodSignature(
|
|
|
293
287
|
lines.push(' limit: Optional[int] = None,');
|
|
294
288
|
lines.push(' before: Optional[str] = None,');
|
|
295
289
|
lines.push(' after: Optional[str] = None,');
|
|
296
|
-
// Use typed enum for order param if the spec provides one, otherwise fall
|
|
290
|
+
// Use typed enum for order param if the spec provides one, otherwise fall
|
|
291
|
+
// back to str. The default value comes from the spec's `default:` field;
|
|
292
|
+
// when the spec drops the default, we surface that as `None` rather than
|
|
293
|
+
// silently restoring "desc" client-side (which would mask a server-side
|
|
294
|
+
// behavior change from the caller).
|
|
297
295
|
const orderParam = op.queryParams.find((p) => p.name === 'order');
|
|
298
296
|
const orderType =
|
|
299
297
|
orderParam && orderParam.type.kind === 'enum' ? mapTypeRefUnquoted(orderParam.type, specEnumNames, true) : 'str';
|
|
300
|
-
|
|
298
|
+
const orderDefaultRaw = orderParam?.default;
|
|
299
|
+
const orderDefault =
|
|
300
|
+
typeof orderDefaultRaw === 'string' || typeof orderDefaultRaw === 'number' || typeof orderDefaultRaw === 'boolean'
|
|
301
|
+
? pythonLiteral(orderDefaultRaw)
|
|
302
|
+
: 'None';
|
|
303
|
+
lines.push(` order: Optional[${orderType}] = ${orderDefault},`);
|
|
301
304
|
// Additional non-pagination query params
|
|
302
305
|
for (const param of op.queryParams) {
|
|
303
306
|
if (['limit', 'before', 'after', 'order'].includes(param.name)) continue;
|
|
@@ -1020,15 +1023,6 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1020
1023
|
lines.push('');
|
|
1021
1024
|
lines.push('from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Type, Union, cast');
|
|
1022
1025
|
|
|
1023
|
-
// urllib.parse.quote is needed whenever any operation has a path parameter,
|
|
1024
|
-
// so each interpolated id can be URL-encoded with safe="" before being
|
|
1025
|
-
// concatenated into the request path.
|
|
1026
|
-
const hasAnyPathParam =
|
|
1027
|
-
allOperations.some((op) => op.pathParams.length > 0) || allOperations.some((op) => /\{[^{}]+\}/.test(op.path));
|
|
1028
|
-
if (hasAnyPathParam) {
|
|
1029
|
-
lines.push('from urllib.parse import quote');
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
1026
|
lines.push('');
|
|
1033
1027
|
lines.push('if TYPE_CHECKING:');
|
|
1034
1028
|
lines.push(` from ${importPrefix}_client import AsyncWorkOSClient, WorkOSClient`);
|
|
@@ -1127,7 +1121,8 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1127
1121
|
const actualModelImports = [...modelImports];
|
|
1128
1122
|
|
|
1129
1123
|
// Split imports into same-service and cross-service (using mount-based dirs)
|
|
1130
|
-
const
|
|
1124
|
+
const placement = computeSchemaPlacement(ctx.spec, ctx);
|
|
1125
|
+
const modelToServiceMap = new Map(placement.modelToService);
|
|
1131
1126
|
// Discriminator variant type aliases (e.g. EventSchemaVariant) live in the same
|
|
1132
1127
|
// service as their dispatcher model, so ensure they resolve to the same directory.
|
|
1133
1128
|
for (const model of ctx.spec.models) {
|
|
@@ -1167,30 +1162,10 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1167
1162
|
}
|
|
1168
1163
|
}
|
|
1169
1164
|
|
|
1170
|
-
// Enum imports — same-service vs cross-service
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
for (const svc of ctx.spec.services) {
|
|
1175
|
-
for (const op of svc.operations) {
|
|
1176
|
-
const refs = new Set<string>();
|
|
1177
|
-
// Walk all type refs (including nested nullable/array/union) to find enums
|
|
1178
|
-
const allTypeRefs = [
|
|
1179
|
-
op.response,
|
|
1180
|
-
...(op.requestBody ? [op.requestBody] : []),
|
|
1181
|
-
...op.pathParams.map((p) => p.type),
|
|
1182
|
-
...op.queryParams.map((p) => p.type),
|
|
1183
|
-
...op.headerParams.map((p) => p.type),
|
|
1184
|
-
];
|
|
1185
|
-
for (const typeRef of allTypeRefs) {
|
|
1186
|
-
for (const ref of collectEnumRefs(typeRef)) refs.add(ref);
|
|
1187
|
-
}
|
|
1188
|
-
if (refs.has(e.name) && !enumToServiceMap.has(e.name)) {
|
|
1189
|
-
enumToServiceMap.set(e.name, svc.name);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1165
|
+
// Enum imports — same-service vs cross-service. Shared enums (referenced
|
|
1166
|
+
// by 2+ services) are intentionally absent from this map so they resolve
|
|
1167
|
+
// to common/ via the resolveDir() fallback below.
|
|
1168
|
+
const enumToServiceMap = placement.enumToService;
|
|
1194
1169
|
|
|
1195
1170
|
const localEnums: string[] = [];
|
|
1196
1171
|
const crossServiceEnums = new Map<string, string[]>();
|