1import { assert, describe, it } from "@effect/vitest"
2import { Context, Effect, Schema, SchemaAST, SchemaGetter, SchemaTransformation } from "effect"
3
4describe("TemplateLiteral", () => {
5 it("does not expand shared subtemplates during construction", () => {
6 let visits = 0
7 let part: SchemaAST.AST = new Proxy(new SchemaAST.String(), {
8 get(target, property, receiver) {
9 if (property === "encoding") visits++
10 return Reflect.get(target, property, receiver)
11 }
12 })
13 const depth = 20
14 for (let level = 0; level < depth; level++) {
15 part = new SchemaAST.TemplateLiteral([part, part])
16 }
17 assert.isAtMost(visits, depth)
18 })
19
20 it("rejects codecs at construction, including transformations between equal types", () => {
21 for (
22 const part of [
23 Schema.BooleanFromBit,
24 Schema.FiniteFromString,
25 Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel()))
26 ]
27 ) {
28 assert.throws(
29 () => Schema.TemplateLiteral([part]),
30 "TemplateLiteral parts cannot have an encoding at parts[0]"
31 )
32 assert.throws(
33 () => new SchemaAST.TemplateLiteral([part.ast]),
34 "TemplateLiteral parts cannot have an encoding at parts[0]"
35 )
36 }
37 })
38
39 it("rejects codecs inside nested unions", () => {
40 const part = Schema.Union([
41 Schema.String,
42 Schema.Union([Schema.Number, Schema.BooleanFromBit])
43 ])
44 assert.throws(
45 () => Schema.TemplateLiteral(["prefix", part]),
46 "TemplateLiteral parts cannot have an encoding at parts[1].types[1].types[1]"
47 )
48 })
49
50 it("rejects a transformed nested template", () => {
51 const part = Schema.TemplateLiteral([Schema.String]).pipe(
52 Schema.decode(SchemaTransformation.snakeToCamel())
53 )
54 assert.throws(
55 () => Schema.TemplateLiteral([part]),
56 "TemplateLiteral parts cannot have an encoding at parts[0]"
57 )
58 })
59
60 it.effect("supports branded parts, checks, and explicit projections", () =>
61 Effect.gen(function*() {
62 const schema = Schema.TemplateLiteral([
63 Schema.NonEmptyString.pipe(Schema.brand("Prefix")),
64 ":",
65 Schema.Int.check(Schema.isGreaterThan(0))
66 ])
67 assert.strictEqual(yield* Schema.decodeEffect(schema)("a:1"), "a:1")
68 assert.isFalse(Schema.is(schema)(":1"))
69 assert.isFalse(Schema.is(schema)("a:0"))
70
71 const bits = Schema.TemplateLiteral([Schema.toEncoded(Schema.BooleanFromBit)])
72 assert.isTrue(Schema.is(bits)("0"))
73 assert.isTrue(Schema.is(bits)("1"))
74 assert.isFalse(Schema.is(bits)("true"))
75 }))
76})
77
78describe("TemplateLiteralParser", () => {
79 it.effect("transforms a template into a tuple and encodes transformed parts", () =>
80 Effect.gen(function*() {
81 const schema = Schema.TemplateLiteralParser(["bit:", Schema.BooleanFromBit])
82 const encoded = Schema.toEncoded(schema)
83 assert.strictEqual(encoded.ast._tag, "TemplateLiteral")
84 assert.isTrue(Schema.is(encoded)("bit:1"))
85 assert.isFalse(Schema.is(encoded)("bit:true"))
86 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("bit:1"), ["bit:", true])
87 assert.strictEqual(yield* Schema.encodeEffect(schema)(["bit:", false]), "bit:0")
88 assert.strictEqual((yield* Effect.result(Schema.decodeUnknownEffect(schema)("bit:true")))._tag, "Failure")
89 }))
90
91 it.effect("preserves decoded checks and supports decode-only parts", () =>
92 Effect.gen(function*() {
93 const part = Schema.Literals([0, 1]).pipe(Schema.decodeTo(
94 Schema.Boolean.check(Schema.makeFilter((value) => value)),
95 {
96 decode: SchemaGetter.transform((value) => value === 1),
97 encode: SchemaGetter.forbiddenEncoding
98 }
99 ))
100 const schema = Schema.TemplateLiteralParser([part])
101 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("1"), [true])
102 assert.strictEqual((yield* Effect.result(Schema.decodeEffect(schema)("0")))._tag, "Failure")
103 assert.strictEqual((yield* Effect.result(Schema.encodeEffect(schema)([true])))._tag, "Failure")
104 }))
105
106 it.effect("preserves string coercion for transformed numeric parts", () =>
107 Effect.gen(function*() {
108 const schema = Schema.TemplateLiteralParser(["a", Schema.FiniteFromString])
109 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("a"), ["a", 0])
110 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("a1"), ["a", 1])
111 assert.strictEqual((yield* Effect.result(Schema.decodeEffect(schema)("ab")))._tag, "Failure")
112 }))
113
114 it.effect("enforces oneOf after decoding overlapping encoded members", () =>
115 Effect.gen(function*() {
116 const part = Schema.Union([
117 Schema.String.pipe(Schema.decodeTo(Schema.Literal("a"))),
118 Schema.String.pipe(Schema.decodeTo(Schema.Literal("b")))
119 ], { mode: "oneOf" })
120 const schema = Schema.TemplateLiteralParser([part])
121 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("a"), ["a"])
122 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("b"), ["b"])
123 assert.strictEqual((yield* Effect.result(Schema.decodeEffect(schema)("c")))._tag, "Failure")
124 assert.strictEqual(yield* Schema.encodeEffect(schema)(["a"]), "a")
125
126 const ambiguous = Schema.TemplateLiteralParser([
127 Schema.Union([Schema.String, Schema.Number], { mode: "oneOf" })
128 ])
129 assert.strictEqual((yield* Effect.result(Schema.decodeEffect(ambiguous)("1")))._tag, "Failure")
130 assert.strictEqual(yield* Schema.encodeEffect(ambiguous)(["1"]), "1")
131 }))
132
133 it.effect("preserves shared unions when normalizing encoded parts", () =>
134 Effect.gen(function*() {
135 let part: Schema.Codec<"a" | "b", string> = Schema.Union([
136 Schema.String.pipe(Schema.decodeTo(Schema.Literal("a"))),
137 Schema.String.pipe(Schema.decodeTo(Schema.Literal("b")))
138 ], { mode: "oneOf" })
139 const depth = 12
140 for (let level = 0; level < depth; level++) {
141 part = Schema.Union([part, part])
142 }
143 const schema = Schema.TemplateLiteralParser([part, ":", part])
144 const source = Schema.toEncoded(schema).ast
145 if (source._tag !== "TemplateLiteral") return assert.fail("Expected a TemplateLiteral source")
146 assert.isTrue(source.parts[0] === source.parts[2])
147 let current = source.parts[0]
148 for (let level = 0; level < depth; level++) {
149 if (current._tag !== "Union") return assert.fail("Expected a Union part")
150 assert.isTrue(current.types[0] === current.types[1])
151 current = current.types[0]
152 }
153 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)("a:b"), ["a", ":", "b"])
154 assert.strictEqual(yield* Schema.encodeEffect(schema)(["b", ":", "a"]), "b:a")
155 }))
156
157 it.effect("keeps greedy segmentation without requiring tuple round-trips", () =>
158 Effect.gen(function*() {
159 const schema = Schema.TemplateLiteralParser([Schema.String, ":", Schema.String])
160 const encoded = yield* Schema.encodeEffect(schema)(["a", ":", "b:c"])
161 assert.strictEqual(encoded, "a:b:c")
162 assert.deepStrictEqual(yield* Schema.decodeEffect(schema)(encoded), ["a:b", ":", "c"])
163 }))
164
165 it.effect("uses separate decoding and encoding services", () =>
166 Effect.gen(function*() {
167 class DecodeScale extends Context.Service<DecodeScale, number>()("TemplateLiteral/DecodeScale") {}
168 class EncodeScale extends Context.Service<EncodeScale, number>()("TemplateLiteral/EncodeScale") {}
169 const part = Schema.Number.pipe(Schema.decode({
170 decode: SchemaGetter.transformEffect((value) => Effect.map(DecodeScale, (scale) => value * scale)),
171 encode: SchemaGetter.transformEffect((value) => Effect.map(EncodeScale, (scale) => value / scale))
172 }))
173 const schema = Schema.TemplateLiteralParser(["value:", part])
174 assert.deepStrictEqual(
175 yield* Schema.decodeEffect(schema)("value:2").pipe(Effect.provideService(DecodeScale, 3)),
176 ["value:", 6]
177 )
178 assert.strictEqual(
179 yield* Schema.encodeEffect(schema)(["value:", 6]).pipe(Effect.provideService(EncodeScale, 2)),
180 "value:3"
181 )
182 }))
183})