1import { Effect, Option, Schema, SchemaGetter, SchemaTransformation } from "effect"
2import { invalid } from "effect/schema/SchemaCompiler"
3import { constructionSchemas } from "./construction.ts"
4
5export const key = Symbol("key")
6export const token = Symbol("token")
7export const events: Array<string> = []
8
9const transformed = Schema.String.pipe(
10 Schema.decodeTo(
11 Schema.Number.check(Schema.isGreaterThan(0)),
12 SchemaTransformation.transform({
13 decode: (input) => {
14 events.push("transform")
15 return Number(input)
16 },
17 encode: String
18 })
19 )
20)
21
22const pureTransformed = Schema.String.pipe(
23 Schema.decodeTo(
24 Schema.Number,
25 SchemaTransformation.transform({
26 decode: (input) => {
27 events.push("pure transform")
28 return input === "invalid" ? "invalid" as any : Number(input)
29 },
30 encode: String
31 })
32 )
33)
34
35const pureTransform = (decode: (input: string) => number) =>
36 Schema.String.pipe(
37 Schema.decodeTo(
38 Schema.Number,
39 SchemaTransformation.transform({ decode, encode: String })
40 )
41 )
42const sharedPureTransform = pureTransform(Number)
43const repeatedUnionMember = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.Number })
44
45const middleware = transformed.pipe(
46 Schema.middlewareDecoding((effect) => {
47 events.push("middleware")
48 return Effect.catchEager(effect, () => {
49 events.push("recover")
50 return Effect.succeed(Option.some(1))
51 })
52 })
53)
54
55const asynchronous = Schema.String.pipe(
56 Schema.decodeTo(Schema.Number.check(Schema.isGreaterThan(0)), {
57 decode: SchemaGetter.transformOptionalEffect((input) => {
58 events.push("async")
59 return Effect.yieldNow.pipe(Effect.as(Option.map(input, Number)))
60 }),
61 encode: SchemaGetter.transform(String)
62 })
63)
64
65interface Fixture {
66 readonly schema: Schema.ConstraintDecoder<unknown>
67 readonly inputs: ReadonlyArray<unknown>
68}
69
70export const synchronous = {
71 struct: {
72 schema: Schema.Struct({
73 name: Schema.String,
74 nested: Schema.Struct({ count: Schema.Number.check(Schema.isGreaterThan(0)) }),
75 optional: Schema.optionalKey(Schema.String)
76 }).check(Schema.makeFilter((input) => Object.keys(input).length <= 3)),
77 inputs: [
78 { name: "a", nested: { count: 1, ignored: true }, extra: true },
79 { name: "a", nested: { count: -1 } },
80 { name: 1, nested: { count: "invalid" } },
81 { name: "a" }
82 ]
83 },
84 array: {
85 schema: Schema.Array(Schema.Struct({ value: Schema.String })),
86 inputs: [[{ value: "a", extra: true }], [{ value: 1 }, {}], null]
87 },
88 tuple: {
89 schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]),
90 inputs: [["a", 1, 2, true], ["a", true], ["a", "invalid", false], ["a"]]
91 },
92 tagged: {
93 schema: Schema.Union([
94 Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }),
95 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.Number })
96 ]),
97 inputs: [{ kind: "b", value: 1 }, { kind: "b", value: "invalid" }, { kind: "c" }]
98 },
99 literals: {
100 schema: Schema.Literals(["a", "b", 0, 1, 2, 3, 4, 5, 6, 7, 8]),
101 inputs: ["a", -0, 8, 9, null]
102 },
103 oneOf: {
104 schema: Schema.Union([Schema.String, Schema.Literal("a")], { mode: "oneOf" }),
105 inputs: ["b", "a", false]
106 },
107 repeatedOneOf: {
108 schema: Schema.Union([
109 repeatedUnionMember,
110 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.String }),
111 repeatedUnionMember,
112 Schema.Never
113 ], { mode: "oneOf" }),
114 inputs: [{ kind: "a", value: 1 }, { kind: "b", value: "value" }, { kind: "c" }]
115 },
116 sentinel: {
117 schema: Schema.Union([Schema.Symbol, Schema.String]),
118 inputs: [invalid]
119 },
120 sentinelLookup: {
121 schema: Schema.Union([Schema.UniqueSymbol(invalid), Schema.Literal("valid")]),
122 inputs: [invalid]
123 },
124 checkedSentinel: {
125 schema: Schema.Struct({ value: Schema.Union([Schema.Symbol, Schema.String]) }).check(
126 Schema.makeFilter(() => true)
127 ),
128 inputs: [{ value: invalid }]
129 },
130 symbols: {
131 schema: Schema.Struct({ [key]: Schema.UniqueSymbol(token) }),
132 inputs: [{ [key]: token }, { [key]: key }, {}]
133 },
134 enumeration: {
135 schema: Schema.Enum({ a: "a", b: "b", c: 0, d: 1, e: 2, f: 3, g: 4, h: 5, i: 6 }),
136 inputs: ["a", -0, 6, "invalid"]
137 },
138 record: {
139 schema: Schema.Record(Schema.String, Schema.Struct({ count: Schema.Number })),
140 inputs: [{ a: { count: 1 } }, { a: { count: "invalid" }, b: {} }, {}]
141 },
142 mixedRecord: {
143 schema: Schema.StructWithRest(
144 Schema.Struct({ fixed: Schema.Number }),
145 [
146 Schema.Record(Schema.TemplateLiteral(["data-", Schema.String]), Schema.Number),
147 Schema.Record(Schema.Symbol, Schema.Number)
148 ]
149 ),
150 inputs: [
151 { fixed: 1, "data-a": 2, [key]: 3 },
152 { fixed: 1, ignored: true },
153 { fixed: 1, [key]: "invalid" }
154 ]
155 },
156 numericRecord: {
157 schema: Schema.Record(Schema.Union([Schema.Literal(1), Schema.Symbol]), Schema.String),
158 inputs: [{ 1: "one", [key]: "symbol" }, { 1: "one", extra: true }, { 1: 1 }]
159 },
160 templateLiteral: {
161 schema: Schema.TemplateLiteral(["count:", Schema.Int.check(Schema.isGreaterThan(0))]),
162 inputs: ["count:1", "count:0", "count:1.5", "invalid", null]
163 },
164 templateLiteralParser: {
165 schema: Schema.TemplateLiteralParser(["bit:", Schema.BooleanFromBit]),
166 inputs: ["bit:1", "bit:0", "bit:true", null]
167 },
168 transformed: { schema: transformed, inputs: ["2", "-1", false] },
169 checkedTransformedStruct: {
170 schema: Schema.Struct({ value: transformed }).check(Schema.makeFilter((output) => {
171 events.push("struct check")
172 return output.value < 10 && Object.keys(output).length === 1
173 })),
174 inputs: [{ value: "2", extra: true }, { value: "12" }, { value: "-1" }, {}]
175 },
176 encodingCheckedTransformedStruct: {
177 schema: Schema.Struct({ value: transformed }).pipe(
178 Schema.flip,
179 Schema.check(Schema.makeFilter((input) => {
180 events.push("encoding check")
181 return input.value !== "02"
182 })),
183 Schema.flip
184 ),
185 inputs: [{ value: "2" }, { value: "02" }, { value: "-1" }]
186 },
187 transformedStruct: {
188 schema: Schema.Struct({ before: Schema.String, value: transformed, after: Schema.Boolean }),
189 inputs: [
190 { before: "a", value: "2", after: true },
191 { before: "a", value: "-1", after: true },
192 { before: "a", value: "2", after: "invalid" }
193 ]
194 },
195 pureTransformedStruct: {
196 schema: Schema.Struct({ value: pureTransformed }),
197 inputs: [{ value: "2" }, { value: false }, { value: "invalid" }]
198 },
199 pureTransformedFields: {
200 schema: Schema.Struct({
201 first: sharedPureTransform,
202 second: sharedPureTransform,
203 incremented: pureTransform((input) => Number(input) + 1),
204 doubled: pureTransform((input) => Number(input) * 2)
205 }),
206 inputs: [
207 { first: "1", second: "2", incremented: "3", doubled: "4" },
208 { first: false, second: "2", incremented: "3", doubled: "4" }
209 ]
210 },
211 middleware: {
212 schema: middleware,
213 inputs: ["2", "-1", false]
214 }
215} satisfies Record<string, Fixture>
216
217export const asyncFixture = {
218 schema: Schema.Struct({ before: Schema.String, value: asynchronous, after: Schema.Boolean }).check(
219 Schema.makeFilter((output) => {
220 events.push("async struct check")
221 return output.value < 10
222 })
223 ),
224 inputs: [
225 { before: "a", value: "2", after: true },
226 { before: "a", value: "12", after: true },
227 { before: "a", value: "-1", after: true },
228 { before: "a", value: "2", after: "invalid" }
229 ]
230}
231
232export let suspendEvaluations = 0
233export const lazy = Schema.suspend(() => {
234 suspendEvaluations++
235 return Schema.Struct({ value: Schema.String })
236})
237
238export const proof = Schema.Struct({ value: Schema.String })
239
240export const schemas: Readonly<Record<string, Schema.ConstraintDecoder<unknown>>> = {
241 ...constructionSchemas,
242 ...Object.fromEntries(Object.entries(synchronous).map(([name, fixture]) => [name, fixture.schema])),
243 asynchronous: asyncFixture.schema,
244 lazy,
245 proofArray: Schema.Array(proof),
246 proof
247}
248
249export const roots = [...Object.values(schemas).map((schema) => schema.ast), proof.ast]