@workos/oagen-emitters 0.2.1 → 0.3.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 +8 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11893 -3226
- package/dist/index.mjs.map +1 -1
- 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 +298 -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-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- 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 +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -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 +3 -0
- package/src/node/client.ts +78 -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 +2 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +374 -364
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +32 -12
- 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 +171 -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 +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -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 +209 -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 +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -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/node/client.test.ts +18 -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 +99 -69
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -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 +644 -0
- package/test/php/tests.test.ts +118 -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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { EmitterContext, ResolvedOperation, ResolvedWrapper, TypeRef } from '@workos/oagen';
|
|
2
|
+
import { toCamelCase } from '@workos/oagen';
|
|
3
|
+
import { mapTypeRef, mapTypeRefForPHPDoc } from './type-map.js';
|
|
4
|
+
import { className, fieldName } from './naming.js';
|
|
5
|
+
import { phpDocComment } from './utils.js';
|
|
6
|
+
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate PHP wrapper methods for split union operations.
|
|
10
|
+
*/
|
|
11
|
+
export function generateWrapperMethods(resolvedOp: ResolvedOperation, ctx: EmitterContext): string[] {
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
for (const wrapper of resolvedOp.wrappers ?? []) {
|
|
14
|
+
emitWrapperMethod(lines, resolvedOp, wrapper, ctx);
|
|
15
|
+
}
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function emitWrapperMethod(
|
|
20
|
+
lines: string[],
|
|
21
|
+
resolvedOp: ResolvedOperation,
|
|
22
|
+
wrapper: ResolvedWrapper,
|
|
23
|
+
ctx: EmitterContext,
|
|
24
|
+
): void {
|
|
25
|
+
const method = toCamelCase(wrapper.name);
|
|
26
|
+
const ns = ctx.namespacePascal;
|
|
27
|
+
const wrapperParams = resolveWrapperParams(wrapper, ctx);
|
|
28
|
+
|
|
29
|
+
lines.push('');
|
|
30
|
+
|
|
31
|
+
// PHPDoc block
|
|
32
|
+
const docParts: string[] = [];
|
|
33
|
+
for (const { paramName, field, isOptional } of wrapperParams) {
|
|
34
|
+
const docType = field ? mapTypeRefForPHPDoc(field.type) : 'mixed';
|
|
35
|
+
const nullSuffix = isOptional && !docType.endsWith('|null') ? '|null' : '';
|
|
36
|
+
docParts.push(`@param ${docType}${nullSuffix} $${fieldName(paramName)}`);
|
|
37
|
+
}
|
|
38
|
+
const op2 = resolvedOp.operation;
|
|
39
|
+
const returnDocType = op2.response.kind === 'model' ? `\\${ns}\\Resource\\${className(op2.response.name)}` : 'mixed';
|
|
40
|
+
docParts.push(`@return ${returnDocType}`);
|
|
41
|
+
lines.push(...phpDocComment(docParts.join('\n'), 4));
|
|
42
|
+
|
|
43
|
+
lines.push(` public function ${method}(`);
|
|
44
|
+
|
|
45
|
+
// Build params: required first, then optional, to avoid PHP deprecation
|
|
46
|
+
const requiredParams: string[] = [];
|
|
47
|
+
const optionalParamLines: string[] = [];
|
|
48
|
+
for (const { paramName, field, isOptional } of wrapperParams) {
|
|
49
|
+
const phpName = fieldName(paramName);
|
|
50
|
+
if (field) {
|
|
51
|
+
const phpType = mapTypeRef(field.type);
|
|
52
|
+
if (isOptional) {
|
|
53
|
+
const nullableType = phpType.startsWith('?') ? phpType : `?${phpType}`;
|
|
54
|
+
optionalParamLines.push(` ${nullableType} $${phpName} = null,`);
|
|
55
|
+
} else {
|
|
56
|
+
requiredParams.push(` ${phpType} $${phpName},`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
optionalParamLines.push(` mixed $${phpName} = null,`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
optionalParamLines.push(` ?\\${ns}\\RequestOptions $options = null,`);
|
|
63
|
+
for (const p of [...requiredParams, ...optionalParamLines]) {
|
|
64
|
+
lines.push(p);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return type
|
|
68
|
+
const op = resolvedOp.operation;
|
|
69
|
+
const responseType = op.response.kind === 'model' ? `\\${ns}\\Resource\\${className(op.response.name)}` : 'mixed';
|
|
70
|
+
lines.push(` ): ${responseType} {`);
|
|
71
|
+
|
|
72
|
+
// Build body using array_filter for consistency
|
|
73
|
+
const bodyEntries: string[] = [];
|
|
74
|
+
|
|
75
|
+
// Defaults (always included)
|
|
76
|
+
if (wrapper.defaults) {
|
|
77
|
+
for (const [key, value] of Object.entries(wrapper.defaults)) {
|
|
78
|
+
bodyEntries.push(`'${key}' => ${phpLiteral(value)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Exposed params (extract enum values)
|
|
83
|
+
for (const { paramName, field } of wrapperParams) {
|
|
84
|
+
const phpName = fieldName(paramName);
|
|
85
|
+
if (field && isEnumType(field.type)) {
|
|
86
|
+
bodyEntries.push(`'${paramName}' => $${phpName}?->value`);
|
|
87
|
+
} else {
|
|
88
|
+
bodyEntries.push(`'${paramName}' => $${phpName}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push(' $body = array_filter([');
|
|
93
|
+
for (const entry of bodyEntries) {
|
|
94
|
+
lines.push(` ${entry},`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(' ], fn ($v) => $v !== null);');
|
|
97
|
+
|
|
98
|
+
// inferFromClient fields need special handling (conditional injection)
|
|
99
|
+
for (const clientField of wrapper.inferFromClient ?? []) {
|
|
100
|
+
const clientExpr = clientFieldExpression(clientField);
|
|
101
|
+
lines.push(` $body['${clientField}'] = ${clientExpr};`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Delegate to HTTP client
|
|
105
|
+
const httpMethod = op.httpMethod.toUpperCase();
|
|
106
|
+
let path = op.path.startsWith('/') ? op.path.slice(1) : op.path;
|
|
107
|
+
const hasInterpolation = /\{[^}]+\}/.test(path);
|
|
108
|
+
path = path.replace(/\{([^}]+)\}/g, (_match, param) => `{$${fieldName(param)}}`);
|
|
109
|
+
const pathQuote = hasInterpolation ? '"' : "'";
|
|
110
|
+
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(' $response = $this->client->request(');
|
|
113
|
+
lines.push(` method: '${httpMethod}',`);
|
|
114
|
+
lines.push(` path: ${pathQuote}${path}${pathQuote},`);
|
|
115
|
+
lines.push(' body: $body,');
|
|
116
|
+
lines.push(' options: $options,');
|
|
117
|
+
lines.push(' );');
|
|
118
|
+
|
|
119
|
+
if (op.response.kind === 'model') {
|
|
120
|
+
lines.push(` return ${className(op.response.name)}::fromArray($response);`);
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(' return $response;');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lines.push(' }');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isEnumType(ref: TypeRef): boolean {
|
|
129
|
+
if (ref.kind === 'enum') return true;
|
|
130
|
+
if (ref.kind === 'nullable') return isEnumType(ref.inner);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function phpLiteral(value: unknown): string {
|
|
135
|
+
if (typeof value === 'string') return `'${value}'`;
|
|
136
|
+
if (typeof value === 'number') return String(value);
|
|
137
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
138
|
+
return 'null';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function clientFieldExpression(field: string): string {
|
|
142
|
+
// Map inferFromClient fields to the actual client/config accessors
|
|
143
|
+
switch (field) {
|
|
144
|
+
case 'client_id':
|
|
145
|
+
return '$this->client->requireClientId()';
|
|
146
|
+
case 'client_secret':
|
|
147
|
+
return '$this->client->requireApiKey()';
|
|
148
|
+
default:
|
|
149
|
+
return `$this->client->${toCamelCase(field)}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase } from '@workos/oagen';
|
|
3
|
+
import { className, resolveServiceDir, servicePropertyName, buildMountDirMap, dirToModule } from './naming.js';
|
|
4
|
+
import { resolveResourceClassName } from './resources.js';
|
|
5
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
6
|
+
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
7
|
+
|
|
8
|
+
/** Python-specific wiring for each non-spec service. */
|
|
9
|
+
interface PythonNonSpecWiring {
|
|
10
|
+
/** Python import line (e.g. "from .vault import Vault, AsyncVault") */
|
|
11
|
+
importLine: string;
|
|
12
|
+
/** Property name on the client class */
|
|
13
|
+
prop: string;
|
|
14
|
+
/** Sync class name */
|
|
15
|
+
syncClass: string;
|
|
16
|
+
/** Async class name, or null if no async variant */
|
|
17
|
+
asyncClass: string | null;
|
|
18
|
+
/** Constructor expression — 'self' if the class takes the client, '' if stateless */
|
|
19
|
+
ctorArg: 'self' | '';
|
|
20
|
+
/** One-line docstring for the client property */
|
|
21
|
+
docstring?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PYTHON_NON_SPEC_WIRING: Record<string, PythonNonSpecWiring> = {
|
|
25
|
+
passwordless: {
|
|
26
|
+
importLine: 'from .passwordless import AsyncPasswordless, Passwordless',
|
|
27
|
+
prop: 'passwordless',
|
|
28
|
+
syncClass: 'Passwordless',
|
|
29
|
+
asyncClass: 'AsyncPasswordless',
|
|
30
|
+
ctorArg: 'self',
|
|
31
|
+
docstring: 'Passwordless authentication sessions.',
|
|
32
|
+
},
|
|
33
|
+
vault: {
|
|
34
|
+
importLine: 'from .vault import AsyncVault, Vault',
|
|
35
|
+
prop: 'vault',
|
|
36
|
+
syncClass: 'Vault',
|
|
37
|
+
asyncClass: 'AsyncVault',
|
|
38
|
+
ctorArg: 'self',
|
|
39
|
+
docstring: 'Vault encryption, key management, and secret storage.',
|
|
40
|
+
},
|
|
41
|
+
actions: {
|
|
42
|
+
importLine: 'from .actions import Actions, AsyncActions',
|
|
43
|
+
prop: 'actions',
|
|
44
|
+
syncClass: 'Actions',
|
|
45
|
+
asyncClass: 'AsyncActions',
|
|
46
|
+
ctorArg: '',
|
|
47
|
+
docstring: 'Actions logging and audit trail.',
|
|
48
|
+
},
|
|
49
|
+
pkce: {
|
|
50
|
+
importLine: 'from .pkce import PKCE',
|
|
51
|
+
prop: 'pkce',
|
|
52
|
+
syncClass: 'PKCE',
|
|
53
|
+
asyncClass: null,
|
|
54
|
+
ctorArg: '',
|
|
55
|
+
docstring: 'PKCE (Proof Key for Code Exchange) utilities.',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate the slim Python client class (service-wiring only),
|
|
61
|
+
* service __init__.py files, and types barrels.
|
|
62
|
+
*
|
|
63
|
+
* Static HTTP infrastructure lives in _base_client.py (hand-maintained
|
|
64
|
+
* in the target SDK, marked @oagen-ignore-file).
|
|
65
|
+
*/
|
|
66
|
+
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
67
|
+
assertPublicClientReachability(spec, ctx);
|
|
68
|
+
|
|
69
|
+
const files: GeneratedFile[] = [];
|
|
70
|
+
|
|
71
|
+
files.push(...generateWorkOSClient(spec, ctx));
|
|
72
|
+
files.push(...generateServiceInits(spec, ctx));
|
|
73
|
+
files.push(...generateTypesBarrels(spec, ctx));
|
|
74
|
+
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deduplicate services by mount target. Multiple IR services may mount to the
|
|
80
|
+
* same target (e.g., Applications + ApplicationClientSecrets -> Connect).
|
|
81
|
+
* Returns one representative service per unique mount target, using the service
|
|
82
|
+
* whose PascalCase name matches the target (if any), or the first one found.
|
|
83
|
+
*/
|
|
84
|
+
function deduplicateByMount(services: Service[], ctx: EmitterContext): Service[] {
|
|
85
|
+
const byTarget = new Map<string, Service>();
|
|
86
|
+
for (const s of services) {
|
|
87
|
+
const target = getMountTarget(s, ctx);
|
|
88
|
+
const existing = byTarget.get(target);
|
|
89
|
+
if (!existing || toPascalCase(s.name) === target) {
|
|
90
|
+
byTarget.set(target, s);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...byTarget.values()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext): Map<string, string> {
|
|
97
|
+
const topLevel = deduplicateByMount(services, ctx);
|
|
98
|
+
const paths = new Map<string, string>();
|
|
99
|
+
|
|
100
|
+
for (const service of topLevel) {
|
|
101
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
102
|
+
const prop = servicePropertyName(resolvedName);
|
|
103
|
+
paths.set(service.name, prop);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build reverse map: mount target name -> access path
|
|
107
|
+
const targetPaths = new Map<string, string>();
|
|
108
|
+
for (const service of topLevel) {
|
|
109
|
+
const target = getMountTarget(service, ctx);
|
|
110
|
+
if (!targetPaths.has(target) && paths.has(service.name)) {
|
|
111
|
+
targetPaths.set(target, paths.get(service.name)!);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Map mounted services to their mount target's access path
|
|
116
|
+
for (const service of services) {
|
|
117
|
+
if (paths.has(service.name)) continue;
|
|
118
|
+
const mountTarget = getMountTarget(service, ctx);
|
|
119
|
+
const targetPath = targetPaths.get(mountTarget) ?? paths.get(mountTarget);
|
|
120
|
+
if (targetPath) paths.set(service.name, targetPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return paths;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertPublicClientReachability(spec: ApiSpec, ctx: EmitterContext): void {
|
|
127
|
+
const topLevelServices = deduplicateByMount(spec.services, ctx);
|
|
128
|
+
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
129
|
+
const unreachableServices = topLevelServices
|
|
130
|
+
.filter((service) => service.operations.length > 0 && !accessPaths.has(service.name))
|
|
131
|
+
.map((service) => service.name);
|
|
132
|
+
|
|
133
|
+
if (unreachableServices.length > 0) {
|
|
134
|
+
throw new Error(`Python emitter reachability audit failed for services: ${unreachableServices.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate the slim _client.py that subclasses the static _base_client.py
|
|
140
|
+
* and wires spec-driven service accessors.
|
|
141
|
+
*/
|
|
142
|
+
function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
const topLevelServices = deduplicateByMount(spec.services, ctx);
|
|
145
|
+
|
|
146
|
+
// --- Imports ---
|
|
147
|
+
lines.push('from __future__ import annotations');
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('import functools');
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('from ._base_client import (');
|
|
152
|
+
lines.push(' WorkOSClient as _SyncBase,');
|
|
153
|
+
lines.push(' AsyncWorkOSClient as _AsyncBase,');
|
|
154
|
+
lines.push(')');
|
|
155
|
+
|
|
156
|
+
// Import resource classes (both sync and async)
|
|
157
|
+
const serviceDirMap = buildMountDirMap(ctx);
|
|
158
|
+
for (const service of topLevelServices) {
|
|
159
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
160
|
+
const clsName = className(resolvedName);
|
|
161
|
+
const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
|
|
162
|
+
lines.push(`from .${dirToModule(dirName)}._resource import ${clsName}, Async${clsName}`);
|
|
163
|
+
}
|
|
164
|
+
// Non-spec service imports (driven by shared/non-spec-services.ts)
|
|
165
|
+
for (const s of NON_SPEC_SERVICES) {
|
|
166
|
+
const w = PYTHON_NON_SPEC_WIRING[s.id];
|
|
167
|
+
if (w) lines.push(w.importLine);
|
|
168
|
+
}
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push('');
|
|
171
|
+
|
|
172
|
+
// --- Sync client ---
|
|
173
|
+
lines.push('class WorkOSClient(_SyncBase):');
|
|
174
|
+
lines.push(' """Synchronous WorkOS API client with service accessors."""');
|
|
175
|
+
|
|
176
|
+
// Collect all generated property names
|
|
177
|
+
const generatedProps = new Set<string>();
|
|
178
|
+
for (const service of topLevelServices) {
|
|
179
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
180
|
+
const clsName = className(resolvedName);
|
|
181
|
+
const prop = servicePropertyName(resolvedName);
|
|
182
|
+
const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(' @functools.cached_property');
|
|
185
|
+
lines.push(` def ${prop}(self) -> ${clsName}:`);
|
|
186
|
+
lines.push(` """${readable} API resources."""`);
|
|
187
|
+
lines.push(` return ${clsName}(self)`);
|
|
188
|
+
generatedProps.add(prop);
|
|
189
|
+
}
|
|
190
|
+
emitCompatClientPropertyAliases(lines, generatedProps, false);
|
|
191
|
+
emitCompatClientAccessors(lines, false);
|
|
192
|
+
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push('');
|
|
195
|
+
|
|
196
|
+
// --- Async client ---
|
|
197
|
+
lines.push('class AsyncWorkOSClient(_AsyncBase):');
|
|
198
|
+
lines.push(' """Asynchronous WorkOS API client with service accessors."""');
|
|
199
|
+
|
|
200
|
+
const asyncGeneratedProps = new Set<string>();
|
|
201
|
+
for (const service of topLevelServices) {
|
|
202
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
203
|
+
const clsName = className(resolvedName);
|
|
204
|
+
const prop = servicePropertyName(resolvedName);
|
|
205
|
+
const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push(' @functools.cached_property');
|
|
208
|
+
lines.push(` def ${prop}(self) -> Async${clsName}:`);
|
|
209
|
+
lines.push(` """${readable} API resources."""`);
|
|
210
|
+
lines.push(` return Async${clsName}(self)`);
|
|
211
|
+
asyncGeneratedProps.add(prop);
|
|
212
|
+
}
|
|
213
|
+
emitCompatClientPropertyAliases(lines, asyncGeneratedProps, true);
|
|
214
|
+
emitCompatClientAccessors(lines, true);
|
|
215
|
+
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
path: `src/${ctx.namespace}/_client.py`,
|
|
219
|
+
content: lines.join('\n'),
|
|
220
|
+
integrateTarget: true,
|
|
221
|
+
overwriteExisting: true,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
227
|
+
const files: GeneratedFile[] = [];
|
|
228
|
+
const topLevel = deduplicateByMount(spec.services, ctx);
|
|
229
|
+
const serviceDirMap = buildMountDirMap(ctx);
|
|
230
|
+
|
|
231
|
+
for (const service of topLevel) {
|
|
232
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
233
|
+
const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
|
|
236
|
+
lines.push(`from ._resource import ${resolvedName}, Async${resolvedName}`);
|
|
237
|
+
lines.push('from .models import *');
|
|
238
|
+
|
|
239
|
+
files.push({
|
|
240
|
+
path: `src/${ctx.namespace}/${dirName}/__init__.py`,
|
|
241
|
+
content: lines.join('\n'),
|
|
242
|
+
integrateTarget: true,
|
|
243
|
+
overwriteExisting: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Ensure models/__init__.py exists even if no models are assigned to this service
|
|
247
|
+
files.push({
|
|
248
|
+
path: `src/${ctx.namespace}/${dirName}/models/__init__.py`,
|
|
249
|
+
content: '',
|
|
250
|
+
skipIfExists: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return files;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function emitCompatClientPropertyAliases(lines: string[], generatedProps: Set<string>, isAsync: boolean): void {
|
|
258
|
+
const aliases: Array<{ alias: string; typeName: string; returnExpr: string; docstring: string }> = [];
|
|
259
|
+
if (generatedProps.has('multi_factor_auth') && !generatedProps.has('mfa')) {
|
|
260
|
+
const mfaType = isAsync ? 'AsyncMultiFactorAuth' : 'MultiFactorAuth';
|
|
261
|
+
aliases.push({
|
|
262
|
+
alias: 'mfa',
|
|
263
|
+
typeName: mfaType,
|
|
264
|
+
returnExpr: 'self.multi_factor_auth',
|
|
265
|
+
docstring: '"""Alias for multi_factor_auth."""',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const alias of aliases) {
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push(' @functools.cached_property');
|
|
271
|
+
lines.push(` def ${alias.alias}(self) -> ${alias.typeName}:`);
|
|
272
|
+
lines.push(` ${alias.docstring}`);
|
|
273
|
+
lines.push(` return ${alias.returnExpr}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function emitCompatClientAccessors(lines: string[], isAsync: boolean): void {
|
|
278
|
+
for (const s of NON_SPEC_SERVICES) {
|
|
279
|
+
const w = PYTHON_NON_SPEC_WIRING[s.id];
|
|
280
|
+
if (!w) continue;
|
|
281
|
+
// Skip async-only services when emitting the sync client, and vice versa
|
|
282
|
+
const typeName = isAsync ? (w.asyncClass ?? w.syncClass) : w.syncClass;
|
|
283
|
+
const arg = w.ctorArg === 'self' ? 'self' : '';
|
|
284
|
+
|
|
285
|
+
lines.push('');
|
|
286
|
+
lines.push(' @functools.cached_property');
|
|
287
|
+
lines.push(` def ${w.prop}(self) -> ${typeName}:`);
|
|
288
|
+
if (w.docstring) {
|
|
289
|
+
lines.push(` """${w.docstring}"""`);
|
|
290
|
+
}
|
|
291
|
+
lines.push(` return ${typeName}(${arg})`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generate types/<service>/__init__.py re-export barrels so that
|
|
297
|
+
* `from workos.types.<service> import Model` continues to work.
|
|
298
|
+
*/
|
|
299
|
+
function generateTypesBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
300
|
+
const files: GeneratedFile[] = [];
|
|
301
|
+
const serviceDirMap = buildMountDirMap(ctx);
|
|
302
|
+
|
|
303
|
+
// Collect (types dir name -> set of service dirs whose models should be re-exported)
|
|
304
|
+
const typesEntries = new Map<string, Set<string>>();
|
|
305
|
+
|
|
306
|
+
for (const service of spec.services) {
|
|
307
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
308
|
+
const prop = servicePropertyName(resolvedName);
|
|
309
|
+
const dir = serviceDirMap.get(service.name) ?? prop;
|
|
310
|
+
const dirs = typesEntries.get(prop) ?? new Set();
|
|
311
|
+
dirs.add(dir);
|
|
312
|
+
typesEntries.set(prop, dirs);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const [typesDir, serviceDirs] of typesEntries) {
|
|
316
|
+
const imports = [...serviceDirs]
|
|
317
|
+
.sort()
|
|
318
|
+
.map((dir) => `from ${ctx.namespace}.${dirToModule(dir)}.models import * # noqa: F401,F403`);
|
|
319
|
+
|
|
320
|
+
files.push({
|
|
321
|
+
path: `src/${ctx.namespace}/types/${typesDir}/__init__.py`,
|
|
322
|
+
content: imports.join('\n'),
|
|
323
|
+
integrateTarget: true,
|
|
324
|
+
overwriteExisting: true,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Root types/__init__.py
|
|
329
|
+
files.push({
|
|
330
|
+
path: `src/${ctx.namespace}/types/__init__.py`,
|
|
331
|
+
content: '',
|
|
332
|
+
integrateTarget: true,
|
|
333
|
+
overwriteExisting: true,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return files;
|
|
337
|
+
}
|