1import { BigDecimal, DateTime, Duration, HashMap, Option, Redacted, Result, Schema, SchemaGetter } from "effect"
2import { describe, it } from "vitest"
3import { strictEqual } from "../utils/assert.ts"
4
5describe("toFormatter", () => {
6 it("Never", () => {
7 const format = Schema.toFormatter(Schema.Never)
8 strictEqual(format(1 as never), "never")
9 })
10
11 it("Any", () => {
12 const format = Schema.toFormatter(Schema.Any)
13 strictEqual(format(1), "1")
14 strictEqual(format("a"), `"a"`)
15 strictEqual(format(true), "true")
16 strictEqual(format(false), "false")
17 strictEqual(format(null), "null")
18 strictEqual(format(undefined), "undefined")
19 strictEqual(format({ a: 1 }), `{"a":1}`)
20 strictEqual(format([1, 2, 3]), `[1,2,3]`)
21 })
22
23 it("Unknown", () => {
24 const format = Schema.toFormatter(Schema.Unknown)
25 strictEqual(format(1), "1")
26 strictEqual(format("a"), `"a"`)
27 strictEqual(format(true), "true")
28 strictEqual(format(false), "false")
29 strictEqual(format(null), "null")
30 strictEqual(format(undefined), "undefined")
31 strictEqual(format({ a: 1 }), `{"a":1}`)
32 strictEqual(format([1, 2, 3]), `[1,2,3]`)
33 })
34
35 it("Void", () => {
36 const format = Schema.toFormatter(Schema.Void)
37 strictEqual(format(undefined), "void")
38 })
39
40 it("Null", () => {
41 const format = Schema.toFormatter(Schema.Null)
42 strictEqual(format(null), "null")
43 })
44
45 it("String", () => {
46 const format = Schema.toFormatter(Schema.String)
47 strictEqual(format("a"), `"a"`)
48 })
49
50 it("Number", () => {
51 const format = Schema.toFormatter(Schema.Number)
52 strictEqual(format(1), "1")
53 })
54
55 it("Boolean", () => {
56 const format = Schema.toFormatter(Schema.Boolean)
57 strictEqual(format(true), "true")
58 strictEqual(format(false), "false")
59 })
60
61 it("BigInt", () => {
62 const format = Schema.toFormatter(Schema.BigInt)
63 strictEqual(format(1n), "1n")
64 })
65
66 it("Symbol", () => {
67 const format = Schema.toFormatter(Schema.Symbol)
68 strictEqual(format(Symbol.for("a")), "Symbol(a)")
69 })
70
71 it("UniqueSymbol", () => {
72 const format = Schema.toFormatter(Schema.UniqueSymbol(Symbol.for("a")))
73 strictEqual(format(Symbol.for("a")), "Symbol(a)")
74 })
75
76 it("ObjectKeyword", () => {
77 const format = Schema.toFormatter(Schema.ObjectKeyword)
78 strictEqual(format({}), "{}")
79 strictEqual(format({ a: 1 }), `{"a":1}`)
80 strictEqual(format([1, 2, 3]), `[1,2,3]`)
81 })
82
83 describe("Literal", () => {
84 it("string", () => {
85 const format = Schema.toFormatter(Schema.Literal("a"))
86 strictEqual(format("a"), `"a"`)
87 })
88
89 it("number", () => {
90 const format = Schema.toFormatter(Schema.Literal(1))
91 strictEqual(format(1), "1")
92 })
93
94 it("boolean", () => {
95 const format = Schema.toFormatter(Schema.Literal(true))
96 strictEqual(format(true), "true")
97 })
98
99 it("bigint", () => {
100 const format = Schema.toFormatter(Schema.Literal(1n))
101 strictEqual(format(1n), "1n")
102 })
103 })
104
105 it("Literals", () => {
106 const format = Schema.toFormatter(Schema.Literals(["a", "b", "c"]))
107 strictEqual(format("a"), `"a"`)
108 strictEqual(format("b"), `"b"`)
109 strictEqual(format("c"), `"c"`)
110 })
111
112 it("TemplateLiteral", () => {
113 const format = Schema.toFormatter(Schema.TemplateLiteral([Schema.Literal("a"), Schema.String]))
114 strictEqual(format("a"), `"a"`)
115 strictEqual(format("ab"), `"ab"`)
116 })
117
118 describe("Enum", () => {
119 it("Numeric enum", () => {
120 enum Fruits {
121 Apple,
122 Banana
123 }
124 const format = Schema.toFormatter(Schema.Enum(Fruits))
125 strictEqual(format(Fruits.Apple), "0")
126 })
127
128 it("String enum", () => {
129 enum Fruits {
130 Apple = "apple",
131 Banana = "banana",
132 Cantaloupe = 0
133 }
134 const format = Schema.toFormatter(Schema.Enum(Fruits))
135 strictEqual(format(Fruits.Apple), `"apple"`)
136 })
137
138 it("Const enum", () => {
139 const Fruits = {
140 Apple: "apple",
141 Banana: "banana",
142 Cantaloupe: 3
143 } as const
144 const format = Schema.toFormatter(Schema.Enum(Fruits))
145 strictEqual(format(Fruits.Apple), `"apple"`)
146 })
147 })
148
149 it("Union", () => {
150 const format = Schema.toFormatter(Schema.Union([Schema.String, Schema.Number]))
151 strictEqual(format("a"), `"a"`)
152 strictEqual(format(1), "1")
153 })
154
155 it("precompiles Union members", () => {
156 let derivations = 0
157 const String = Schema.String.pipe(
158 Schema.overrideToFormatter(() => {
159 derivations++
160 return (s) => s.toUpperCase()
161 })
162 )
163 const format = Schema.toFormatter(Schema.Union([String, Schema.Number, Schema.Never]))
164
165 strictEqual(derivations, 1)
166 strictEqual(format("a"), "A")
167 strictEqual(format("b"), "B")
168 strictEqual(derivations, 1)
169 })
170
171 it("selects transformed Union members on the Type side", () => {
172 const Target = Schema.Struct({ value: Schema.String }).pipe(
173 Schema.overrideToFormatter(() => (a) => `formatted:${a.value}`)
174 )
175 const Transformed = Schema.String.pipe(
176 Schema.decodeTo(Target, {
177 decode: SchemaGetter.transform((s) => ({ value: s })),
178 encode: SchemaGetter.transform((a) => a.value)
179 })
180 )
181 const format = Schema.toFormatter(Schema.Union([Transformed, Schema.Boolean]))
182
183 strictEqual(format({ value: "a" }), "formatted:a")
184 })
185
186 it("preserves Union member formatter annotations", () => {
187 const member = Schema.String.pipe(
188 Schema.flip,
189 Schema.check(Schema.makeFilter(() => true)),
190 Schema.flip,
191 Schema.overrideToFormatter(() => (s) => s.toUpperCase())
192 )
193 const format = Schema.toFormatter(Schema.Union([member, Schema.Number]))
194
195 strictEqual(format("a"), "A")
196 })
197
198 it("preserves Union member encoding metadata for onBefore", () => {
199 const member = Schema.String.pipe(
200 Schema.decode({
201 decode: SchemaGetter.transform((s) => s),
202 encode: SchemaGetter.transform((s) => s)
203 })
204 )
205 const format = Schema.toFormatter(Schema.Union([member, Schema.Number]), {
206 onBefore: (ast) => ast.encoding ? () => "transformed" : undefined
207 })
208
209 strictEqual(format("a"), "transformed")
210 })
211
212 describe("Tuple", () => {
213 it("empty", () => {
214 const format = Schema.toFormatter(Schema.Tuple([]))
215 strictEqual(format([]), "[]")
216 })
217
218 it("elements", () => {
219 const format = Schema.toFormatter(Schema.Tuple([Schema.Option(Schema.String)]))
220 strictEqual(format([Option.some("a")]), `[some("a")]`)
221 strictEqual(format([Option.none()]), `[none()]`)
222 })
223 })
224
225 it("Array", () => {
226 const format = Schema.toFormatter(Schema.Array(Schema.Option(Schema.String)))
227 strictEqual(format([Option.some("a")]), `[some("a")]`)
228 strictEqual(format([Option.none()]), `[none()]`)
229 })
230
231 it("TupleWithRest", () => {
232 const format = Schema.toFormatter(
233 Schema.TupleWithRest(Schema.Tuple([Schema.Option(Schema.Boolean)]), [
234 Schema.Option(Schema.Number),
235 Schema.Option(Schema.String)
236 ])
237 )
238 strictEqual(format([Option.some(true), Option.some(1), Option.some("a")]), `[some(true), some(1), some("a")]`)
239 strictEqual(format([Option.none(), Option.none(), Option.some("a")]), `[none(), none(), some("a")]`)
240 })
241
242 it("TupleWithRest with multiple post-rest elements", () => {
243 const format = Schema.toFormatter(
244 Schema.TupleWithRest(Schema.Tuple([Schema.String]), [
245 Schema.String,
246 Schema.Number,
247 Schema.Boolean,
248 Schema.String
249 ])
250 )
251 strictEqual(format(["head", "tail", 1, true, "last"]), `["head", "tail", 1, true, "last"]`)
252 })
253
254 describe("rest elements", () => {
255 // Pairwise distinct, so a rest element read at the wrong index is observable.
256 const Tagged = (label: string) =>
257 Schema.String.pipe(Schema.overrideToFormatter(() => (s: string) => `${label}:${s}`))
258 const A = Tagged("A")
259 const B = Tagged("B")
260 const C = Tagged("C")
261
262 it("one trailing element after the rest element", () => {
263 const format = Schema.toFormatter(Schema.TupleWithRest(Schema.Tuple([A]), [B, C]))
264 strictEqual(format(["x", "z"]), `[A:x, C:z]`)
265 strictEqual(format(["x", "y", "z"]), `[A:x, B:y, C:z]`)
266 strictEqual(format(["x", "y1", "y2", "z"]), `[A:x, B:y1, B:y2, C:z]`)
267 })
268
269 it("multiple trailing elements after the rest element", () => {
270 const D = Tagged("D")
271 const format = Schema.toFormatter(Schema.TupleWithRest(Schema.Tuple([A]), [B, C, D]))
272 strictEqual(format(["x", "z", "w"]), `[A:x, C:z, D:w]`)
273 strictEqual(format(["x", "y", "z", "w"]), `[A:x, B:y, C:z, D:w]`)
274 strictEqual(format(["x", "y1", "y2", "z", "w"]), `[A:x, B:y1, B:y2, C:z, D:w]`)
275 })
276 })
277
278 describe("Struct", () => {
279 it("empty", () => {
280 const format = Schema.toFormatter(Schema.Struct({}))
281 strictEqual(format({}), "{}")
282 strictEqual(format(1), "1")
283 strictEqual(format("a"), `"a"`)
284 strictEqual(format(true), "true")
285 strictEqual(format(false), "false")
286 strictEqual(format({ a: 1 }), `{"a":1}`)
287 strictEqual(format([1, 2, 3]), `[1,2,3]`)
288 })
289
290 it("required fields", () => {
291 const format = Schema.toFormatter(Schema.Struct({
292 a: Schema.Option(Schema.String)
293 }))
294 strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
295 strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
296 })
297
298 it("required field with undefined", () => {
299 const format = Schema.toFormatter(Schema.Struct({
300 a: Schema.Option(Schema.UndefinedOr(Schema.String))
301 }))
302 strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
303 strictEqual(format({ a: Option.some(undefined) }), `{ "a": some(undefined) }`)
304 strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
305 })
306
307 it("optionalKey field", () => {
308 const format = Schema.toFormatter(Schema.Struct({
309 a: Schema.optionalKey(Schema.Option(Schema.String))
310 }))
311 strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
312 strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
313 strictEqual(format({}), `{}`)
314 })
315
316 it("optional field", () => {
317 const format = Schema.toFormatter(Schema.Struct({
318 a: Schema.optional(Schema.Option(Schema.String))
319 }))
320 strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
321 strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
322 strictEqual(format({ a: undefined }), `{ "a": undefined }`)
323 strictEqual(format({}), `{}`)
324 })
325 })
326
327 describe("Record", () => {
328 it("Record(String, Option(Number))", () => {
329 const format = Schema.toFormatter(Schema.Record(Schema.String, Schema.Option(Schema.Number)))
330 strictEqual(format({ a: Option.some(1) }), `{ "a": some(1) }`)
331 strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
332 })
333
334 it("Record(String.check, Option(Number)) should use the key checks to select keys", () => {
335 const format = Schema.toFormatter(Schema.Record(
336 Schema.String.check(Schema.isPattern(/^a/)),
337 Schema.Option(Schema.Number)
338 ))
339 strictEqual(format({ a: Option.some(1), b: Option.some(2) }), `{ "a": some(1) }`)
340 })
341
342 it("Record(Symbol, Option(Number))", () => {
343 const format = Schema.toFormatter(Schema.Record(Schema.Symbol, Schema.Option(Schema.Number)))
344 strictEqual(format({ [Symbol.for("a")]: Option.some(1) }), `{ Symbol(a): some(1) }`)
345 strictEqual(format({ [Symbol.for("a")]: Option.none() }), `{ Symbol(a): none() }`)
346 })
347 })
348
349 it("StructWithRest", () => {
350 const format = Schema.toFormatter(Schema.StructWithRest(
351 Schema.Struct({ a: Schema.Number }),
352 [Schema.Record(Schema.String, Schema.Number)]
353 ))
354 strictEqual(format({ a: 1, b: 2 }), `{ "a": 1, "b": 2 }`)
355 })
356
357 it("Class", () => {
358 class A extends Schema.Class<A>("A")({
359 a: Schema.Option(Schema.String)
360 }) {}
361 const format = Schema.toFormatter(A)
362 strictEqual(format({ a: Option.some("a") }), `A({ "a": some("a") })`)
363 strictEqual(format({ a: Option.none() }), `A({ "a": none() })`)
364 })
365
366 describe("suspend", () => {
367 it("compiles a recursive schema once", () => {
368 interface Tree {
369 readonly a: number
370 readonly as: ReadonlyArray<Tree>
371 }
372 let compiled = 0
373 const Tree = Schema.Struct({
374 a: Schema.Number.pipe(Schema.overrideToFormatter(() => {
375 compiled++
376 return String
377 })),
378 as: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => Tree))
379 })
380 const format = Schema.toFormatter(Tree)
381 strictEqual(
382 format({ a: 1, as: [{ a: 2, as: [{ a: 3, as: [] }] }] }),
383 `{ "a": 1, "as": [{ "a": 2, "as": [{ "a": 3, "as": [] }] }] }`
384 )
385 strictEqual(compiled, 1)
386 })
387
388 it("Tuple", () => {
389 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
390 const schema = Schema.Tuple([
391 Schema.Number,
392 Schema.NullOr(Rec)
393 ])
394 const format = Schema.toFormatter(schema)
395 strictEqual(format([1, null]), `[1, null]`)
396 strictEqual(format([1, [2, null]]), `[1, [2, null]]`)
397 })
398
399 it("Array", () => {
400 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
401 const schema: any = Schema.Array(Schema.Union([Schema.String, Rec]))
402 const format = Schema.toFormatter(schema)
403 strictEqual(format(["a"]), `["a"]`)
404 })
405
406 it("Struct", () => {
407 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
408 const schema = Schema.Struct({
409 a: Schema.String,
410 as: Schema.Array(Rec)
411 })
412 const format = Schema.toFormatter(schema)
413 strictEqual(
414 format({ a: "a", as: [{ a: "b", as: [] }, { a: "c", as: [] }] }),
415 `{ "a": "a", "as": [{ "a": "b", "as": [] }, { "a": "c", "as": [] }] }`
416 )
417 })
418
419 it("Record", () => {
420 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
421 const schema = Schema.Record(Schema.String, Rec)
422 const format = Schema.toFormatter(schema)
423 strictEqual(format({ a: { a: { a: {} } } }), `{ "a": { "a": { "a": {} } } }`)
424 })
425
426 it("optional", () => {
427 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
428 const schema: any = Schema.Struct({
429 a: Schema.optional(Rec)
430 })
431 const format = Schema.toFormatter(schema)
432 strictEqual(format({ a: "a" }), `{ "a": "a" }`)
433 })
434
435 it("Array + Array", () => {
436 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
437 const schema: any = Schema.Struct({
438 a: Schema.Array(Rec),
439 b: Schema.Array(Rec)
440 })
441 const format = Schema.toFormatter(schema)
442 strictEqual(
443 format({
444 a: [{ a: [{ a: [], b: [] }], b: [] }],
445 b: [{ a: [], b: [] }]
446 }),
447 `{ "a": [{ "a": [{ "a": [], "b": [] }], "b": [] }], "b": [{ "a": [], "b": [] }] }`
448 )
449 })
450
451 it("optional + Array", () => {
452 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
453 const schema: any = Schema.Struct({
454 a: Schema.optional(Rec),
455 b: Schema.Array(Rec)
456 })
457 const format = Schema.toFormatter(schema)
458 strictEqual(format({ a: "a", b: [{ a: "b", b: [] }] }), `{ "a": "a", "b": [{ "a": "b", "b": [] }] }`)
459 })
460
461 it("mutually suspended schemas", () => {
462 interface Expression {
463 readonly type: "expression"
464 readonly value: number | Operation
465 }
466
467 interface Operation {
468 readonly type: "operation"
469 readonly operator: "+" | "-"
470 readonly left: Expression
471 readonly right: Expression
472 }
473
474 const Expression = Schema.Struct({
475 type: Schema.Literal("expression"),
476 value: Schema.Union([Schema.Finite, Schema.suspend((): Schema.Codec<Operation> => Operation)])
477 })
478
479 const Operation = Schema.Struct({
480 type: Schema.Literal("operation"),
481 operator: Schema.Literals(["+", "-"]),
482 left: Expression,
483 right: Expression
484 })
485 const format = Schema.toFormatter(Operation)
486 strictEqual(
487 format({
488 type: "operation",
489 operator: "+",
490 left: { type: "expression", value: 1 },
491 right: { type: "expression", value: 2 }
492 }),
493 `{ "type": "operation", "operator": "+", "left": { "type": "expression", "value": 1 }, "right": { "type": "expression", "value": 2 } }`
494 )
495 })
496
497 it("Option", () => {
498 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
499 const schema = Schema.Struct({
500 a: Schema.String,
501 as: Schema.Option(Rec)
502 })
503 const format = Schema.toFormatter(schema)
504 strictEqual(
505 format({ a: "a", as: Option.some({ a: "b", as: Option.none() }) }),
506 `{ "a": "a", "as": some({ "a": "b", "as": none() }) }`
507 )
508 })
509
510 it("ReadonlySet", () => {
511 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
512 const schema = Schema.ReadonlySet(Rec)
513 const format = Schema.toFormatter(schema)
514 strictEqual(format(new Set()), `ReadonlySet(0) {}`)
515 strictEqual(format(new Set([new Set([new Set()])])), `ReadonlySet(1) { ReadonlySet(1) { ReadonlySet(0) {} } }`)
516 })
517
518 it("ReadonlyMap", () => {
519 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
520 const schema = Schema.ReadonlyMap(Schema.String, Rec)
521 const format = Schema.toFormatter(schema)
522 strictEqual(format(new Map()), `ReadonlyMap(0) {}`)
523 strictEqual(
524 format(new Map([["a", new Map([["b", new Map()]])]])),
525 `ReadonlyMap(1) { "a" => ReadonlyMap(1) { "b" => ReadonlyMap(0) {} } }`
526 )
527 })
528
529 it("HashMap", () => {
530 const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
531 const schema = Schema.HashMap(Schema.String, Rec)
532 const format = Schema.toFormatter(schema)
533 strictEqual(format(HashMap.empty()), `HashMap(0) {}`)
534 strictEqual(
535 format(HashMap.make(["a", HashMap.make(["b", HashMap.empty()])])),
536 `HashMap(1) { "a" => HashMap(1) { "b" => HashMap(0) {} } }`
537 )
538 })
539 })
540
541 it("Date", () => {
542 const format = Schema.toFormatter(Schema.Date)
543 strictEqual(format(new Date(0)), "1970-01-01T00:00:00.000Z")
544 })
545
546 it("URL", () => {
547 const format = Schema.toFormatter(Schema.URL)
548 strictEqual(format(new URL("https://www.example.com")), "https://www.example.com/")
549 })
550
551 it("RegExp", () => {
552 const format = Schema.toFormatter(Schema.RegExp)
553 strictEqual(format(/a/), `/a/`)
554 strictEqual(format(/a/i), `/a/i`)
555 })
556
557 it("Option(String)", () => {
558 const format = Schema.toFormatter(Schema.Option(Schema.String))
559 strictEqual(format(Option.some("a")), `some("a")`)
560 strictEqual(format(Option.none()), "none()")
561 })
562
563 it("Result(Number, String)", () => {
564 const format = Schema.toFormatter(Schema.Result(Schema.Number, Schema.String))
565 strictEqual(format(Result.succeed(1)), `success(1)`)
566 strictEqual(format(Result.fail("a")), `failure("a")`)
567 })
568
569 it("ReadonlyMap(String, Option(Number))", () => {
570 const format = Schema.toFormatter(Schema.ReadonlyMap(Schema.String, Schema.Option(Schema.Number)))
571 strictEqual(format(new Map([["a", Option.some(1)]])), `ReadonlyMap(1) { "a" => some(1) }`)
572 strictEqual(format(new Map([["a", Option.none()]])), `ReadonlyMap(1) { "a" => none() }`)
573 })
574
575 it("HashMap(String, Option(Number))", () => {
576 const format = Schema.toFormatter(Schema.HashMap(Schema.String, Schema.Option(Schema.Number)))
577 strictEqual(format(HashMap.make(["a", Option.some(1)])), `HashMap(1) { "a" => some(1) }`)
578 strictEqual(format(HashMap.make(["a", Option.none()])), `HashMap(1) { "a" => none() }`)
579 })
580
581 describe("Redacted", () => {
582 it("Redacted(String)", () => {
583 const format = Schema.toFormatter(Schema.Redacted(Schema.String))
584 strictEqual(format(Redacted.make("a")), `<redacted>`)
585 })
586
587 it("with label", () => {
588 const format = Schema.toFormatter(Schema.Redacted(Schema.String, { label: "password" }))
589 strictEqual(format(Redacted.make("a", { label: "password" })), `<redacted:password>`)
590 })
591 })
592
593 it("Duration", () => {
594 const format = Schema.toFormatter(Schema.Duration)
595 strictEqual(format(Duration.millis(100)), `100 millis`)
596 strictEqual(format(Duration.nanos(1000n)), `1000 nanos`)
597 strictEqual(format(Duration.infinity), "Infinity")
598 strictEqual(format(Duration.negativeInfinity), "-Infinity")
599 })
600
601 it("BigDecimal", () => {
602 const format = Schema.toFormatter(Schema.BigDecimal)
603 strictEqual(format(BigDecimal.fromStringUnsafe("123.45")), "123.45")
604 strictEqual(format(BigDecimal.fromStringUnsafe("-5")), "-5")
605 strictEqual(format(BigDecimal.fromStringUnsafe("0")), "0")
606 })
607
608 it("DateTimeUtc", () => {
609 const format = Schema.toFormatter(Schema.DateTimeUtc)
610 strictEqual(format(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")), "DateTime.Utc(2021-01-01T00:00:00.000Z)")
611 })
612
613 it("TimeZoneOffset", () => {
614 const format = Schema.toFormatter(Schema.TimeZoneOffset)
615 strictEqual(format(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)), "+03:00")
616 })
617
618 it("TimeZoneNamed", () => {
619 const format = Schema.toFormatter(Schema.TimeZoneNamed)
620 strictEqual(format(DateTime.zoneMakeNamedUnsafe("Europe/London")), "Europe/London")
621 })
622
623 it("TimeZone", () => {
624 const format = Schema.toFormatter(Schema.TimeZone)
625 strictEqual(format(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)), "+03:00")
626 strictEqual(format(DateTime.zoneMakeNamedUnsafe("Europe/London")), "Europe/London")
627 })
628
629 it("DateTimeZoned", () => {
630 const format = Schema.toFormatter(Schema.DateTimeZoned)
631 const zoned = DateTime.makeZonedUnsafe("2024-01-01T00:00:00.000Z", { timeZone: "Europe/London" })
632 strictEqual(format(zoned), DateTime.formatIsoZoned(zoned))
633 })
634
635 it("custom class", () => {
636 class A {
637 constructor(readonly a: string) {}
638 }
639 const schema = Schema.instanceOf(A)
640 const format = Schema.toFormatter(schema)
641 strictEqual(format(new A("a")), `A({"a":"a"})`)
642 })
643
644 it("custom class with a toString() method", () => {
645 class A {
646 constructor(readonly a: string) {}
647 toString() {
648 return `A(${this.a})`
649 }
650 }
651 const schema = Schema.instanceOf(A)
652 const format = Schema.toFormatter(schema)
653 strictEqual(format(new A("a")), `A(a)`)
654 })
655
656 describe("Annotations", () => {
657 describe("overrideToFormatter", () => {
658 it("String", () => {
659 const schema = Schema.String.pipe(Schema.overrideToFormatter(() => (s) => s.toUpperCase()))
660 const format = Schema.toFormatter(schema)
661 strictEqual(format("a"), "A")
662 })
663
664 it("String & isMinLength(1)", () => {
665 const schema = Schema.String.check(Schema.isMinLength(1)).pipe(
666 Schema.overrideToFormatter(() => (s) => s.toUpperCase())
667 )
668 const format = Schema.toFormatter(schema)
669 strictEqual(format("a"), "A")
670 })
671 })
672 })
673
674 it("should allow for ast-level overrides", () => {
675 const toFormatter = <S extends Schema.Constraint>(schema: S) =>
676 Schema.toFormatter(schema, {
677 onBefore: (ast) => {
678 if (ast._tag === "Boolean") {
679 return (b: boolean) => b ? "True" : "False"
680 }
681 }
682 })
683 strictEqual(toFormatter(Schema.Boolean)(true), `True`)
684 const schema = Schema.Tuple([Schema.String, Schema.Boolean])
685 strictEqual(toFormatter(schema)(["a", true]), `["a", True]`)
686 })
687})