@workos/oagen-emitters 0.12.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 +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-C408Wh-o.mjs → plugin-CmfzawTp.mjs} +825 -66
- package/dist/plugin-CmfzawTp.mjs.map +1 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/package.json +9 -9
- package/src/rust/fixtures.ts +87 -1
- package/src/rust/models.ts +17 -2
- package/src/rust/resources.ts +697 -62
- package/src/rust/tests.ts +540 -20
- package/test/rust/fixtures.test.ts +227 -0
- package/test/rust/models.test.ts +38 -0
- package/test/rust/resources.test.ts +505 -2
- package/test/rust/tests.test.ts +504 -0
- package/dist/plugin-C408Wh-o.mjs.map +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { ApiSpec, Enum, Model } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { exampleFromSpec, generateFixtures, generateModelFixture } from '../../src/rust/fixtures.js';
|
|
5
|
+
|
|
6
|
+
function spec(models: Model[], enums: Enum[] = []): ApiSpec {
|
|
7
|
+
return {
|
|
8
|
+
name: 'Test',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
baseUrl: '',
|
|
11
|
+
services: [],
|
|
12
|
+
models,
|
|
13
|
+
enums,
|
|
14
|
+
sdk: defaultSdkBehavior(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('rust/fixtures', () => {
|
|
19
|
+
it('prefers a spec `example` over the generated placeholder for a primitive field', () => {
|
|
20
|
+
const models: Model[] = [
|
|
21
|
+
{
|
|
22
|
+
name: 'Event',
|
|
23
|
+
fields: [
|
|
24
|
+
{
|
|
25
|
+
name: 'id',
|
|
26
|
+
type: { kind: 'primitive', type: 'string' },
|
|
27
|
+
required: true,
|
|
28
|
+
example: 'event_01XXXX',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'created_at',
|
|
32
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
33
|
+
required: true,
|
|
34
|
+
example: '2026-02-02T16:35:39.317Z',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const files = generateFixtures(spec(models));
|
|
40
|
+
const file = files.find((f) => f.path === 'tests/fixtures/event.json')!;
|
|
41
|
+
expect(file).toBeDefined();
|
|
42
|
+
const parsed = JSON.parse(file.content);
|
|
43
|
+
expect(parsed.id).toBe('event_01XXXX');
|
|
44
|
+
expect(parsed.created_at).toBe('2026-02-02T16:35:39.317Z');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('falls back to the placeholder when the example shape does not match the type', () => {
|
|
48
|
+
const models: Model[] = [
|
|
49
|
+
{
|
|
50
|
+
name: 'Wrong',
|
|
51
|
+
fields: [
|
|
52
|
+
// Type is integer but example is a string — must fall back.
|
|
53
|
+
{
|
|
54
|
+
name: 'count',
|
|
55
|
+
type: { kind: 'primitive', type: 'integer' },
|
|
56
|
+
required: true,
|
|
57
|
+
example: 'not-a-number',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const files = generateFixtures(spec(models));
|
|
63
|
+
const file = files.find((f) => f.path === 'tests/fixtures/wrong.json')!;
|
|
64
|
+
const parsed = JSON.parse(file.content);
|
|
65
|
+
expect(parsed.count).toBe(0); // placeholder fallback
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses an example array of strings for an array<string> field', () => {
|
|
69
|
+
const models: Model[] = [
|
|
70
|
+
{
|
|
71
|
+
name: 'Org',
|
|
72
|
+
fields: [
|
|
73
|
+
{
|
|
74
|
+
name: 'domains',
|
|
75
|
+
type: {
|
|
76
|
+
kind: 'array',
|
|
77
|
+
items: { kind: 'primitive', type: 'string' },
|
|
78
|
+
},
|
|
79
|
+
required: true,
|
|
80
|
+
example: ['example.com', 'foo.com'],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const files = generateFixtures(spec(models));
|
|
86
|
+
const file = files.find((f) => f.path === 'tests/fixtures/org.json')!;
|
|
87
|
+
const parsed = JSON.parse(file.content);
|
|
88
|
+
expect(parsed.domains).toEqual(['example.com', 'foo.com']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('skips a model-shaped example to avoid mis-shaped nested structs', () => {
|
|
92
|
+
const models: Model[] = [
|
|
93
|
+
{
|
|
94
|
+
name: 'Outer',
|
|
95
|
+
fields: [
|
|
96
|
+
{
|
|
97
|
+
name: 'actor',
|
|
98
|
+
type: { kind: 'model', name: 'Actor' },
|
|
99
|
+
required: true,
|
|
100
|
+
// Provided as a free-form example; we should NOT use it verbatim.
|
|
101
|
+
example: { not_a_real_field: 'whoops' },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Actor',
|
|
107
|
+
fields: [
|
|
108
|
+
{
|
|
109
|
+
name: 'id',
|
|
110
|
+
type: { kind: 'primitive', type: 'string' },
|
|
111
|
+
required: true,
|
|
112
|
+
example: 'user_TF4C5938',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'type',
|
|
116
|
+
type: { kind: 'primitive', type: 'string' },
|
|
117
|
+
required: true,
|
|
118
|
+
example: 'user',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
const files = generateFixtures(spec(models));
|
|
124
|
+
const file = files.find((f) => f.path === 'tests/fixtures/outer.json')!;
|
|
125
|
+
const parsed = JSON.parse(file.content);
|
|
126
|
+
// The nested model is regenerated from its own fields' examples, not from
|
|
127
|
+
// the parent's free-form example blob.
|
|
128
|
+
expect(parsed.actor).toEqual({ id: 'user_TF4C5938', type: 'user' });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses an enum example only when it matches a known enum value', () => {
|
|
132
|
+
const enums: Enum[] = [
|
|
133
|
+
{
|
|
134
|
+
name: 'Status',
|
|
135
|
+
values: [
|
|
136
|
+
{ name: 'Active', value: 'active' },
|
|
137
|
+
{ name: 'Pending', value: 'pending' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
const models: Model[] = [
|
|
142
|
+
{
|
|
143
|
+
name: 'GoodEx',
|
|
144
|
+
fields: [
|
|
145
|
+
{
|
|
146
|
+
name: 'status',
|
|
147
|
+
type: { kind: 'enum', name: 'Status' },
|
|
148
|
+
required: true,
|
|
149
|
+
example: 'pending',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'BadEx',
|
|
155
|
+
fields: [
|
|
156
|
+
{
|
|
157
|
+
name: 'status',
|
|
158
|
+
type: { kind: 'enum', name: 'Status' },
|
|
159
|
+
required: true,
|
|
160
|
+
example: 'something_unknown',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
const files = generateFixtures(spec(models, enums));
|
|
166
|
+
const good = JSON.parse(files.find((f) => f.path === 'tests/fixtures/good_ex.json')!.content);
|
|
167
|
+
const bad = JSON.parse(files.find((f) => f.path === 'tests/fixtures/bad_ex.json')!.content);
|
|
168
|
+
expect(good.status).toBe('pending'); // valid example wins
|
|
169
|
+
expect(bad.status).toBe('active'); // unknown example → first enum value
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('treats null examples as unusable so required fields keep a value', () => {
|
|
173
|
+
const models: Model[] = [
|
|
174
|
+
{
|
|
175
|
+
name: 'Nullish',
|
|
176
|
+
fields: [
|
|
177
|
+
{
|
|
178
|
+
name: 'name',
|
|
179
|
+
type: { kind: 'primitive', type: 'string' },
|
|
180
|
+
required: true,
|
|
181
|
+
example: null,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const files = generateFixtures(spec(models));
|
|
187
|
+
const parsed = JSON.parse(files.find((f) => f.path === 'tests/fixtures/nullish.json')!.content);
|
|
188
|
+
expect(parsed.name).toBe('test_name');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('exampleFromSpec exposes the shape-checking helper for reuse', () => {
|
|
192
|
+
const enums = new Map<string, Enum>();
|
|
193
|
+
// Primitives are unwrapped through nullable.
|
|
194
|
+
expect(exampleFromSpec('hello', { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, enums)).toBe(
|
|
195
|
+
'hello',
|
|
196
|
+
);
|
|
197
|
+
// Integer floats are rejected (they would corrupt typed deserialisation).
|
|
198
|
+
expect(exampleFromSpec(1.5, { kind: 'primitive', type: 'integer' }, enums)).toBeUndefined();
|
|
199
|
+
// Empty arrays fall back so the placeholder can emit a one-element array.
|
|
200
|
+
expect(exampleFromSpec([], { kind: 'array', items: { kind: 'primitive', type: 'string' } }, enums)).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('threads required-only field selection through generateModelFixture', () => {
|
|
204
|
+
const models: Model[] = [
|
|
205
|
+
{
|
|
206
|
+
name: 'Mixed',
|
|
207
|
+
fields: [
|
|
208
|
+
{
|
|
209
|
+
name: 'kept',
|
|
210
|
+
type: { kind: 'primitive', type: 'string' },
|
|
211
|
+
required: true,
|
|
212
|
+
example: 'real',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'dropped',
|
|
216
|
+
type: { kind: 'primitive', type: 'string' },
|
|
217
|
+
required: false,
|
|
218
|
+
example: 'ignored',
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
const modelMap = new Map(models.map((m) => [m.name, m]));
|
|
224
|
+
const fixture = generateModelFixture(models[0]!, modelMap, new Map(), new Set());
|
|
225
|
+
expect(fixture).toEqual({ kept: 'real' });
|
|
226
|
+
});
|
|
227
|
+
});
|
package/test/rust/models.test.ts
CHANGED
|
@@ -119,6 +119,44 @@ describe('rust/models', () => {
|
|
|
119
119
|
expect(unions.content).toContain('pub enum EventPayloadOneOf {');
|
|
120
120
|
});
|
|
121
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
|
+
|
|
122
160
|
it('emits a barrel re-exporting each module', () => {
|
|
123
161
|
const models: Model[] = [
|
|
124
162
|
{
|