1import * as Effect from "effect/Effect"
2import * as Schema from "effect/Schema"
3import * as SchemaAST from "effect/SchemaAST"
4import * as SchemaParser from "effect/SchemaParser"
5import * as SchemaTransformation from "effect/SchemaTransformation"
6import assert from "node:assert/strict"
7
8export const person = () => Schema.Struct({ name: Schema.String, age: Schema.Number, active: Schema.Boolean })
9export const input = { name: "Ada", age: 37, active: true }
10const small = person()
11const nested = Schema.Struct({ user: person(), tags: Schema.Array(Schema.String) })
12const array = Schema.Array(person())
13const arrayInput = Array.from({ length: 32 }, () => ({ ...input }))
14const tuple = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])
15const tupleInput = ["head", ...Array.from({ length: 32 }, (_, i) => i), true]
16const record = Schema.Record(Schema.String, person())
17const recordInput = Object.fromEntries(arrayInput.map((value, i) => [String(i), value]))
18const transformedKeyRecord = Schema.Record(
19 Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())),
20 Schema.String
21)
22const transformedKeyRecordInput = Object.fromEntries(
23 Array.from({ length: 32 }, (_, index) => [`field_${index}_value`, `value${index}`])
24)
25const transformedKeyRecordOutput = Object.fromEntries(
26 Array.from({ length: 32 }, (_, index) => [`field${index}Value`, `value${index}`])
27)
28const union = Schema.Union(
29 Array.from({ length: 8 }, (_, i) => Schema.Struct({ tag: Schema.Literal(i), value: Schema.Number }))
30)
31const oneOf = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })], {
32 mode: "oneOf"
33})
34const fields = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, Schema.NumberFromString]))
35const transformed = Schema.Struct(fields)
36const transformedInput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, String(i)]))
37const transformedOutput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, i]))
38const checkedTransform = Schema.String.pipe(
39 Schema.decodeTo(
40 Schema.Number.check(Schema.isGreaterThan(0)),
41 SchemaTransformation.transform({ decode: Number, encode: String })
42 )
43)
44const rootTransform = Schema.FiniteFromString
45const middleware = small.pipe(Schema.middlewareDecoding((effect) => effect))
46const declaration = Schema.ReadonlySet(person())
47const declarationInput = new Set(arrayInput)
48interface Node {
49 readonly value: number
50 readonly children: ReadonlyArray<Node>
51}
52const recursive: Schema.Codec<Node> = Schema.Struct({
53 value: Schema.Number,
54 children: Schema.Array(Schema.suspend(() => recursive))
55})
56const node = (depth: number): Node => ({
57 value: depth,
58 children: depth === 0 ? [] : [node(depth - 1), node(depth - 1)]
59})
60const recursiveInput = node(4)
61const suspendRoot = Schema.suspend(() => Schema.Struct({ value: Schema.Number }))
62const defaults = Schema.Struct(
63 Object.fromEntries(
64 Array.from(
65 { length: 32 },
66 (_, i) => [`v${i}`, Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(i)))]
67 )
68 )
69)
70
71type Case = {
72 schema: Schema.Constraint
73 input: unknown
74 expected: unknown
75 operation?: "is" | "make" | "encode"
76 invalid?: boolean
77 options?: SchemaAST.ParseOptions
78}
79
80export const cases: Record<string, Case> = {
81 parseValid: { schema: small, input, expected: input },
82 parseExtra: { schema: small, input: { ...input, extra: 1 }, expected: input },
83 parseInvalid: { schema: small, input: { ...input, age: "bad" }, expected: true, invalid: true },
84 isValid: { schema: small, input, expected: true, operation: "is" },
85 isInvalid: { schema: small, input: { ...input, age: "bad" }, expected: false, operation: "is" },
86 encode: { schema: small, input, expected: input, operation: "encode" },
87 strict: { schema: small, input, expected: input, options: { onExcessProperty: "error" } },
88 nested: { schema: nested, input: { user: input, tags: ["a", "b"] }, expected: { user: input, tags: ["a", "b"] } },
89 array: { schema: array, input: arrayInput, expected: arrayInput },
90 arrayInvalid: { schema: array, input: [...arrayInput, { ...input, age: "bad" }], expected: true, invalid: true },
91 tuple: { schema: tuple, input: tupleInput, expected: tupleInput },
92 record: { schema: record, input: recordInput, expected: recordInput },
93 recordTransformedKeys: {
94 schema: transformedKeyRecord,
95 input: transformedKeyRecordInput,
96 expected: transformedKeyRecordOutput
97 },
98 union: { schema: union, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 } },
99 oneOf: { schema: oneOf, input: { b: 1 }, expected: { b: 1 } },
100 transform: { schema: transformed, input: transformedInput, expected: transformedOutput },
101 transformInvalid: { schema: checkedTransform, input: "-1", expected: true, invalid: true },
102 rootTransform: { schema: rootTransform, input: "123", expected: 123 },
103 middleware: { schema: middleware, input, expected: input },
104 recursive: { schema: recursive, input: recursiveInput, expected: recursiveInput },
105 suspendRoot: { schema: suspendRoot, input: { value: 1 }, expected: { value: 1 } },
106 declaration: { schema: declaration, input: declarationInput, expected: declarationInput },
107 makeStruct: { schema: defaults, input: {}, expected: transformedOutput, operation: "make" },
108 makeArray: { schema: array, input: arrayInput, expected: arrayInput, operation: "make" },
109 makeUnion: { schema: union, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 }, operation: "make" }
110}
111
112export const targets = Object.values(cases).map(({ operation, schema }) =>
113 operation === "encode"
114 ? { ast: SchemaAST.flip(schema.ast), operations: ["decode"] as const }
115 : operation === "is" || operation === "make"
116 ? { ast: SchemaAST.toType(schema.ast), operations: [operation] as const }
117 : { ast: schema.ast, operations: ["decode"] as const }
118)
119export const roots = targets.map((target) => target.ast)
120
121export const fixture = (name: string) => {
122 const { schema, input, expected, operation, invalid, options } = cases[name]
123 const parse = operation === "is" ?
124 SchemaParser.is(schema)
125 : operation === "make" ?
126 SchemaParser.make(schema)
127 : operation === "encode" ?
128 SchemaParser.encodeUnknownSync(schema as Schema.ConstraintEncoder<unknown>)
129 : SchemaParser.decodeUnknownSync(schema as Schema.ConstraintDecoder<unknown>, options)
130 return {
131 run: invalid ?
132 () => {
133 try {
134 parse(input)
135 return false
136 } catch {
137 return true
138 }
139 } :
140 () => parse(input),
141 validate: (result: unknown) => assert.deepEqual(result, expected)
142 }
143}