| 1 | import { Effect, Option, Schema, SchemaGetter, SchemaTransformation } from "effect" |
| 2 | import { invalid } from "effect/schema/SchemaCompiler" |
| 3 | import { constructionSchemas } from "./construction.ts" |
| 4 | |
| 5 | export const key = Symbol("key") |
| 6 | export const token = Symbol("token") |
| 7 | export const events: Array<string> = [] |
| 8 | |
| 9 | const 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 | |
| 22 | const 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 | |
| 35 | const pureTransform = (decode: (input: string) => number) => |
| 36 | Schema.String.pipe( |
| 37 | Schema.decodeTo( |
| 38 | Schema.Number, |
| 39 | SchemaTransformation.transform({ decode, encode: String }) |
| 40 | ) |
| 41 | ) |
| 42 | const sharedPureTransform = pureTransform(Number) |
| 43 | const repeatedUnionMember = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.Number }) |
| 44 | |
| 45 | const 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 | |
| 55 | const 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 | |
| 65 | interface Fixture { |
| 66 | readonly schema: Schema.ConstraintDecoder<unknown> |
| 67 | readonly inputs: ReadonlyArray<unknown> |
| 68 | } |
| 69 | |
| 70 | export 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 | |
| 217 | export 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 | |
| 232 | export let suspendEvaluations = 0 |
| 233 | export const lazy = Schema.suspend(() => { |
| 234 | suspendEvaluations++ |
| 235 | return Schema.Struct({ value: Schema.String }) |
| 236 | }) |
| 237 | |
| 238 | export const proof = Schema.Struct({ value: Schema.String }) |
| 239 | |
| 240 | export 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 | |
| 249 | export const roots = [...Object.values(schemas).map((schema) => schema.ast), proof.ast] |