@workos/oagen-emitters 0.11.0 → 0.12.1
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 +19 -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-DW3cnedr.mjs → plugin-CmfzawTp.mjs} +2851 -524
- package/dist/plugin-CmfzawTp.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 +9 -9
- package/src/index.ts +1 -0
- package/src/plugin.ts +2 -1
- package/src/python/path-expression.ts +75 -26
- package/src/python/resources.ts +0 -9
- package/src/rust/client.ts +62 -0
- package/src/rust/enums.ts +201 -0
- package/src/rust/fixtures.ts +196 -0
- package/src/rust/index.ts +95 -0
- package/src/rust/manifest.ts +31 -0
- package/src/rust/models.ts +165 -0
- package/src/rust/naming.ts +131 -0
- package/src/rust/resources.ts +1324 -0
- package/src/rust/secret.ts +59 -0
- package/src/rust/tests.ts +818 -0
- package/src/rust/type-map.ts +225 -0
- package/test/entrypoint.test.ts +1 -0
- package/test/plugin.test.ts +2 -1
- package/test/python/resources.test.ts +2 -2
- package/test/rust/client.test.ts +62 -0
- package/test/rust/enums.test.ts +117 -0
- package/test/rust/fixtures.test.ts +227 -0
- package/test/rust/manifest.test.ts +73 -0
- package/test/rust/models.test.ts +177 -0
- package/test/rust/resources.test.ts +748 -0
- package/test/rust/tests.test.ts +504 -0
- package/test/rust/type-map.test.ts +83 -0
- package/dist/plugin-DW3cnedr.mjs.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { buildOperationsMap } from '../../src/rust/manifest.js';
|
|
5
|
+
|
|
6
|
+
const spec: ApiSpec = {
|
|
7
|
+
name: 'Test',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
baseUrl: '',
|
|
10
|
+
services: [
|
|
11
|
+
{
|
|
12
|
+
name: 'Organizations',
|
|
13
|
+
operations: [
|
|
14
|
+
{
|
|
15
|
+
name: 'createOrganization',
|
|
16
|
+
httpMethod: 'post',
|
|
17
|
+
path: '/organizations',
|
|
18
|
+
pathParams: [],
|
|
19
|
+
queryParams: [],
|
|
20
|
+
headerParams: [],
|
|
21
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
22
|
+
errors: [],
|
|
23
|
+
injectIdempotencyKey: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'getOrganization',
|
|
27
|
+
httpMethod: 'get',
|
|
28
|
+
path: '/organizations/{id}',
|
|
29
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
30
|
+
queryParams: [],
|
|
31
|
+
headerParams: [],
|
|
32
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
33
|
+
errors: [],
|
|
34
|
+
injectIdempotencyKey: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
models: [],
|
|
40
|
+
enums: [],
|
|
41
|
+
sdk: defaultSdkBehavior(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ctx: EmitterContext = {
|
|
45
|
+
namespace: 'workos',
|
|
46
|
+
namespacePascal: 'WorkOS',
|
|
47
|
+
spec,
|
|
48
|
+
resolvedOperations: spec.services.flatMap((service) =>
|
|
49
|
+
service.operations.map((operation) => ({
|
|
50
|
+
service,
|
|
51
|
+
operation,
|
|
52
|
+
methodName: operation.name,
|
|
53
|
+
mountOn: service.name,
|
|
54
|
+
defaults: {},
|
|
55
|
+
inferFromClient: [],
|
|
56
|
+
urlBuilder: false,
|
|
57
|
+
})),
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('rust/manifest', () => {
|
|
62
|
+
it('maps each HTTP operation to an SDK method + service accessor', () => {
|
|
63
|
+
const map = buildOperationsMap(spec, ctx);
|
|
64
|
+
expect(map['POST /organizations']).toEqual({
|
|
65
|
+
sdkMethod: 'create_organization',
|
|
66
|
+
service: 'organizations',
|
|
67
|
+
});
|
|
68
|
+
expect(map['GET /organizations/{id}']).toEqual({
|
|
69
|
+
sdkMethod: 'get_organization',
|
|
70
|
+
service: 'organizations',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateModels } from '../../src/rust/models.js';
|
|
5
|
+
import { generateClient } from '../../src/rust/client.js';
|
|
6
|
+
import { UnionRegistry } from '../../src/rust/type-map.js';
|
|
7
|
+
|
|
8
|
+
const emptySpec: ApiSpec = {
|
|
9
|
+
name: 'Test',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
baseUrl: '',
|
|
12
|
+
services: [],
|
|
13
|
+
models: [],
|
|
14
|
+
enums: [],
|
|
15
|
+
sdk: defaultSdkBehavior(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ctx: EmitterContext = {
|
|
19
|
+
namespace: 'workos',
|
|
20
|
+
namespacePascal: 'WorkOS',
|
|
21
|
+
spec: emptySpec,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('rust/models', () => {
|
|
25
|
+
it('emits only an empty barrel when no models', () => {
|
|
26
|
+
const files = generateModels([], ctx, new UnionRegistry());
|
|
27
|
+
expect(files).toHaveLength(1);
|
|
28
|
+
expect(files[0]!.path).toBe('src/models/mod.rs');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('generates a struct with required and optional fields', () => {
|
|
32
|
+
const models: Model[] = [
|
|
33
|
+
{
|
|
34
|
+
name: 'Organization',
|
|
35
|
+
fields: [
|
|
36
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
37
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
38
|
+
{
|
|
39
|
+
name: 'metadata',
|
|
40
|
+
type: { kind: 'map', valueType: { kind: 'primitive', type: 'string' } },
|
|
41
|
+
required: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const files = generateModels(models, ctx, new UnionRegistry());
|
|
47
|
+
expect(files.length).toBeGreaterThanOrEqual(2); // model + barrel
|
|
48
|
+
const orgFile = files.find((f) => f.path === 'src/models/organization.rs')!;
|
|
49
|
+
expect(orgFile).toBeDefined();
|
|
50
|
+
const content = orgFile.content;
|
|
51
|
+
expect(content).toContain('use serde::{Deserialize, Serialize};');
|
|
52
|
+
expect(content).toContain('pub struct Organization {');
|
|
53
|
+
expect(content).toContain('pub id: String,');
|
|
54
|
+
expect(content).toContain('pub name: String,');
|
|
55
|
+
expect(content).toContain('pub metadata: Option<std::collections::HashMap<String, String>>,');
|
|
56
|
+
expect(content).toContain('#[derive(Debug, Clone, Serialize, Deserialize)]');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renames struct fields when serde wire name differs', () => {
|
|
60
|
+
const models: Model[] = [
|
|
61
|
+
{
|
|
62
|
+
name: 'User',
|
|
63
|
+
fields: [{ name: 'userId', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
const files = generateModels(models, ctx, new UnionRegistry());
|
|
67
|
+
const userFile = files.find((f) => f.path === 'src/models/user.rs')!;
|
|
68
|
+
expect(userFile.content).toContain('#[serde(rename = "userId")]');
|
|
69
|
+
expect(userFile.content).toContain('pub user_id: String,');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('skips serializing None for optional fields', () => {
|
|
73
|
+
const models: Model[] = [
|
|
74
|
+
{
|
|
75
|
+
name: 'Maybe',
|
|
76
|
+
fields: [{ name: 'value', type: { kind: 'primitive', type: 'string' }, required: false }],
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
const files = generateModels(models, ctx, new UnionRegistry());
|
|
80
|
+
const f = files.find((x) => x.path === 'src/models/maybe.rs')!;
|
|
81
|
+
expect(f.content).toContain('#[serde(skip_serializing_if = "Option::is_none", default)]');
|
|
82
|
+
expect(f.content).toContain('pub value: Option<String>,');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('synthesises a _unions module when a model has an inline union field', () => {
|
|
86
|
+
const models: Model[] = [
|
|
87
|
+
{
|
|
88
|
+
name: 'Event',
|
|
89
|
+
fields: [
|
|
90
|
+
{
|
|
91
|
+
name: 'payload',
|
|
92
|
+
type: {
|
|
93
|
+
kind: 'union',
|
|
94
|
+
variants: [
|
|
95
|
+
{ kind: 'model', name: 'UserCreated' },
|
|
96
|
+
{ kind: 'model', name: 'UserDeleted' },
|
|
97
|
+
],
|
|
98
|
+
discriminator: {
|
|
99
|
+
property: 'event',
|
|
100
|
+
mapping: { 'user.created': 'UserCreated', 'user.deleted': 'UserDeleted' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: true,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const registry = new UnionRegistry();
|
|
109
|
+
const files = generateModels(models, ctx, registry);
|
|
110
|
+
const event = files.find((f) => f.path === 'src/models/event.rs')!;
|
|
111
|
+
expect(event.content).toContain('pub payload: EventPayloadOneOf,');
|
|
112
|
+
const barrel = files.find((f) => f.path === 'src/models/mod.rs')!;
|
|
113
|
+
expect(barrel.content).toContain('pub mod _unions;');
|
|
114
|
+
// The _unions.rs file is rendered by generateClient (the final structural
|
|
115
|
+
// pass) so resource-side body unions can join the same registry.
|
|
116
|
+
const clientFiles = generateClient(emptySpec, ctx, registry);
|
|
117
|
+
const unions = clientFiles.find((f) => f.path === 'src/models/_unions.rs')!;
|
|
118
|
+
expect(unions.content).toContain('#[serde(tag = "event")]');
|
|
119
|
+
expect(unions.content).toContain('pub enum EventPayloadOneOf {');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('documents Field.default as a "Defaults to" doc comment', () => {
|
|
123
|
+
const models: Model[] = [
|
|
124
|
+
{
|
|
125
|
+
name: 'Pagination',
|
|
126
|
+
fields: [
|
|
127
|
+
{
|
|
128
|
+
name: 'limit',
|
|
129
|
+
type: { kind: 'primitive', type: 'integer' },
|
|
130
|
+
required: false,
|
|
131
|
+
description: 'Page size.',
|
|
132
|
+
default: 10,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'order',
|
|
136
|
+
type: { kind: 'primitive', type: 'string' },
|
|
137
|
+
required: false,
|
|
138
|
+
default: 'desc',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'verbose',
|
|
142
|
+
type: { kind: 'primitive', type: 'boolean' },
|
|
143
|
+
required: false,
|
|
144
|
+
default: true,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
const files = generateModels(models, ctx, new UnionRegistry());
|
|
150
|
+
const f = files.find((x) => x.path === 'src/models/pagination.rs')!;
|
|
151
|
+
// Number default with description: description first, blank `///`, then defaults.
|
|
152
|
+
expect(f.content).toContain('/// Page size.');
|
|
153
|
+
expect(f.content).toContain('/// Defaults to `10`.');
|
|
154
|
+
// String default renders bare (no JSON quotes).
|
|
155
|
+
expect(f.content).toContain('/// Defaults to `desc`.');
|
|
156
|
+
// Boolean default uses JSON encoding (`true`, not `"true"`).
|
|
157
|
+
expect(f.content).toContain('/// Defaults to `true`.');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('emits a barrel re-exporting each module', () => {
|
|
161
|
+
const models: Model[] = [
|
|
162
|
+
{
|
|
163
|
+
name: 'Alpha',
|
|
164
|
+
fields: [{ name: 'x', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Beta',
|
|
168
|
+
fields: [{ name: 'y', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
const files = generateModels(models, ctx, new UnionRegistry());
|
|
172
|
+
const barrel = files.find((f) => f.path === 'src/models/mod.rs')!;
|
|
173
|
+
expect(barrel.content).toContain('pub mod alpha;');
|
|
174
|
+
expect(barrel.content).toContain('pub mod beta;');
|
|
175
|
+
expect(barrel.content).toContain('pub use alpha::*;');
|
|
176
|
+
});
|
|
177
|
+
});
|