@workos/oagen-emitters 0.2.0 → 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/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -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 +11943 -2728
- 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-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +137 -46
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- 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 +167 -122
- package/src/node/enums.ts +13 -4
- package/src/node/errors.ts +42 -233
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +15 -5
- package/src/node/index.ts +65 -16
- package/src/node/models.ts +264 -96
- package/src/node/naming.ts +52 -25
- package/src/node/resources.ts +621 -172
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +71 -27
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +56 -64
- 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 +199 -94
- package/test/node/enums.test.ts +75 -3
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +109 -20
- package/test/node/naming.test.ts +37 -4
- package/test/node/resources.test.ts +662 -30
- package/test/node/serializers.test.ts +36 -7
- 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 -744
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ApiSpec, Service, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase, toCamelCase } from '@workos/oagen';
|
|
3
|
+
import { className, servicePropertyName } from './naming.js';
|
|
4
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
5
|
+
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PHP-specific class-name overrides for non-spec services.
|
|
9
|
+
* If a service id isn't listed here, PascalCase(id) is used.
|
|
10
|
+
*/
|
|
11
|
+
const PHP_NON_SPEC_CLASS_NAMES: Record<string, string> = {
|
|
12
|
+
webhook_verification: 'WebhookVerification',
|
|
13
|
+
session_manager: 'SessionManager',
|
|
14
|
+
pkce: 'PKCEHelper',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Derive PHP class name + property name from a non-spec service id. */
|
|
18
|
+
function phpNonSpecAccessor(id: string): { className: string; propName: string } {
|
|
19
|
+
return {
|
|
20
|
+
className: PHP_NON_SPEC_CLASS_NAMES[id] ?? toPascalCase(id),
|
|
21
|
+
propName:
|
|
22
|
+
id === 'webhook_verification'
|
|
23
|
+
? 'webhookVerification'
|
|
24
|
+
: id === 'session_manager'
|
|
25
|
+
? 'sessionManager'
|
|
26
|
+
: toCamelCase(id),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate the main PHP client class (service wiring only).
|
|
32
|
+
*
|
|
33
|
+
* Static infrastructure (HttpClient, PaginatedResponse, RequestOptions) is
|
|
34
|
+
* now hand-maintained in the target SDK with @oagen-ignore-file.
|
|
35
|
+
*/
|
|
36
|
+
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
37
|
+
const ns = ctx.namespacePascal;
|
|
38
|
+
const dedupedServices = deduplicateByMount(spec.services, ctx);
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
path: `lib/${ns}.php`,
|
|
43
|
+
content: generateMainClient(spec, dedupedServices, ctx),
|
|
44
|
+
overwriteExisting: true,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a map from IR service name to the client accessor property name.
|
|
51
|
+
*/
|
|
52
|
+
export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext): Map<string, string> {
|
|
53
|
+
const map = new Map<string, string>();
|
|
54
|
+
for (const service of services) {
|
|
55
|
+
const target = getMountTarget(service, ctx);
|
|
56
|
+
map.set(service.name, servicePropertyName(target));
|
|
57
|
+
map.set(target, servicePropertyName(target));
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deduplicateByMount(services: Service[], ctx: EmitterContext): { name: string; propName: string }[] {
|
|
63
|
+
const seen = new Map<string, { name: string; propName: string }>();
|
|
64
|
+
for (const service of services) {
|
|
65
|
+
const target = getMountTarget(service, ctx);
|
|
66
|
+
if (!seen.has(target)) {
|
|
67
|
+
seen.set(target, {
|
|
68
|
+
name: className(target),
|
|
69
|
+
propName: servicePropertyName(target),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [...seen.values()];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function generateMainClient(
|
|
77
|
+
spec: ApiSpec,
|
|
78
|
+
services: { name: string; propName: string }[],
|
|
79
|
+
ctx: EmitterContext,
|
|
80
|
+
): string {
|
|
81
|
+
const ns = ctx.namespacePascal;
|
|
82
|
+
const lines: string[] = [];
|
|
83
|
+
|
|
84
|
+
// No <?php here — the file header from fileHeader() provides it
|
|
85
|
+
lines.push(`namespace ${ns};`);
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
// Use imports (sorted case-insensitively for PSR-12)
|
|
89
|
+
const nonSpecAccessors = NON_SPEC_SERVICES.map((s) => phpNonSpecAccessor(s.id));
|
|
90
|
+
const allImports: string[] = [];
|
|
91
|
+
for (const svc of services) {
|
|
92
|
+
allImports.push(`use ${ns}\\Service\\${svc.name};`);
|
|
93
|
+
}
|
|
94
|
+
allImports.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
95
|
+
for (const imp of allImports) {
|
|
96
|
+
lines.push(imp);
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
|
|
100
|
+
lines.push(`class ${ns}`);
|
|
101
|
+
lines.push('{');
|
|
102
|
+
lines.push(' private static ?string $apiKey = null;');
|
|
103
|
+
lines.push(' private static ?string $clientId = null;');
|
|
104
|
+
lines.push(' private ?HttpClient $httpClient = null;');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(' public static function getApiKey(): ?string');
|
|
107
|
+
lines.push(' {');
|
|
108
|
+
lines.push(' return self::$apiKey;');
|
|
109
|
+
lines.push(' }');
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push(' public static function setApiKey(?string $key): void');
|
|
112
|
+
lines.push(' {');
|
|
113
|
+
lines.push(' self::$apiKey = $key;');
|
|
114
|
+
lines.push(' }');
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push(' public static function getClientId(): ?string');
|
|
117
|
+
lines.push(' {');
|
|
118
|
+
lines.push(' return self::$clientId;');
|
|
119
|
+
lines.push(' }');
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push(' public static function setClientId(?string $id): void');
|
|
122
|
+
lines.push(' {');
|
|
123
|
+
lines.push(' self::$clientId = $id;');
|
|
124
|
+
lines.push(' }');
|
|
125
|
+
|
|
126
|
+
// Nullable resource properties
|
|
127
|
+
for (const svc of services) {
|
|
128
|
+
lines.push(` private ?Service\\${svc.name} $${svc.propName} = null;`);
|
|
129
|
+
}
|
|
130
|
+
// Non-spec service properties (hand-maintained modules)
|
|
131
|
+
for (const a of nonSpecAccessors) {
|
|
132
|
+
lines.push(` private ?${a.className} $${a.propName} = null;`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(' public function __construct(');
|
|
137
|
+
lines.push(' ?string $apiKey = null,');
|
|
138
|
+
lines.push(' ?string $clientId = null,');
|
|
139
|
+
lines.push(` string $baseUrl = '${spec.baseUrl}',`);
|
|
140
|
+
lines.push(' int $timeout = 60,');
|
|
141
|
+
lines.push(' int $maxRetries = 3,');
|
|
142
|
+
lines.push(' ?\\GuzzleHttp\\HandlerStack $handler = null,');
|
|
143
|
+
lines.push(' ) {');
|
|
144
|
+
lines.push(" $apiKey ??= getenv('WORKOS_API_KEY') ?: self::$apiKey ?? '';");
|
|
145
|
+
lines.push(" $clientId ??= getenv('WORKOS_CLIENT_ID') ?: self::$clientId;");
|
|
146
|
+
lines.push(
|
|
147
|
+
' $this->httpClient = new HttpClient($apiKey, $clientId, $baseUrl, $timeout, $maxRetries, $handler);',
|
|
148
|
+
);
|
|
149
|
+
lines.push(' }');
|
|
150
|
+
|
|
151
|
+
// Resource accessors
|
|
152
|
+
for (const svc of services) {
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(` public function ${svc.propName}(): ${svc.name}`);
|
|
155
|
+
lines.push(' {');
|
|
156
|
+
lines.push(` return $this->${svc.propName} ??= new Service\\${svc.name}($this->httpClient);`);
|
|
157
|
+
lines.push(' }');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Non-spec service accessors (hand-maintained modules)
|
|
161
|
+
for (const a of nonSpecAccessors) {
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push(` public function ${a.propName}(): ${a.className}`);
|
|
164
|
+
lines.push(' {');
|
|
165
|
+
lines.push(` return $this->${a.propName} ??= new ${a.className}($this->httpClient);`);
|
|
166
|
+
lines.push(' }');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push('}');
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
package/src/php/enums.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase } from '@workos/oagen';
|
|
3
|
+
import { className, resolveEnumName } from './naming.js';
|
|
4
|
+
import { phpDocComment } from './utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate PHP enum files from IR enums.
|
|
8
|
+
*/
|
|
9
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
10
|
+
if (enums.length === 0) return [];
|
|
11
|
+
|
|
12
|
+
const files: GeneratedFile[] = [];
|
|
13
|
+
const emittedCanonical = new Set<string>();
|
|
14
|
+
|
|
15
|
+
for (const e of enums) {
|
|
16
|
+
const canonical = resolveEnumName(e.name);
|
|
17
|
+
if (emittedCanonical.has(canonical)) continue; // skip aliases
|
|
18
|
+
emittedCanonical.add(canonical);
|
|
19
|
+
|
|
20
|
+
const name = className(canonical);
|
|
21
|
+
const _isAllStrings = e.values.every((v) => typeof v.value === 'string');
|
|
22
|
+
const isAllInts = e.values.every((v) => typeof v.value === 'number' && Number.isInteger(v.value));
|
|
23
|
+
const backingType = isAllInts ? 'int' : 'string';
|
|
24
|
+
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
// No <?php here — the file header from fileHeader() provides it
|
|
27
|
+
lines.push(`namespace ${ctx.namespacePascal}\\Resource;`);
|
|
28
|
+
lines.push('');
|
|
29
|
+
lines.push(`enum ${name}: ${backingType}`);
|
|
30
|
+
lines.push('{');
|
|
31
|
+
|
|
32
|
+
// Deduplicate case names
|
|
33
|
+
const usedNames = new Map<string, number>();
|
|
34
|
+
for (const val of e.values) {
|
|
35
|
+
let caseName = toPascalCase(val.name.toLowerCase());
|
|
36
|
+
const baseName = caseName;
|
|
37
|
+
const count = usedNames.get(baseName) ?? 0;
|
|
38
|
+
if (count > 0) {
|
|
39
|
+
caseName = `${baseName}${count + 1}`;
|
|
40
|
+
}
|
|
41
|
+
usedNames.set(baseName, count + 1);
|
|
42
|
+
|
|
43
|
+
if (val.description || val.deprecated) {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
if (val.description) parts.push(val.description);
|
|
46
|
+
if (val.deprecated) parts.push('@deprecated');
|
|
47
|
+
lines.push(...phpDocComment(parts.join('\n'), 4));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof val.value === 'string') {
|
|
51
|
+
lines.push(` case ${caseName} = '${val.value}';`);
|
|
52
|
+
} else {
|
|
53
|
+
lines.push(` case ${caseName} = ${val.value};`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.push('}');
|
|
58
|
+
|
|
59
|
+
files.push({
|
|
60
|
+
path: `lib/Resource/${name}.php`,
|
|
61
|
+
content: lines.join('\n'),
|
|
62
|
+
overwriteExisting: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
2
|
+
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
3
|
+
import { snakeName } from './naming.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prefix mapping for generating realistic ID fixture values.
|
|
7
|
+
*/
|
|
8
|
+
export const ID_PREFIXES: Record<string, string> = {
|
|
9
|
+
Connection: 'conn_',
|
|
10
|
+
Organization: 'org_',
|
|
11
|
+
OrganizationMembership: 'om_',
|
|
12
|
+
User: 'user_',
|
|
13
|
+
Directory: 'directory_',
|
|
14
|
+
DirectoryGroup: 'dir_grp_',
|
|
15
|
+
DirectoryUser: 'dir_usr_',
|
|
16
|
+
Invitation: 'inv_',
|
|
17
|
+
Session: 'session_',
|
|
18
|
+
AuthenticationFactor: 'auth_factor_',
|
|
19
|
+
EmailVerification: 'email_verification_',
|
|
20
|
+
MagicAuth: 'magic_auth_',
|
|
21
|
+
PasswordReset: 'password_reset_',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate JSON fixture files for test data.
|
|
26
|
+
*/
|
|
27
|
+
export function generateFixtures(spec: {
|
|
28
|
+
models: Model[];
|
|
29
|
+
enums: Enum[];
|
|
30
|
+
services: any[];
|
|
31
|
+
}): { path: string; content: string }[] {
|
|
32
|
+
if (spec.models.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
35
|
+
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
36
|
+
const files: { path: string; content: string }[] = [];
|
|
37
|
+
|
|
38
|
+
for (const model of spec.models) {
|
|
39
|
+
if (isListMetadataModel(model)) continue;
|
|
40
|
+
if (isListWrapperModel(model)) continue;
|
|
41
|
+
|
|
42
|
+
const fixture = generateModelFixture(model, modelMap, enumMap);
|
|
43
|
+
|
|
44
|
+
files.push({
|
|
45
|
+
path: `tests/Fixtures/${snakeName(model.name)}.json`,
|
|
46
|
+
content: JSON.stringify(fixture, null, 2),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate list fixtures for paginated responses
|
|
51
|
+
for (const service of spec.services) {
|
|
52
|
+
for (const op of service.operations) {
|
|
53
|
+
if (op.pagination) {
|
|
54
|
+
let itemModel = op.pagination.itemType.kind === 'model' ? modelMap.get(op.pagination.itemType.name) : null;
|
|
55
|
+
if (itemModel) {
|
|
56
|
+
const unwrapped = unwrapListModel(itemModel, modelMap);
|
|
57
|
+
if (unwrapped) itemModel = unwrapped;
|
|
58
|
+
if (itemModel.fields.length === 0) continue;
|
|
59
|
+
const fixture = generateModelFixture(itemModel, modelMap, enumMap);
|
|
60
|
+
const listFixture = {
|
|
61
|
+
data: [fixture],
|
|
62
|
+
list_metadata: {
|
|
63
|
+
before: null,
|
|
64
|
+
after: null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
files.push({
|
|
68
|
+
path: `tests/Fixtures/list_${snakeName(itemModel.name)}.json`,
|
|
69
|
+
content: JSON.stringify(listFixture, null, 2),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function unwrapListModel(model: Model, modelMap: Map<string, Model>): Model | null {
|
|
80
|
+
const dataField = model.fields.find((f) => f.name === 'data');
|
|
81
|
+
const hasListMetadata = model.fields.some((f) => f.name === 'list_metadata' || f.name === 'listMetadata');
|
|
82
|
+
if (dataField && hasListMetadata && dataField.type.kind === 'array') {
|
|
83
|
+
const itemType = dataField.type.items;
|
|
84
|
+
if (itemType.kind === 'model') {
|
|
85
|
+
return modelMap.get(itemType.name) ?? null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function generateModelFixture(
|
|
92
|
+
model: Model,
|
|
93
|
+
modelMap: Map<string, Model>,
|
|
94
|
+
enumMap: Map<string, Enum>,
|
|
95
|
+
): Record<string, any> {
|
|
96
|
+
const fixture: Record<string, any> = {};
|
|
97
|
+
|
|
98
|
+
for (const field of model.fields) {
|
|
99
|
+
const wireName = field.name;
|
|
100
|
+
if (field.example !== undefined) {
|
|
101
|
+
fixture[wireName] = field.example;
|
|
102
|
+
} else {
|
|
103
|
+
fixture[wireName] = generateFieldValue(field.type, field.name, model.name, modelMap, enumMap);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fixture;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function generateFieldValue(
|
|
111
|
+
ref: TypeRef,
|
|
112
|
+
fieldName: string,
|
|
113
|
+
modelName: string,
|
|
114
|
+
modelMap: Map<string, Model>,
|
|
115
|
+
enumMap: Map<string, Enum>,
|
|
116
|
+
): any {
|
|
117
|
+
switch (ref.kind) {
|
|
118
|
+
case 'primitive':
|
|
119
|
+
return generatePrimitiveValue(ref.type, ref.format, fieldName, modelName);
|
|
120
|
+
case 'literal':
|
|
121
|
+
return ref.value;
|
|
122
|
+
case 'enum': {
|
|
123
|
+
const e = enumMap.get(ref.name);
|
|
124
|
+
return e?.values[0]?.value ?? 'unknown';
|
|
125
|
+
}
|
|
126
|
+
case 'model': {
|
|
127
|
+
const nested = modelMap.get(ref.name);
|
|
128
|
+
if (nested) return generateModelFixture(nested, modelMap, enumMap);
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
case 'array': {
|
|
132
|
+
if (ref.items.kind === 'enum') {
|
|
133
|
+
const e = enumMap.get(ref.items.name);
|
|
134
|
+
if (e && e.values.length > 0) {
|
|
135
|
+
return e.values.map((v) => v.value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const item = generateFieldValue(ref.items, fieldName, modelName, modelMap, enumMap);
|
|
139
|
+
return [item];
|
|
140
|
+
}
|
|
141
|
+
case 'nullable':
|
|
142
|
+
return generateFieldValue(ref.inner, fieldName, modelName, modelMap, enumMap);
|
|
143
|
+
case 'union':
|
|
144
|
+
if (ref.variants.length > 0) {
|
|
145
|
+
return generateFieldValue(ref.variants[0], fieldName, modelName, modelMap, enumMap);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
case 'map':
|
|
149
|
+
return {
|
|
150
|
+
key: generateFieldValue(ref.valueType, 'value', modelName, modelMap, enumMap),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function generatePrimitiveValue(type: string, format: string | undefined, name: string, modelName: string): any {
|
|
156
|
+
switch (type) {
|
|
157
|
+
case 'string':
|
|
158
|
+
if (format === 'date-time') return '2023-01-01T00:00:00.000Z';
|
|
159
|
+
if (format === 'date') return '2023-01-01';
|
|
160
|
+
if (format === 'uuid') return '00000000-0000-0000-0000-000000000000';
|
|
161
|
+
if (name === 'id') {
|
|
162
|
+
const prefix = ID_PREFIXES[modelName] ?? '';
|
|
163
|
+
return `${prefix}01234`;
|
|
164
|
+
}
|
|
165
|
+
if (name.includes('id')) return `${name}_01234`;
|
|
166
|
+
if (name.includes('email')) return 'test@example.com';
|
|
167
|
+
if (name.includes('url') || name.includes('uri')) return 'https://example.com';
|
|
168
|
+
if (name.includes('name')) return 'Test';
|
|
169
|
+
return `test_${name}`;
|
|
170
|
+
case 'integer':
|
|
171
|
+
return 1;
|
|
172
|
+
case 'number':
|
|
173
|
+
return 1.0;
|
|
174
|
+
case 'boolean':
|
|
175
|
+
return true;
|
|
176
|
+
case 'unknown':
|
|
177
|
+
return {};
|
|
178
|
+
default:
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
package/src/php/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Emitter,
|
|
3
|
+
EmitterContext,
|
|
4
|
+
FormatCommand,
|
|
5
|
+
GeneratedFile,
|
|
6
|
+
ApiSpec,
|
|
7
|
+
Model,
|
|
8
|
+
Enum,
|
|
9
|
+
Service,
|
|
10
|
+
} from '@workos/oagen';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { generateModels } from './models.js';
|
|
15
|
+
import { generateEnums } from './enums.js';
|
|
16
|
+
import { generateResources } from './resources.js';
|
|
17
|
+
import { generateClient } from './client.js';
|
|
18
|
+
import { generateTests } from './tests.js';
|
|
19
|
+
import { generateManifest } from './manifest.js';
|
|
20
|
+
import { initializeEnumDedup } from './naming.js';
|
|
21
|
+
|
|
22
|
+
/** Initialize enum deduplication from spec data. */
|
|
23
|
+
function ensureNamingInitialized(ctx: EmitterContext): void {
|
|
24
|
+
initializeEnumDedup(ctx.spec.enums);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Ensure every generated file's content ends with a trailing newline. */
|
|
28
|
+
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
29
|
+
for (const f of files) {
|
|
30
|
+
if (f.content && !f.content.endsWith('\n')) {
|
|
31
|
+
f.content += '\n';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const phpEmitter: Emitter = {
|
|
38
|
+
language: 'php',
|
|
39
|
+
|
|
40
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
|
+
ensureNamingInitialized(ctx);
|
|
42
|
+
return ensureTrailingNewlines(generateModels(models, ctx));
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
46
|
+
ensureNamingInitialized(ctx);
|
|
47
|
+
return ensureTrailingNewlines(generateEnums(enums, ctx));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
51
|
+
ensureNamingInitialized(ctx);
|
|
52
|
+
return ensureTrailingNewlines(generateResources(services, ctx));
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
56
|
+
ensureNamingInitialized(ctx);
|
|
57
|
+
return ensureTrailingNewlines(generateClient(spec, ctx));
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
generateErrors(): GeneratedFile[] {
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
generateTypeSignatures(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
|
|
65
|
+
// PHP uses inline type hints — no separate type signature files needed
|
|
66
|
+
return [];
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
70
|
+
ensureNamingInitialized(ctx);
|
|
71
|
+
return ensureTrailingNewlines(generateTests(spec, ctx));
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
75
|
+
ensureNamingInitialized(ctx);
|
|
76
|
+
return ensureTrailingNewlines(generateManifest(spec, ctx));
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
fileHeader(): string {
|
|
80
|
+
return '<?php\n\ndeclare(strict_types=1);\n\n// This file is auto-generated by oagen. Do not edit.';
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
formatCommand(targetDir: string): FormatCommand | null {
|
|
84
|
+
const hasPhpCsFixer =
|
|
85
|
+
fs.existsSync(path.join(targetDir, '.php-cs-fixer.dist.php')) ||
|
|
86
|
+
fs.existsSync(path.join(targetDir, '.php-cs-fixer.php'));
|
|
87
|
+
if (hasPhpCsFixer) {
|
|
88
|
+
return {
|
|
89
|
+
cmd: 'bash',
|
|
90
|
+
args: ['-c', 'php vendor/bin/php-cs-fixer fix --using-cache=no --quiet . || true'],
|
|
91
|
+
batchSize: 999999,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { resolveMethodName } from './naming.js';
|
|
3
|
+
import { buildServiceAccessPaths } from './client.js';
|
|
4
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate smoke test manifest mapping HTTP operations to SDK methods.
|
|
8
|
+
*/
|
|
9
|
+
export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
10
|
+
const manifest: Record<string, { sdkMethod: string; service: string }> = {};
|
|
11
|
+
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
12
|
+
|
|
13
|
+
for (const service of spec.services) {
|
|
14
|
+
let propName = accessPaths.get(service.name);
|
|
15
|
+
if (!propName) {
|
|
16
|
+
const mountTarget = getMountTarget(service, ctx);
|
|
17
|
+
propName = accessPaths.get(mountTarget);
|
|
18
|
+
}
|
|
19
|
+
if (!propName) {
|
|
20
|
+
throw new Error(`Missing public client access path for service ${service.name}`);
|
|
21
|
+
}
|
|
22
|
+
for (const op of service.operations) {
|
|
23
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
24
|
+
const method = resolveMethodName(op, service, ctx);
|
|
25
|
+
manifest[httpKey] = { sdkMethod: method, service: propName };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
path: 'smoke-manifest.json',
|
|
32
|
+
content: JSON.stringify(manifest, null, 2),
|
|
33
|
+
integrateTarget: false,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|