| 1 | import * as Effect from "effect/Effect" |
| 2 | import * as Schema from "effect/Schema" |
| 3 | import * as SchemaAST from "effect/SchemaAST" |
| 4 | import * as SchemaParser from "effect/SchemaParser" |
| 5 | import * as SchemaTransformation from "effect/SchemaTransformation" |
| 6 | import assert from "node:assert/strict" |
| 7 | |
| 8 | export const person = () => Schema.Struct({ name: Schema.String, age: Schema.Number, active: Schema.Boolean }) |
| 9 | export const input = { name: "Ada", age: 37, active: true } |
| 10 | const small = person() |
| 11 | const nested = Schema.Struct({ user: person(), tags: Schema.Array(Schema.String) }) |
| 12 | const array = Schema.Array(person()) |
| 13 | const arrayInput = Array.from({ length: 32 }, () => ({ ...input })) |
| 14 | const tuple = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) |
| 15 | const tupleInput = ["head", ...Array.from({ length: 32 }, (_, i) => i), true] |
| 16 | const record = Schema.Record(Schema.String, person()) |
| 17 | const recordInput = Object.fromEntries(arrayInput.map((value, i) => [String(i), value])) |
| 18 | const transformedKeyRecord = Schema.Record( |
| 19 | Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())), |
| 20 | Schema.String |
| 21 | ) |
| 22 | const transformedKeyRecordInput = Object.fromEntries( |
| 23 | Array.from({ length: 32 }, (_, index) => [`field_${index}_value`, `value${index}`]) |
| 24 | ) |
| 25 | const transformedKeyRecordOutput = Object.fromEntries( |
| 26 | Array.from({ length: 32 }, (_, index) => [`field${index}Value`, `value${index}`]) |
| 27 | ) |
| 28 | const union = Schema.Union( |
| 29 | Array.from({ length: 8 }, (_, i) => Schema.Struct({ tag: Schema.Literal(i), value: Schema.Number })) |
| 30 | ) |
| 31 | const oneOf = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })], { |
| 32 | mode: "oneOf" |
| 33 | }) |
| 34 | const fields = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, Schema.NumberFromString])) |
| 35 | const transformed = Schema.Struct(fields) |
| 36 | const transformedInput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, String(i)])) |
| 37 | const transformedOutput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, i])) |
| 38 | const checkedTransform = Schema.String.pipe( |
| 39 | Schema.decodeTo( |
| 40 | Schema.Number.check(Schema.isGreaterThan(0)), |
| 41 | SchemaTransformation.transform({ decode: Number, encode: String }) |
| 42 | ) |
| 43 | ) |
| 44 | const rootTransform = Schema.FiniteFromString |
| 45 | const middleware = small.pipe(Schema.middlewareDecoding((effect) => effect)) |
| 46 | const declaration = Schema.ReadonlySet(person()) |
| 47 | const declarationInput = new Set(arrayInput) |
| 48 | interface Node { |
| 49 | readonly value: number |
| 50 | readonly children: ReadonlyArray<Node> |
| 51 | } |
| 52 | const recursive: Schema.Codec<Node> = Schema.Struct({ |
| 53 | value: Schema.Number, |
| 54 | children: Schema.Array(Schema.suspend(() => recursive)) |
| 55 | }) |
| 56 | const node = (depth: number): Node => ({ |
| 57 | value: depth, |
| 58 | children: depth === 0 ? [] : [node(depth - 1), node(depth - 1)] |
| 59 | }) |
| 60 | const recursiveInput = node(4) |
| 61 | const suspendRoot = Schema.suspend(() => Schema.Struct({ value: Schema.Number })) |
| 62 | const 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 | |
| 71 | type 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 | |
| 80 | export 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 | |
| 112 | export 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 | ) |
| 119 | export const roots = targets.map((target) => target.ast) |
| 120 | |
| 121 | export 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 | } |