@ic-reactor/candid 3.0.13-beta.0 → 3.0.14-beta.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/README.md +28 -0
- package/dist/visitor/arguments/helpers.d.ts +5 -5
- package/dist/visitor/arguments/helpers.d.ts.map +1 -1
- package/dist/visitor/arguments/helpers.js.map +1 -1
- package/dist/visitor/arguments/index.d.ts +12 -2
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +140 -25
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +76 -419
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/constants.d.ts.map +1 -1
- package/dist/visitor/constants.js +19 -17
- package/dist/visitor/constants.js.map +1 -1
- package/dist/visitor/returns/types.d.ts +3 -4
- package/dist/visitor/returns/types.d.ts.map +1 -1
- package/dist/visitor/types.d.ts +14 -0
- package/dist/visitor/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/visitor/arguments/helpers.ts +7 -7
- package/src/visitor/arguments/index.test.ts +104 -39
- package/src/visitor/arguments/index.ts +170 -42
- package/src/visitor/arguments/schema.test.ts +114 -4
- package/src/visitor/arguments/types.ts +113 -480
- package/src/visitor/constants.ts +24 -15
- package/src/visitor/returns/index.test.ts +1 -1
- package/src/visitor/returns/types.ts +4 -27
- package/src/visitor/types.ts +45 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isQuery } from "../helpers"
|
|
2
|
+
import { checkTextFormat, checkNumberFormat } from "../constants"
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
+
FieldNode,
|
|
4
5
|
RecordField,
|
|
5
6
|
VariantField,
|
|
6
7
|
TupleField,
|
|
@@ -20,6 +21,8 @@ import type {
|
|
|
20
21
|
PrimitiveInputProps,
|
|
21
22
|
BlobLimits,
|
|
22
23
|
BlobValidationResult,
|
|
24
|
+
TextFormat,
|
|
25
|
+
NumberFormat,
|
|
23
26
|
} from "./types"
|
|
24
27
|
|
|
25
28
|
import { IDL } from "@icp-sdk/core/candid"
|
|
@@ -29,6 +32,8 @@ import * as z from "zod"
|
|
|
29
32
|
import { formatLabel } from "./helpers"
|
|
30
33
|
|
|
31
34
|
export * from "./types"
|
|
35
|
+
export * from "./helpers"
|
|
36
|
+
export { checkTextFormat, checkNumberFormat } from "../constants"
|
|
32
37
|
|
|
33
38
|
// ════════════════════════════════════════════════════════════════════════════
|
|
34
39
|
// Render Hint Helpers
|
|
@@ -177,7 +182,7 @@ function validateBlobInput(
|
|
|
177
182
|
*/
|
|
178
183
|
export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
179
184
|
string,
|
|
180
|
-
|
|
185
|
+
FieldNode | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
|
|
181
186
|
> {
|
|
182
187
|
public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
|
|
183
188
|
|
|
@@ -231,7 +236,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
231
236
|
const fields = t.argTypes.map((arg, index) => {
|
|
232
237
|
return this.withName(`[${index}]`, () =>
|
|
233
238
|
arg.accept(this, `__arg${index}`)
|
|
234
|
-
) as
|
|
239
|
+
) as FieldNode
|
|
235
240
|
})
|
|
236
241
|
|
|
237
242
|
const defaultValues = fields.map((field) => field.defaultValue)
|
|
@@ -272,18 +277,16 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
272
277
|
label: string
|
|
273
278
|
): RecordField {
|
|
274
279
|
const name = this.currentName()
|
|
275
|
-
const fields:
|
|
276
|
-
const fieldMap = new Map<string, Field>()
|
|
280
|
+
const fields: FieldNode[] = []
|
|
277
281
|
const defaultValue: Record<string, unknown> = {}
|
|
278
282
|
const schemaShape: Record<string, z.ZodTypeAny> = {}
|
|
279
283
|
|
|
280
284
|
for (const [key, type] of fields_) {
|
|
281
285
|
const field = this.withName(name ? `.${key}` : key, () =>
|
|
282
286
|
type.accept(this, key)
|
|
283
|
-
) as
|
|
287
|
+
) as FieldNode
|
|
284
288
|
|
|
285
289
|
fields.push(field)
|
|
286
|
-
fieldMap.set(key, field)
|
|
287
290
|
defaultValue[key] = field.defaultValue
|
|
288
291
|
schemaShape[key] = field.schema
|
|
289
292
|
}
|
|
@@ -298,7 +301,6 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
298
301
|
component: "record-container",
|
|
299
302
|
renderHint: COMPOUND_RENDER_HINT,
|
|
300
303
|
fields,
|
|
301
|
-
fieldMap,
|
|
302
304
|
defaultValue,
|
|
303
305
|
schema,
|
|
304
306
|
candidType: "record",
|
|
@@ -311,42 +313,55 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
311
313
|
label: string
|
|
312
314
|
): VariantField {
|
|
313
315
|
const name = this.currentName()
|
|
314
|
-
const fields:
|
|
315
|
-
const options: string[] = []
|
|
316
|
-
const optionMap = new Map<string, Field>()
|
|
316
|
+
const fields: FieldNode[] = []
|
|
317
317
|
const variantSchemas: z.ZodTypeAny[] = []
|
|
318
318
|
|
|
319
319
|
for (const [key, type] of fields_) {
|
|
320
320
|
const field = this.withName(`.${key}`, () =>
|
|
321
321
|
type.accept(this, key)
|
|
322
|
-
) as
|
|
322
|
+
) as FieldNode
|
|
323
323
|
|
|
324
324
|
fields.push(field)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
|
|
326
|
+
if (field.type === "null") {
|
|
327
|
+
variantSchemas.push(z.object({ _type: z.literal(key) }))
|
|
328
|
+
} else {
|
|
329
|
+
variantSchemas.push(
|
|
330
|
+
z.object({
|
|
331
|
+
_type: z.literal(key),
|
|
332
|
+
[key]: field.schema,
|
|
333
|
+
})
|
|
334
|
+
)
|
|
335
|
+
}
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
const defaultOption = options[0]
|
|
331
338
|
const firstField = fields[0]
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
const defaultOption = firstField.label
|
|
340
|
+
|
|
341
|
+
const defaultValue =
|
|
342
|
+
firstField.type === "null"
|
|
343
|
+
? { _type: defaultOption }
|
|
344
|
+
: {
|
|
345
|
+
_type: defaultOption,
|
|
346
|
+
[defaultOption]: firstField.defaultValue,
|
|
347
|
+
}
|
|
335
348
|
|
|
336
349
|
const schema = z.union(variantSchemas as [z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
337
350
|
|
|
338
351
|
// Helper to get default value for any option
|
|
339
352
|
const getOptionDefault = (option: string): Record<string, unknown> => {
|
|
340
|
-
const optField =
|
|
353
|
+
const optField = fields.find((f) => f.label === option)
|
|
341
354
|
if (!optField) {
|
|
342
355
|
throw new Error(`Unknown variant option: ${option}`)
|
|
343
356
|
}
|
|
344
|
-
return
|
|
357
|
+
return optField.type === "null"
|
|
358
|
+
? { _type: option }
|
|
359
|
+
: { _type: option, [option]: optField.defaultValue }
|
|
345
360
|
}
|
|
346
361
|
|
|
347
362
|
// Helper to get field for a specific option
|
|
348
|
-
const getField = (option: string):
|
|
349
|
-
const optField =
|
|
363
|
+
const getField = (option: string): FieldNode => {
|
|
364
|
+
const optField = fields.find((f) => f.label === option)
|
|
350
365
|
if (!optField) {
|
|
351
366
|
throw new Error(`Unknown variant option: ${option}`)
|
|
352
367
|
}
|
|
@@ -355,12 +370,17 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
355
370
|
|
|
356
371
|
// Helper to get currently selected option from a value
|
|
357
372
|
const getSelectedOption = (value: Record<string, unknown>): string => {
|
|
358
|
-
|
|
373
|
+
if (value._type && typeof value._type === "string") {
|
|
374
|
+
return value._type
|
|
375
|
+
}
|
|
376
|
+
const validKeys = Object.keys(value).filter((k) =>
|
|
377
|
+
fields.some((f) => f.label === k)
|
|
378
|
+
)
|
|
359
379
|
return validKeys[0] ?? defaultOption
|
|
360
380
|
}
|
|
361
381
|
|
|
362
382
|
// Helper to get selected field from a value
|
|
363
|
-
const getSelectedField = (value: Record<string, unknown>):
|
|
383
|
+
const getSelectedField = (value: Record<string, unknown>): FieldNode => {
|
|
364
384
|
const selectedOption = getSelectedOption(value)
|
|
365
385
|
return getField(selectedOption)
|
|
366
386
|
}
|
|
@@ -373,9 +393,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
373
393
|
component: "variant-select",
|
|
374
394
|
renderHint: COMPOUND_RENDER_HINT,
|
|
375
395
|
fields,
|
|
376
|
-
options,
|
|
377
396
|
defaultOption,
|
|
378
|
-
optionMap,
|
|
379
397
|
defaultValue,
|
|
380
398
|
schema,
|
|
381
399
|
getOptionDefault,
|
|
@@ -392,7 +410,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
392
410
|
label: string
|
|
393
411
|
): TupleField {
|
|
394
412
|
const name = this.currentName()
|
|
395
|
-
const fields:
|
|
413
|
+
const fields: FieldNode[] = []
|
|
396
414
|
const defaultValue: unknown[] = []
|
|
397
415
|
const schemas: z.ZodTypeAny[] = []
|
|
398
416
|
|
|
@@ -400,7 +418,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
400
418
|
const type = components[index]
|
|
401
419
|
const field = this.withName(`[${index}]`, () =>
|
|
402
420
|
type.accept(this, `_${index}_`)
|
|
403
|
-
) as
|
|
421
|
+
) as FieldNode
|
|
404
422
|
|
|
405
423
|
fields.push(field)
|
|
406
424
|
defaultValue.push(field.defaultValue)
|
|
@@ -432,7 +450,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
432
450
|
|
|
433
451
|
// For optional, the inner field keeps the same name path
|
|
434
452
|
// because the value replaces null directly (not nested)
|
|
435
|
-
const innerField = ty.accept(this, label) as
|
|
453
|
+
const innerField = ty.accept(this, label) as FieldNode
|
|
436
454
|
|
|
437
455
|
const schema = z.union([
|
|
438
456
|
innerField.schema,
|
|
@@ -478,7 +496,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
478
496
|
// Item field uses [0] as template path
|
|
479
497
|
const itemField = this.withName("[0]", () =>
|
|
480
498
|
ty.accept(this, `${label}_item`)
|
|
481
|
-
) as
|
|
499
|
+
) as FieldNode
|
|
482
500
|
|
|
483
501
|
if (isBlob) {
|
|
484
502
|
const schema = z.union([
|
|
@@ -517,7 +535,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
517
535
|
const createItemField = (
|
|
518
536
|
index: number,
|
|
519
537
|
overrides?: { label?: string }
|
|
520
|
-
):
|
|
538
|
+
): FieldNode => {
|
|
521
539
|
// Replace [0] in template with actual index
|
|
522
540
|
const itemName = name ? `${name}[${index}]` : `[${index}]`
|
|
523
541
|
const itemLabel = overrides?.label ?? `Item ${index}`
|
|
@@ -559,13 +577,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
559
577
|
if (this.recursiveSchemas.has(typeName)) {
|
|
560
578
|
schema = this.recursiveSchemas.get(typeName)!
|
|
561
579
|
} else {
|
|
562
|
-
schema = z.lazy(() => (ty.accept(this, label) as
|
|
580
|
+
schema = z.lazy(() => (ty.accept(this, label) as FieldNode).schema)
|
|
563
581
|
this.recursiveSchemas.set(typeName, schema)
|
|
564
582
|
}
|
|
565
583
|
|
|
566
584
|
// Lazy extraction to prevent infinite loops
|
|
567
|
-
const extract = ():
|
|
568
|
-
this.withName(name, () => ty.accept(this, label)) as
|
|
585
|
+
const extract = (): FieldNode =>
|
|
586
|
+
this.withName(name, () => ty.accept(this, label)) as FieldNode
|
|
569
587
|
|
|
570
588
|
// Helper to get inner default (evaluates lazily)
|
|
571
589
|
const getInnerDefault = (): unknown => extract().defaultValue
|
|
@@ -628,6 +646,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
628
646
|
defaultValue: "",
|
|
629
647
|
maxLength: 64,
|
|
630
648
|
minLength: 7,
|
|
649
|
+
format: checkTextFormat(label) as TextFormat,
|
|
631
650
|
schema,
|
|
632
651
|
inputProps,
|
|
633
652
|
candidType: "principal",
|
|
@@ -635,11 +654,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
635
654
|
}
|
|
636
655
|
|
|
637
656
|
public visitText(_t: IDL.TextClass, label: string): TextField {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
657
|
+
const format = checkTextFormat(label) as TextFormat
|
|
658
|
+
|
|
659
|
+
// Generate format-specific inputProps
|
|
660
|
+
const inputProps = this.getTextInputProps(format)
|
|
661
|
+
|
|
662
|
+
// Generate format-specific schema
|
|
663
|
+
const schema = this.getTextSchema(format)
|
|
643
664
|
|
|
644
665
|
return {
|
|
645
666
|
type: "text",
|
|
@@ -649,12 +670,109 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
649
670
|
component: "text-input",
|
|
650
671
|
renderHint: TEXT_RENDER_HINT,
|
|
651
672
|
defaultValue: "",
|
|
652
|
-
|
|
673
|
+
format,
|
|
674
|
+
schema,
|
|
653
675
|
inputProps,
|
|
654
676
|
candidType: "text",
|
|
655
677
|
}
|
|
656
678
|
}
|
|
657
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Generate format-specific input props for text fields.
|
|
682
|
+
*/
|
|
683
|
+
private getTextInputProps(format: TextFormat): PrimitiveInputProps {
|
|
684
|
+
switch (format) {
|
|
685
|
+
case "email":
|
|
686
|
+
return {
|
|
687
|
+
type: "email",
|
|
688
|
+
placeholder: "email@example.com",
|
|
689
|
+
inputMode: "email",
|
|
690
|
+
autoComplete: "email",
|
|
691
|
+
spellCheck: false,
|
|
692
|
+
}
|
|
693
|
+
case "url":
|
|
694
|
+
return {
|
|
695
|
+
type: "url",
|
|
696
|
+
placeholder: "https://example.com",
|
|
697
|
+
inputMode: "url",
|
|
698
|
+
autoComplete: "url",
|
|
699
|
+
spellCheck: false,
|
|
700
|
+
}
|
|
701
|
+
case "phone":
|
|
702
|
+
return {
|
|
703
|
+
type: "tel",
|
|
704
|
+
placeholder: "+1 (555) 123-4567",
|
|
705
|
+
inputMode: "tel",
|
|
706
|
+
autoComplete: "tel",
|
|
707
|
+
spellCheck: false,
|
|
708
|
+
}
|
|
709
|
+
case "uuid":
|
|
710
|
+
return {
|
|
711
|
+
type: "text",
|
|
712
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
713
|
+
pattern:
|
|
714
|
+
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
715
|
+
spellCheck: false,
|
|
716
|
+
autoComplete: "off",
|
|
717
|
+
}
|
|
718
|
+
case "btc":
|
|
719
|
+
return {
|
|
720
|
+
type: "text",
|
|
721
|
+
placeholder: "bc1... or 1... or 3...",
|
|
722
|
+
spellCheck: false,
|
|
723
|
+
autoComplete: "off",
|
|
724
|
+
}
|
|
725
|
+
case "eth":
|
|
726
|
+
return {
|
|
727
|
+
type: "text",
|
|
728
|
+
placeholder: "0x...",
|
|
729
|
+
pattern: "0x[0-9a-fA-F]{40}",
|
|
730
|
+
spellCheck: false,
|
|
731
|
+
autoComplete: "off",
|
|
732
|
+
}
|
|
733
|
+
case "account-id":
|
|
734
|
+
return {
|
|
735
|
+
type: "text",
|
|
736
|
+
placeholder: "64-character hex string",
|
|
737
|
+
pattern: "[0-9a-fA-F]{64}",
|
|
738
|
+
maxLength: 64,
|
|
739
|
+
spellCheck: false,
|
|
740
|
+
autoComplete: "off",
|
|
741
|
+
}
|
|
742
|
+
case "principal":
|
|
743
|
+
return {
|
|
744
|
+
type: "text",
|
|
745
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
746
|
+
minLength: 7,
|
|
747
|
+
maxLength: 64,
|
|
748
|
+
spellCheck: false,
|
|
749
|
+
autoComplete: "off",
|
|
750
|
+
}
|
|
751
|
+
default:
|
|
752
|
+
return {
|
|
753
|
+
type: "text",
|
|
754
|
+
placeholder: "Enter text...",
|
|
755
|
+
spellCheck: true,
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Generate format-specific zod schema for text fields.
|
|
762
|
+
*/
|
|
763
|
+
private getTextSchema(format: TextFormat): z.ZodTypeAny {
|
|
764
|
+
switch (format) {
|
|
765
|
+
case "email":
|
|
766
|
+
return z.email("Invalid email address")
|
|
767
|
+
case "url":
|
|
768
|
+
return z.url("Invalid URL")
|
|
769
|
+
case "uuid":
|
|
770
|
+
return z.uuid("Invalid UUID")
|
|
771
|
+
default:
|
|
772
|
+
return z.string().min(1, "Required")
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
658
776
|
public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
|
|
659
777
|
const inputProps: PrimitiveInputProps = {
|
|
660
778
|
type: "checkbox",
|
|
@@ -706,6 +824,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
706
824
|
max?: string
|
|
707
825
|
}
|
|
708
826
|
): NumberField | TextField {
|
|
827
|
+
const format = checkNumberFormat(label) as NumberFormat
|
|
828
|
+
|
|
709
829
|
let schema = z.string().min(1, "Required")
|
|
710
830
|
|
|
711
831
|
if (options.isFloat) {
|
|
@@ -722,6 +842,11 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
722
842
|
const type = isBigInt ? "text" : "number"
|
|
723
843
|
|
|
724
844
|
if (type === "text") {
|
|
845
|
+
// Propagate timestamp/cycle format if detected, otherwise default to plain
|
|
846
|
+
let textFormat: TextFormat = "plain"
|
|
847
|
+
if (format === "timestamp") textFormat = "timestamp"
|
|
848
|
+
if (format === "cycle") textFormat = "cycle"
|
|
849
|
+
|
|
725
850
|
const inputProps: PrimitiveInputProps = {
|
|
726
851
|
type: "text",
|
|
727
852
|
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
@@ -739,6 +864,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
739
864
|
component: "text-input",
|
|
740
865
|
renderHint: TEXT_RENDER_HINT,
|
|
741
866
|
defaultValue: "",
|
|
867
|
+
format: textFormat,
|
|
742
868
|
candidType,
|
|
743
869
|
schema,
|
|
744
870
|
inputProps,
|
|
@@ -763,7 +889,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
763
889
|
renderHint: NUMBER_RENDER_HINT,
|
|
764
890
|
defaultValue: "",
|
|
765
891
|
candidType,
|
|
766
|
-
|
|
892
|
+
format,
|
|
893
|
+
schema,
|
|
767
894
|
inputProps,
|
|
768
895
|
...options,
|
|
769
896
|
}
|
|
@@ -838,6 +965,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
838
965
|
isPrimitive: false,
|
|
839
966
|
},
|
|
840
967
|
defaultValue: undefined,
|
|
968
|
+
candidType: "unknown",
|
|
841
969
|
schema: z.any(),
|
|
842
970
|
}
|
|
843
971
|
}
|
|
@@ -62,6 +62,108 @@ describe("FieldVisitor Schema Generation", () => {
|
|
|
62
62
|
})
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// Format Detection & Validation
|
|
67
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
describe("Format Detection & Validation", () => {
|
|
70
|
+
describe("Text Formats", () => {
|
|
71
|
+
it("should detect email format", () => {
|
|
72
|
+
const field = visitor.visitText(IDL.Text, "user_email")
|
|
73
|
+
|
|
74
|
+
expect(field.format).toBe("email")
|
|
75
|
+
expect(field.inputProps).toMatchObject({
|
|
76
|
+
type: "email",
|
|
77
|
+
inputMode: "email",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Valid email
|
|
81
|
+
expect(field.schema.parse("test@example.com")).toBe("test@example.com")
|
|
82
|
+
// Invalid email
|
|
83
|
+
expect(() => field.schema.parse("invalid-email")).toThrow()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("should detect url format", () => {
|
|
87
|
+
const field = visitor.visitText(IDL.Text, "website_link")
|
|
88
|
+
|
|
89
|
+
expect(field.format).toBe("url")
|
|
90
|
+
expect(field.inputProps).toMatchObject({
|
|
91
|
+
type: "url",
|
|
92
|
+
inputMode: "url",
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Valid URL
|
|
96
|
+
expect(field.schema.parse("https://example.com")).toBe(
|
|
97
|
+
"https://example.com"
|
|
98
|
+
)
|
|
99
|
+
// Invalid URL
|
|
100
|
+
expect(() => field.schema.parse("not-a-url")).toThrow()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should detect uuid format", () => {
|
|
104
|
+
const field = visitor.visitText(IDL.Text, "transaction_uuid")
|
|
105
|
+
|
|
106
|
+
expect(field.format).toBe("uuid")
|
|
107
|
+
|
|
108
|
+
const validUuid = "123e4567-e89b-12d3-a456-426614174000"
|
|
109
|
+
expect(field.schema.parse(validUuid)).toBe(validUuid)
|
|
110
|
+
|
|
111
|
+
expect(() => field.schema.parse("invalid-uuid")).toThrow()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should detect ethereum address format", () => {
|
|
115
|
+
const field = visitor.visitText(IDL.Text, "eth_address")
|
|
116
|
+
|
|
117
|
+
expect(field.format).toBe("eth")
|
|
118
|
+
expect(field.inputProps.pattern).toContain("0x")
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("should fallback to plain text for unknown formats", () => {
|
|
122
|
+
const field = visitor.visitText(IDL.Text, "some_random_field")
|
|
123
|
+
|
|
124
|
+
expect(field.format).toBe("plain")
|
|
125
|
+
expect(field.inputProps).toMatchObject({
|
|
126
|
+
type: "text",
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("Number Formats", () => {
|
|
132
|
+
it("should detect timestamp format", () => {
|
|
133
|
+
const field = visitor.visitInt(IDL.Int, "created_at")
|
|
134
|
+
|
|
135
|
+
expect(field.format).toBe("timestamp")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("should detect cycle format", () => {
|
|
139
|
+
const field = visitor.visitNat(IDL.Nat, "cycles_balance")
|
|
140
|
+
|
|
141
|
+
expect(field.format).toBe("cycle")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("should fallback to normal format", () => {
|
|
145
|
+
const field = visitor.visitNat(IDL.Nat, "quantity")
|
|
146
|
+
|
|
147
|
+
expect(field.format).toBe("plain")
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe("Principal Format", () => {
|
|
152
|
+
it("should detect principal format and set properties", () => {
|
|
153
|
+
const field = visitor.visitPrincipal(
|
|
154
|
+
IDL.Principal,
|
|
155
|
+
"controller_principal"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(field.format).toBe("principal")
|
|
159
|
+
expect(field.inputProps).toMatchObject({
|
|
160
|
+
minLength: 7,
|
|
161
|
+
maxLength: 64,
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
65
167
|
// ════════════════════════════════════════════════════════════════════════
|
|
66
168
|
// Compound Types
|
|
67
169
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -106,10 +208,16 @@ describe("FieldVisitor Schema Generation", () => {
|
|
|
106
208
|
)
|
|
107
209
|
const schema = field.schema as z.ZodUnion<any>
|
|
108
210
|
|
|
109
|
-
expect(schema.parse({ Ok: "Success" })).toEqual({
|
|
110
|
-
|
|
211
|
+
expect(schema.parse({ _type: "Ok", Ok: "Success" })).toEqual({
|
|
212
|
+
_type: "Ok",
|
|
213
|
+
Ok: "Success",
|
|
214
|
+
})
|
|
215
|
+
expect(schema.parse({ _type: "Err", Err: "Error" })).toEqual({
|
|
216
|
+
_type: "Err",
|
|
217
|
+
Err: "Error",
|
|
218
|
+
})
|
|
111
219
|
|
|
112
|
-
expect(() => schema.parse({ Other: "value" })).toThrow()
|
|
220
|
+
expect(() => schema.parse({ _type: "Other", Other: "value" })).toThrow()
|
|
113
221
|
})
|
|
114
222
|
})
|
|
115
223
|
|
|
@@ -179,12 +287,14 @@ describe("FieldVisitor Schema Generation", () => {
|
|
|
179
287
|
const schema = field.schema
|
|
180
288
|
|
|
181
289
|
const validList = {
|
|
290
|
+
_type: "Cons",
|
|
182
291
|
Cons: {
|
|
183
292
|
head: "1",
|
|
184
293
|
tail: {
|
|
294
|
+
_type: "Cons",
|
|
185
295
|
Cons: {
|
|
186
296
|
head: "2",
|
|
187
|
-
tail: {
|
|
297
|
+
tail: { _type: "Nil" },
|
|
188
298
|
},
|
|
189
299
|
},
|
|
190
300
|
},
|