@ic-reactor/codegen 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.
@@ -1,573 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import {
3
- BUILT_IN_JSDOC_FORMAT_TYPES,
4
- generateCodecDeclarations,
5
- } from "./renderer.js"
6
- import type { CandidSchema } from "@ic-reactor/parser"
7
-
8
- describe("Codec Declarations Generator", () => {
9
- it("generates declarations for simple services and types", () => {
10
- const schema: CandidSchema = {
11
- types: [
12
- {
13
- name: "Profile",
14
- type: {
15
- kind: "record",
16
- fields: [
17
- { name: "name", type: { kind: "text" } },
18
- { name: "age", type: { kind: "nat8" } },
19
- ],
20
- },
21
- },
22
- ],
23
- service: {
24
- methods: [
25
- {
26
- name: "getProfile",
27
- mode: "query",
28
- args: [{ kind: "principal" }],
29
- returns: [{ kind: "reference", name: "Profile" }],
30
- },
31
- ],
32
- },
33
- }
34
-
35
- const code = generateCodecDeclarations(schema, "myService")
36
-
37
- expect(code).toContain('import { c } from "@ic-reactor/cod"')
38
- expect(code).toContain("export const Profile = c.record({")
39
- expect(code).toContain("name: c.text(),")
40
- expect(code).toContain("age: c.nat8(),")
41
- expect(code).toContain("export type Profile = c.infer<typeof Profile>")
42
- expect(code).toContain("export const myService = c.service({")
43
- expect(code).toContain("getProfile: c.query([c.principal()], Profile),")
44
- expect(code).not.toContain("export const idlFactory")
45
- expect(code).not.toContain("export type _SERVICE")
46
- expect(code).not.toContain("export const manifest")
47
- expect(code).not.toContain("IDL.")
48
- expect(code).not.toContain("init")
49
- })
50
-
51
- it("can emit compatibility service aliases when requested", () => {
52
- const schema: CandidSchema = {
53
- types: [],
54
- service: {
55
- methods: [
56
- {
57
- name: "ping",
58
- mode: "query",
59
- args: [],
60
- returns: [{ kind: "text" }],
61
- },
62
- ],
63
- },
64
- }
65
-
66
- const code = generateCodecDeclarations(schema, {
67
- serviceExportName: "backend",
68
- includeCompatibilityExports: true,
69
- })
70
-
71
- expect(code).toContain("export const backend = c.service({")
72
- expect(code).toContain("export const idlFactory = backend.idlFactory")
73
- expect(code).toContain("export type _SERVICE = c.ServiceOf<typeof backend>")
74
- expect(code).toContain("export const manifest = backend.manifest()")
75
- })
76
-
77
- it("falls back to _SERVICE when neither canisterName nor serviceExportName is provided", () => {
78
- const schema: CandidSchema = {
79
- types: [
80
- {
81
- name: "canister",
82
- type: {
83
- kind: "record",
84
- fields: [{ name: "name", type: { kind: "text" } }],
85
- },
86
- },
87
- ],
88
- service: {
89
- methods: [
90
- {
91
- name: "get",
92
- mode: "query",
93
- args: [],
94
- returns: [{ kind: "reference", name: "canister" }],
95
- },
96
- ],
97
- },
98
- }
99
-
100
- const code = generateCodecDeclarations(schema)
101
-
102
- expect(code).toContain("export const canister = c.record({")
103
- expect(code).toContain("export const _SERVICE = c.service({")
104
- expect(code).toContain("get: c.query([], canister),")
105
- })
106
-
107
- it("derives SCREAMING_SNAKE_CASE service export from canisterName", () => {
108
- const schema: CandidSchema = {
109
- types: [],
110
- service: {
111
- methods: [
112
- {
113
- name: "ping",
114
- mode: "query",
115
- args: [],
116
- returns: [{ kind: "text" }],
117
- },
118
- ],
119
- },
120
- }
121
-
122
- const code = generateCodecDeclarations(schema, {
123
- canisterName: "my_backend",
124
- })
125
-
126
- expect(code).toContain("export const MY_BACKEND = c.service({")
127
- })
128
-
129
- it("converts hyphens to underscores for canisterName", () => {
130
- const schema: CandidSchema = {
131
- types: [],
132
- service: {
133
- methods: [
134
- {
135
- name: "ping",
136
- mode: "query",
137
- args: [],
138
- returns: [{ kind: "text" }],
139
- },
140
- ],
141
- },
142
- }
143
-
144
- const code = generateCodecDeclarations(schema, {
145
- canisterName: "internet-identity",
146
- })
147
-
148
- expect(code).toContain("export const INTERNET_IDENTITY = c.service({")
149
- })
150
-
151
- it("appends _SERVICE suffix when canisterName collides with a declared type", () => {
152
- const schema: CandidSchema = {
153
- types: [
154
- {
155
- name: "BACKEND",
156
- type: {
157
- kind: "record",
158
- fields: [{ name: "id", type: { kind: "text" } }],
159
- },
160
- },
161
- ],
162
- service: {
163
- methods: [
164
- {
165
- name: "get",
166
- mode: "query",
167
- args: [],
168
- returns: [{ kind: "reference", name: "BACKEND" }],
169
- },
170
- ],
171
- },
172
- }
173
-
174
- const code = generateCodecDeclarations(schema, {
175
- canisterName: "backend",
176
- })
177
-
178
- expect(code).toContain("export const BACKEND = c.record({")
179
- expect(code).toContain("export const BACKEND_SERVICE = c.service({")
180
- })
181
-
182
- it("serviceExportName takes priority over canisterName", () => {
183
- const schema: CandidSchema = {
184
- types: [],
185
- service: {
186
- methods: [
187
- {
188
- name: "ping",
189
- mode: "query",
190
- args: [],
191
- returns: [{ kind: "text" }],
192
- },
193
- ],
194
- },
195
- }
196
-
197
- const code = generateCodecDeclarations(schema, {
198
- canisterName: "backend",
199
- serviceExportName: "myCustomName",
200
- })
201
-
202
- expect(code).toContain("export const myCustomName = c.service({")
203
- expect(code).not.toContain("BACKEND")
204
- })
205
-
206
- it("sorts type declarations topologically", () => {
207
- const schema: CandidSchema = {
208
- types: [
209
- {
210
- name: "Result",
211
- type: {
212
- kind: "variant",
213
- fields: [
214
- { name: "Ok", type: { kind: "reference", name: "Profile" } },
215
- { name: "Err", type: { kind: "text" } },
216
- ],
217
- },
218
- },
219
- {
220
- name: "Profile",
221
- type: {
222
- kind: "record",
223
- fields: [{ name: "name", type: { kind: "text" } }],
224
- },
225
- },
226
- ],
227
- service: null,
228
- }
229
-
230
- const code = generateCodecDeclarations(schema)
231
- const profileIdx = code.indexOf("export const Profile")
232
- const resultIdx = code.indexOf("export const Result")
233
-
234
- expect(profileIdx).toBeGreaterThan(0)
235
- expect(resultIdx).toBeGreaterThan(0)
236
- expect(profileIdx).toBeLessThan(resultIdx)
237
- })
238
-
239
- it("rejects recursive references until c.recursive is implemented", () => {
240
- const schema: CandidSchema = {
241
- types: [
242
- {
243
- name: "Node",
244
- type: {
245
- kind: "record",
246
- fields: [
247
- { name: "value", type: { kind: "text" } },
248
- {
249
- name: "next",
250
- type: {
251
- kind: "opt",
252
- type: { kind: "reference", name: "Node" },
253
- },
254
- },
255
- ],
256
- },
257
- },
258
- ],
259
- service: null,
260
- }
261
-
262
- expect(() => generateCodecDeclarations(schema)).toThrow(
263
- "Recursive Candid types are not supported yet: Node -> Node"
264
- )
265
- })
266
-
267
- it("falls back to reserved for unsupported Candid type categories", () => {
268
- const schema: CandidSchema = {
269
- types: [
270
- {
271
- name: "WithFunc",
272
- type: {
273
- kind: "record",
274
- fields: [{ name: "callback", type: { kind: "func" } }],
275
- },
276
- },
277
- ],
278
- service: null,
279
- }
280
-
281
- const code = generateCodecDeclarations(schema)
282
-
283
- expect(code).toContain(
284
- "callback: /* c.func is not supported */ c.reserved(),"
285
- )
286
- })
287
-
288
- it("uses no-return query/update codecs without undefined casts", () => {
289
- const schema: CandidSchema = {
290
- types: [],
291
- service: {
292
- methods: [
293
- {
294
- name: "status",
295
- mode: "query",
296
- args: [],
297
- returns: [],
298
- },
299
- {
300
- name: "fireAndForget",
301
- mode: "update",
302
- args: [{ kind: "text" }],
303
- returns: [],
304
- },
305
- ],
306
- },
307
- }
308
-
309
- const code = generateCodecDeclarations(schema)
310
-
311
- expect(code).toContain("status: c.query([]),")
312
- expect(code).toContain("fireAndForget: c.update([c.text()]),")
313
- expect(code).not.toContain("undefined as any")
314
- })
315
-
316
- it("renders multiple method returns as separate return codecs", () => {
317
- const schema: CandidSchema = {
318
- types: [],
319
- service: {
320
- methods: [
321
- {
322
- name: "stats",
323
- mode: "query",
324
- args: [],
325
- returns: [{ kind: "text" }, { kind: "nat64" }],
326
- },
327
- ],
328
- },
329
- }
330
-
331
- const code = generateCodecDeclarations(schema)
332
-
333
- expect(code).toContain("stats: c.query([], [c.text(), c.nat64()]),")
334
- expect(code).not.toContain("c.tuple([c.text(), c.nat64()])")
335
- })
336
-
337
- it("renders docs and validation tags as codec metadata", () => {
338
- const schema: CandidSchema = {
339
- types: [
340
- {
341
- name: "Profile",
342
- metadata: {
343
- description: "A user profile.",
344
- docs: ["A user profile."],
345
- },
346
- type: {
347
- kind: "record",
348
- fields: [
349
- {
350
- name: "email",
351
- type: { kind: "text" },
352
- metadata: {
353
- description: "Contact email.",
354
- docs: ["Contact email.", "@format email"],
355
- validation: {
356
- format: {
357
- type: "email",
358
- },
359
- },
360
- },
361
- },
362
- {
363
- name: "name",
364
- type: { kind: "text" },
365
- metadata: {
366
- description: "Display name.",
367
- docs: ["Display name.", "@minLength 2"],
368
- validation: {
369
- minLength: { value: "2" },
370
- },
371
- },
372
- },
373
- ],
374
- },
375
- },
376
- ],
377
- service: {
378
- methods: [
379
- {
380
- name: "save",
381
- mode: "update",
382
- args: [{ kind: "reference", name: "Profile" }],
383
- returns: [],
384
- metadata: {
385
- description: "Save a profile.",
386
- docs: ["Save a profile."],
387
- },
388
- },
389
- ],
390
- metadata: {
391
- description: "Profile service.",
392
- docs: ["Profile service."],
393
- },
394
- },
395
- }
396
-
397
- const code = generateCodecDeclarations(schema)
398
-
399
- expect(code).toContain("export const Profile = c.record({")
400
- expect(code).toContain('}).describe("A user profile.")')
401
- expect(code).not.toContain(
402
- 'describe("A user profile.").meta({"docs":["A user profile."]})'
403
- )
404
- expect(code).toContain('email: c.email().describe("Contact email."),')
405
- expect(code).toContain(
406
- 'name: c.text().describe("Display name.").meta({"docs":["Display name.","@minLength 2"],"validation":{"minLength":{"value":"2","message":"Must be at least 2 characters"}}}),'
407
- )
408
- expect(code).toContain("c.service({")
409
- expect(code).toContain('}).describe("Profile service.")')
410
- expect(code).not.toContain(
411
- 'describe("Profile service.").meta({"docs":["Profile service."]})'
412
- )
413
- expect(code).toContain(
414
- 'save: c.update([Profile]).describe("Save a profile.")'
415
- )
416
- expect(code).not.toContain(
417
- 'describe("Save a profile.").meta({"docs":["Save a profile."]})'
418
- )
419
- })
420
-
421
- it("lets custom format definitions override built-in defaults", () => {
422
- const schema: CandidSchema = {
423
- types: [
424
- {
425
- name: "Profile",
426
- type: {
427
- kind: "record",
428
- fields: [
429
- {
430
- name: "email",
431
- type: { kind: "text" },
432
- metadata: {
433
- validation: {
434
- format: {
435
- type: "email",
436
- },
437
- },
438
- },
439
- },
440
- ],
441
- },
442
- },
443
- ],
444
- service: null,
445
- }
446
-
447
- const code = generateCodecDeclarations(schema, {
448
- customJSDocFormatTypes: {
449
- email: {
450
- regex: "^[^@]+@[^@]+$",
451
- errorMessage: "Configured email error",
452
- },
453
- },
454
- })
455
-
456
- expect(code).toContain(
457
- 'email: c.text().meta({"validation":{"format":{"type":"email","regex":"^[^@]+@[^@]+$","jsonSchemaFormat":"email","errorMessage":"Configured email error"}}}),'
458
- )
459
- })
460
-
461
- it("includes Zod-compatible built-in string format names", () => {
462
- expect(Object.keys(BUILT_IN_JSDOC_FORMAT_TYPES).sort()).toEqual(
463
- [
464
- "base64",
465
- "base64url",
466
- "cidrv4",
467
- "cidrv6",
468
- "cuid",
469
- "cuid2",
470
- "date",
471
- "date-time",
472
- "datetime",
473
- "duration",
474
- "email",
475
- "emoji",
476
- "guid",
477
- "httpsUrl",
478
- "ipv4",
479
- "ipv6",
480
- "mac",
481
- "nanoid",
482
- "time",
483
- "ulid",
484
- "uri",
485
- "url",
486
- "uuid",
487
- ].sort()
488
- )
489
- })
490
-
491
- it("renders built-in text format helpers instead of inline metadata", () => {
492
- const schema: CandidSchema = {
493
- types: [
494
- {
495
- name: "WithFormats",
496
- type: {
497
- kind: "record",
498
- fields: [
499
- {
500
- name: "createdAt",
501
- type: { kind: "text" },
502
- metadata: {
503
- validation: { format: { type: "date-time" } },
504
- },
505
- },
506
- {
507
- name: "avatar",
508
- type: { kind: "text" },
509
- metadata: {
510
- validation: { format: { type: "base64" } },
511
- },
512
- },
513
- {
514
- name: "homepage",
515
- type: { kind: "text" },
516
- metadata: {
517
- validation: { format: { type: "uri" } },
518
- },
519
- },
520
- ],
521
- },
522
- },
523
- ],
524
- service: null,
525
- }
526
-
527
- const code = generateCodecDeclarations(schema)
528
-
529
- expect(code).toContain("createdAt: c.dateTime(),")
530
- expect(code).toContain("avatar: c.base64(),")
531
- expect(code).toContain("homepage: c.uri(),")
532
- })
533
-
534
- it("uses built-in format helpers with compact message overrides", () => {
535
- const schema: CandidSchema = {
536
- types: [
537
- {
538
- name: "Profile",
539
- type: {
540
- kind: "record",
541
- fields: [
542
- {
543
- name: "id",
544
- type: { kind: "text" },
545
- metadata: {
546
- description: "Public profile identifier.",
547
- docs: ["Public profile identifier.", "@format uuid UUID"],
548
- validation: {
549
- format: {
550
- type: "uuid",
551
- message: "UUID",
552
- },
553
- },
554
- },
555
- },
556
- ],
557
- },
558
- },
559
- ],
560
- service: null,
561
- }
562
-
563
- const code = generateCodecDeclarations(schema)
564
-
565
- expect(code).toContain(
566
- 'id: c.uuid("UUID").describe("Public profile identifier."),'
567
- )
568
- expect(code).not.toContain(".meta(")
569
- expect(code).not.toContain('"docs"')
570
- expect(code).not.toContain('"regex"')
571
- expect(code).not.toContain('"jsonSchemaFormat"')
572
- })
573
- })