effect-app 4.0.0-beta.7 → 4.0.0-beta.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/dist/Config/SecretURL.js +2 -2
  3. package/dist/Config.d.ts +7 -0
  4. package/dist/Config.d.ts.map +1 -0
  5. package/dist/Config.js +6 -0
  6. package/dist/ConfigProvider.d.ts +39 -0
  7. package/dist/ConfigProvider.d.ts.map +1 -0
  8. package/dist/ConfigProvider.js +42 -0
  9. package/dist/Context.d.ts +40 -0
  10. package/dist/Context.d.ts.map +1 -0
  11. package/dist/Context.js +66 -0
  12. package/dist/Effect.d.ts +8 -7
  13. package/dist/Effect.d.ts.map +1 -1
  14. package/dist/Effect.js +3 -2
  15. package/dist/Layer.d.ts +5 -4
  16. package/dist/Layer.d.ts.map +1 -1
  17. package/dist/Layer.js +1 -1
  18. package/dist/Operations.d.ts +61 -25
  19. package/dist/Operations.d.ts.map +1 -1
  20. package/dist/Pure.d.ts +2 -2
  21. package/dist/Pure.d.ts.map +1 -1
  22. package/dist/Pure.js +13 -13
  23. package/dist/Schema/Class.d.ts +39 -1
  24. package/dist/Schema/Class.d.ts.map +1 -1
  25. package/dist/Schema/Class.js +89 -12
  26. package/dist/Schema/SpecialJsonSchema.d.ts +21 -0
  27. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  28. package/dist/Schema/SpecialJsonSchema.js +59 -0
  29. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  30. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  31. package/dist/Schema/SpecialOpenApi.js +123 -0
  32. package/dist/Schema/brand.d.ts +8 -5
  33. package/dist/Schema/brand.d.ts.map +1 -1
  34. package/dist/Schema/brand.js +1 -1
  35. package/dist/Schema/email.d.ts.map +1 -1
  36. package/dist/Schema/email.js +9 -4
  37. package/dist/Schema/ext.d.ts +81 -45
  38. package/dist/Schema/ext.d.ts.map +1 -1
  39. package/dist/Schema/ext.js +94 -49
  40. package/dist/Schema/moreStrings.d.ts +6 -6
  41. package/dist/Schema/moreStrings.d.ts.map +1 -1
  42. package/dist/Schema/moreStrings.js +14 -9
  43. package/dist/Schema/numbers.d.ts +11 -11
  44. package/dist/Schema/numbers.d.ts.map +1 -1
  45. package/dist/Schema/numbers.js +10 -9
  46. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  47. package/dist/Schema/phoneNumber.js +8 -3
  48. package/dist/Schema/strings.d.ts +4 -4
  49. package/dist/Schema/strings.d.ts.map +1 -1
  50. package/dist/Schema.d.ts +22 -55
  51. package/dist/Schema.d.ts.map +1 -1
  52. package/dist/Schema.js +43 -64
  53. package/dist/client/apiClientFactory.d.ts +3 -3
  54. package/dist/client/apiClientFactory.d.ts.map +1 -1
  55. package/dist/client/apiClientFactory.js +14 -15
  56. package/dist/client/errors.d.ts +17 -9
  57. package/dist/client/errors.d.ts.map +1 -1
  58. package/dist/client/errors.js +35 -10
  59. package/dist/client/makeClient.d.ts +13 -12
  60. package/dist/client/makeClient.d.ts.map +1 -1
  61. package/dist/client/makeClient.js +5 -2
  62. package/dist/http/Request.d.ts.map +1 -1
  63. package/dist/http/Request.js +5 -5
  64. package/dist/ids.d.ts +3 -3
  65. package/dist/ids.d.ts.map +1 -1
  66. package/dist/ids.js +3 -2
  67. package/dist/index.d.ts +3 -8
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -9
  70. package/dist/middleware.d.ts +2 -2
  71. package/dist/middleware.d.ts.map +1 -1
  72. package/dist/middleware.js +3 -3
  73. package/dist/rpc/MiddlewareMaker.d.ts +4 -3
  74. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  75. package/dist/rpc/MiddlewareMaker.js +7 -6
  76. package/dist/rpc/RpcContextMap.d.ts +2 -2
  77. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  78. package/dist/rpc/RpcContextMap.js +4 -4
  79. package/dist/rpc/RpcMiddleware.d.ts +4 -3
  80. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  81. package/dist/rpc/RpcMiddleware.js +1 -1
  82. package/dist/utils/gen.d.ts +1 -1
  83. package/dist/utils/gen.d.ts.map +1 -1
  84. package/dist/utils/logger.d.ts +2 -2
  85. package/dist/utils/logger.d.ts.map +1 -1
  86. package/dist/utils/logger.js +3 -3
  87. package/dist/utils.d.ts +18 -0
  88. package/dist/utils.d.ts.map +1 -1
  89. package/dist/utils.js +24 -5
  90. package/package.json +29 -17
  91. package/src/Config/SecretURL.ts +1 -1
  92. package/src/Config.ts +14 -0
  93. package/src/ConfigProvider.ts +48 -0
  94. package/src/{ServiceMap.ts → Context.ts} +57 -64
  95. package/src/Effect.ts +11 -9
  96. package/src/Layer.ts +5 -4
  97. package/src/Pure.ts +17 -18
  98. package/src/Schema/Class.ts +114 -16
  99. package/src/Schema/SpecialJsonSchema.ts +69 -0
  100. package/src/Schema/SpecialOpenApi.ts +130 -0
  101. package/src/Schema/brand.ts +13 -7
  102. package/src/Schema/email.ts +10 -2
  103. package/src/Schema/ext.ts +182 -80
  104. package/src/Schema/moreStrings.ts +20 -10
  105. package/src/Schema/numbers.ts +9 -8
  106. package/src/Schema/phoneNumber.ts +8 -1
  107. package/src/Schema.ts +79 -103
  108. package/src/client/apiClientFactory.ts +18 -18
  109. package/src/client/errors.ts +46 -12
  110. package/src/client/makeClient.ts +32 -12
  111. package/src/http/Request.ts +7 -4
  112. package/src/ids.ts +3 -2
  113. package/src/index.ts +3 -11
  114. package/src/middleware.ts +2 -2
  115. package/src/rpc/MiddlewareMaker.ts +8 -7
  116. package/src/rpc/RpcContextMap.ts +6 -5
  117. package/src/rpc/RpcMiddleware.ts +5 -4
  118. package/src/utils/gen.ts +1 -1
  119. package/src/utils/logger.ts +2 -2
  120. package/src/utils.ts +26 -4
  121. package/test/dist/moreStrings.test.d.ts.map +1 -0
  122. package/test/dist/rpc.test.d.ts.map +1 -1
  123. package/test/dist/secretURL.test.d.ts.map +1 -0
  124. package/test/dist/special.test.d.ts.map +1 -0
  125. package/test/moreStrings.test.ts +17 -0
  126. package/test/rpc.test.ts +26 -5
  127. package/test/schema.test.ts +396 -3
  128. package/test/secretURL.test.ts +157 -0
  129. package/test/special.test.ts +732 -0
  130. package/test/utils.test.ts +1 -1
  131. package/tsconfig.base.json +0 -1
  132. package/tsconfig.json +0 -1
  133. package/dist/ServiceMap.d.ts +0 -44
  134. package/dist/ServiceMap.d.ts.map +0 -1
  135. package/dist/ServiceMap.js +0 -91
  136. package/dist/Struct.d.ts +0 -44
  137. package/dist/Struct.d.ts.map +0 -1
  138. package/dist/Struct.js +0 -29
  139. package/src/Struct.ts +0 -54
@@ -1,6 +1,7 @@
1
1
  // import { generateFromArbitrary } from "@effect-app/infra/test"
2
2
  import { Array, S } from "effect-app"
3
- import { test } from "vitest"
3
+ import { specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
4
+ import { describe, expect, expectTypeOf, test } from "vitest"
4
5
 
5
6
  const A = S.Struct({ a: S.NonEmptyString255, email: S.NullOr(S.Email) })
6
7
  test("works", () => {
@@ -17,9 +18,401 @@ test("literal default works", () => {
17
18
  const l = S.Literal("a", "b")
18
19
  expect(l.Default).toBe("a")
19
20
  const s = S.Struct({ l: l.withDefault })
20
- expect(s.makeUnsafe({}).l).toBe("a")
21
+ expect(s.make({}).l).toBe("a")
21
22
 
22
23
  const l2 = l.changeDefault("b")
23
24
  const s2 = S.Struct({ l: l2.withDefault })
24
- expect(s2.makeUnsafe({}).l).toBe("b")
25
+ expect(s2.make({}).l).toBe("b")
26
+ })
27
+
28
+ test("tagged union derives tag map and tags from v4 literal ast", () => {
29
+ const schema = S.TaggedUnion(
30
+ S.TaggedStruct("A", { a: S.String }),
31
+ S.TaggedStruct("B", { b: S.Finite }),
32
+ S.TaggedStruct("C", { c: S.Boolean })
33
+ )
34
+ const caseA = schema.cases["A"]
35
+ const caseB = schema.cases["B"]
36
+ const caseC = schema.cases["C"]
37
+ const isAOrB = schema.isAnyOf(["A", "B"])
38
+
39
+ expect(caseA.fields._tag.ast.literal).toBe("A")
40
+ expect(caseB.fields._tag.ast.literal).toBe("B")
41
+ expect(caseC.fields._tag.ast.literal).toBe("C")
42
+ expect(S.decodeSync(schema.tags)("A")).toBe("A")
43
+ expect(S.decodeSync(schema.tags)("B")).toBe("B")
44
+ expect(S.decodeSync(schema.tags)("C")).toBe("C")
45
+ expect(() => S.decodeUnknownSync(schema.tags)("D")).toThrow()
46
+
47
+ expect(schema.guards.A({ _tag: "A", a: "ok" })).toBe(true)
48
+ expect(schema.guards.A({ _tag: "B", b: 1 })).toBe(false)
49
+ expect(schema.guards.B({ _tag: "B", b: 1 })).toBe(true)
50
+ expect(schema.guards.B({ _tag: "A", a: "ok" })).toBe(false)
51
+ expect(schema.guards.C({ _tag: "C", c: true })).toBe(true)
52
+ expect(schema.guards.C({ _tag: "A", a: "ok" })).toBe(false)
53
+
54
+ expect(isAOrB({ _tag: "A", a: "ok" })).toBe(true)
55
+ expect(isAOrB({ _tag: "B", b: 1 })).toBe(true)
56
+ expect(isAOrB({ _tag: "C", c: true })).toBe(false)
57
+ })
58
+
59
+ test("TaggedUnion tags returns a Literals schema with correct literal values", () => {
60
+ const schema = S.TaggedUnion(
61
+ S.TaggedStruct("X", { x: S.String }),
62
+ S.TaggedStruct("Y", { y: S.Finite })
63
+ )
64
+
65
+ expect(schema.tags.literals).toEqual(["X", "Y"])
66
+ expectTypeOf(schema.tags.literals).toMatchTypeOf<readonly ["X", "Y"]>()
67
+ })
68
+
69
+ test("TaggedUnion tags.pick returns a subset of the tag literals", () => {
70
+ const schema = S.TaggedUnion(
71
+ S.TaggedStruct("A", { a: S.String }),
72
+ S.TaggedStruct("B", { b: S.Finite }),
73
+ S.TaggedStruct("C", { c: S.Boolean })
74
+ )
75
+
76
+ const subset = schema.tags.pick(["A", "C"])
77
+ expect(subset.literals).toEqual(["A", "C"])
78
+ expect(S.decodeSync(subset)("A")).toBe("A")
79
+ expect(S.decodeSync(subset)("C")).toBe("C")
80
+ expect(() => S.decodeUnknownSync(subset)("B")).toThrow()
81
+ })
82
+
83
+ test("tags standalone function extracts tags from member schemas", () => {
84
+ const members = [
85
+ S.TaggedStruct("Foo", { foo: S.String }),
86
+ S.TaggedStruct("Bar", { bar: S.Finite })
87
+ ] as const
88
+
89
+ const tagSchema = S.tags(members)
90
+ expect(tagSchema.literals).toEqual(["Foo", "Bar"])
91
+ expect(S.decodeSync(tagSchema)("Foo")).toBe("Foo")
92
+ expect(S.decodeSync(tagSchema)("Bar")).toBe("Bar")
93
+ expect(() => S.decodeUnknownSync(tagSchema)("Baz")).toThrow()
94
+ })
95
+
96
+ test("ExtendTaggedUnion adds tags to an existing Union", () => {
97
+ const union = S.Union([
98
+ S.TaggedStruct("P", { p: S.String }),
99
+ S.TaggedStruct("Q", { q: S.Finite })
100
+ ])
101
+ const extended = S.ExtendTaggedUnion(union)
102
+
103
+ expect(extended.tags.literals).toEqual(["P", "Q"])
104
+ expect(S.decodeSync(extended.tags)("P")).toBe("P")
105
+ expect(S.decodeSync(extended.tags)("Q")).toBe("Q")
106
+ expect(() => S.decodeUnknownSync(extended.tags)("R")).toThrow()
107
+ expect(extended.cases["P"].fields._tag.ast.literal).toBe("P")
108
+ expect(extended.guards.P({ _tag: "P", p: "ok" })).toBe(true)
109
+ expect(extended.guards.P({ _tag: "Q", q: 1 })).toBe(false)
110
+ })
111
+
112
+ test("TaggedUnion match dispatches on _tag", () => {
113
+ const schema = S.TaggedUnion(
114
+ S.TaggedStruct("A", { a: S.String }),
115
+ S.TaggedStruct("B", { b: S.Finite })
116
+ )
117
+ type T = S.Schema.Type<typeof schema>
118
+
119
+ const matcher = schema.match({
120
+ A: (v) => `got A: ${v.a}`,
121
+ B: (v) => `got B: ${v.b}`
122
+ })
123
+ expect(matcher({ _tag: "A", a: "hello" } as T)).toBe("got A: hello")
124
+ expect(matcher({ _tag: "B", b: 42 } as T)).toBe("got B: 42")
125
+ })
126
+
127
+ test("TaggedUnion with single member", () => {
128
+ const schema = S.TaggedUnion(
129
+ S.TaggedStruct("Only", { val: S.String })
130
+ )
131
+
132
+ expect(schema.tags.literals).toEqual(["Only"])
133
+ expect(S.decodeSync(schema.tags)("Only")).toBe("Only")
134
+ expect(() => S.decodeUnknownSync(schema.tags)("Other")).toThrow()
135
+ expect(schema.guards.Only({ _tag: "Only", val: "x" })).toBe(true)
136
+ })
137
+
138
+ test("TaggedUnion tags type is narrowed to the exact tag literals", () => {
139
+ const schema = S.TaggedUnion(
140
+ S.TaggedStruct("Alpha", { a: S.String }),
141
+ S.TaggedStruct("Beta", { b: S.Finite }),
142
+ S.TaggedStruct("Gamma", { c: S.Boolean })
143
+ )
144
+
145
+ type Tags = S.Schema.Type<typeof schema.tags>
146
+ expectTypeOf<Tags>().toEqualTypeOf<"Alpha" | "Beta" | "Gamma">()
147
+ })
148
+
149
+ test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
150
+ const MemberA = S.TaggedStruct("A", { firstName: S.String }).pipe(
151
+ S.encodeKeys({ firstName: "first_name" })
152
+ )
153
+ const MemberB = S.TaggedStruct("B", { lastName: S.Finite }).pipe(
154
+ S.encodeKeys({ lastName: "last_name" })
155
+ )
156
+
157
+ const schema = S.TaggedUnion(MemberA, MemberB)
158
+
159
+ expect(schema.tags.literals).toEqual(["A", "B"])
160
+ expect(S.decodeSync(schema.tags)("A")).toBe("A")
161
+ expect(S.decodeSync(schema.tags)("B")).toBe("B")
162
+
163
+ // decode from encoded (snake_case) to decoded (camelCase)
164
+ const decoded = S.decodeUnknownSync(schema)({ _tag: "A", first_name: "Alice" })
165
+ expect(decoded).toEqual({ _tag: "A", firstName: "Alice" })
166
+
167
+ const decoded2 = S.decodeUnknownSync(schema)({ _tag: "B", last_name: 42 })
168
+ expect(decoded2).toEqual({ _tag: "B", lastName: 42 })
169
+
170
+ // encode back to snake_case
171
+ type T = S.Schema.Type<typeof schema>
172
+ const encoded = S.encodeSync(schema)({ _tag: "A", firstName: "Alice" } as T)
173
+ expect(encoded).toEqual({ _tag: "A", first_name: "Alice" })
174
+
175
+ // guards work on decoded values
176
+ expect(schema.guards.A({ _tag: "A", firstName: "Alice" })).toBe(true)
177
+ expect(schema.guards.A({ _tag: "B", lastName: 42 })).toBe(false)
178
+ expect(schema.guards.B({ _tag: "B", lastName: 42 })).toBe(true)
179
+ })
180
+
181
+ test("TaggedUnion with TaggedClass members", () => {
182
+ class Foo extends S.TaggedClass<Foo>()("Foo", { name: S.String }) {}
183
+ class Bar extends S.TaggedClass<Bar>()("Bar", { count: S.Finite }) {}
184
+
185
+ const schema = S.TaggedUnion(Foo, Bar)
186
+
187
+ expect(schema.tags.literals).toEqual(["Foo", "Bar"])
188
+ expect(S.decodeSync(schema.tags)("Foo")).toBe("Foo")
189
+ expect(S.decodeSync(schema.tags)("Bar")).toBe("Bar")
190
+ expect(() => S.decodeUnknownSync(schema.tags)("Baz")).toThrow()
191
+
192
+ const decoded = S.decodeUnknownSync(schema)({ _tag: "Foo", name: "Alice" })
193
+ expect(decoded).toBeInstanceOf(Foo)
194
+ expect(decoded).toEqual(new Foo({ name: "Alice" }))
195
+
196
+ const decoded2 = S.decodeUnknownSync(schema)({ _tag: "Bar", count: 3 })
197
+ expect(decoded2).toBeInstanceOf(Bar)
198
+ expect(decoded2).toEqual(new Bar({ count: 3 }))
199
+
200
+ expect(schema.guards.Foo(new Foo({ name: "Alice" }))).toBe(true)
201
+ expect(schema.guards.Foo(new Bar({ count: 3 }))).toBe(false)
202
+ expect(schema.guards.Bar(new Bar({ count: 3 }))).toBe(true)
203
+ })
204
+
205
+ describe("ReadonlySetFromArray", () => {
206
+ test("decodes an array of strings to a Set", () => {
207
+ const schema = S.ReadonlySetFromArray(S.String)
208
+ const decoded = S.decodeUnknownSync(schema)(["a", "b", "c"])
209
+ expect(decoded).toEqual(new Set(["a", "b", "c"]))
210
+ })
211
+
212
+ test("encodes a Set back to an array", () => {
213
+ const schema = S.ReadonlySetFromArray(S.String)
214
+ const encoded = S.encodeSync(schema)(new Set(["a", "b"]))
215
+ expect(encoded).toEqual(["a", "b"])
216
+ })
217
+
218
+ test("decodes with NumberFromString as value", () => {
219
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
220
+ const decoded = S.decodeUnknownSync(schema)(["1", "2", "3"])
221
+ expect(decoded).toEqual(new Set([1, 2, 3]))
222
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlySet<number>>()
223
+ })
224
+
225
+ test("encodes with NumberFromString as value", () => {
226
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
227
+ const encoded = S.encodeSync(schema)(new Set([1, 2, 3]))
228
+ expect(encoded).toEqual(["1", "2", "3"])
229
+ })
230
+
231
+ test("rejects invalid input", () => {
232
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
233
+ expect(() => S.decodeUnknownSync(schema)([1, 2])).toThrow()
234
+ })
235
+ })
236
+
237
+ describe("ReadonlyMapFromArray", () => {
238
+ test("decodes an array of tuples to a Map", () => {
239
+ const schema = S.ReadonlyMap({ key: S.String, value: S.Finite })
240
+ const decoded = S.decodeUnknownSync(schema)([["a", 1], ["b", 2]])
241
+ expect(decoded).toEqual(new Map([["a", 1], ["b", 2]]))
242
+ })
243
+
244
+ test("encodes a Map back to an array of tuples", () => {
245
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })
246
+ const encoded = S.encodeSync(schema)(new Map([["a", 1], ["b", 2]]))
247
+ expect(encoded).toEqual([["a", 1], ["b", 2]])
248
+ })
249
+
250
+ test("decodes with NumberFromString as key", () => {
251
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
252
+ const decoded = S.decodeUnknownSync(schema)([["1", "one"], ["2", "two"]])
253
+ expect(decoded).toEqual(new Map([[1, "one"], [2, "two"]]))
254
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, string>>()
255
+ })
256
+
257
+ test("encodes with NumberFromString as key", () => {
258
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
259
+ const encoded = S.encodeSync(schema)(new Map([[1, "one"], [2, "two"]]))
260
+ expect(encoded).toEqual([["1", "one"], ["2", "two"]])
261
+ })
262
+
263
+ test("decodes with NumberFromString as value", () => {
264
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
265
+ const decoded = S.decodeUnknownSync(schema)([["a", "10"], ["b", "20"]])
266
+ expect(decoded).toEqual(new Map([["a", 10], ["b", 20]]))
267
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<string, number>>()
268
+ })
269
+
270
+ test("encodes with NumberFromString as value", () => {
271
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
272
+ const encoded = S.encodeSync(schema)(new Map([["a", 10], ["b", 20]]))
273
+ expect(encoded).toEqual([["a", "10"], ["b", "20"]])
274
+ })
275
+
276
+ test("decodes with NumberFromString as both key and value", () => {
277
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.NumberFromString })
278
+ const decoded = S.decodeUnknownSync(schema)([["1", "10"], ["2", "20"]])
279
+ expect(decoded).toEqual(new Map([[1, 10], [2, 20]]))
280
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, number>>()
281
+ })
282
+
283
+ test("rejects invalid input", () => {
284
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
285
+ expect(() => S.decodeUnknownSync(schema)([[1, "val"]])).toThrow()
286
+ })
287
+ })
288
+
289
+ describe("ReadonlySet (with withDefault)", () => {
290
+ test("make provides withDefault", () => {
291
+ const schema = S.ReadonlySet(S.NumberFromString)
292
+ const struct = S.Struct({ items: schema.withDefault })
293
+ const made = struct.make({})
294
+ expect(made.items).toEqual(new Set())
295
+ })
296
+
297
+ test("decodes array with NumberFromString values", () => {
298
+ const schema = S.ReadonlySet(S.NumberFromString)
299
+ const decoded = S.decodeUnknownSync(schema)(["1", "2"])
300
+ expect(decoded).toEqual(new Set([1, 2]))
301
+ })
302
+ })
303
+
304
+ describe("ReadonlyMap (with withDefault)", () => {
305
+ test("make provides withDefault", () => {
306
+ const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
307
+ const struct = S.Struct({ items: schema.withDefault })
308
+ const made = struct.make({})
309
+ expect(made.items).toEqual(new Map())
310
+ })
311
+
312
+ test("decodes array of tuples with NumberFromString keys", () => {
313
+ const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
314
+ const decoded = S.decodeUnknownSync(schema)([["1", "one"]])
315
+ expect(decoded).toEqual(new Map([[1, "one"]]))
316
+ })
317
+ })
318
+
319
+ describe("JSON Schema", () => {
320
+ test("Email has format, minLength, maxLength", () => {
321
+ const doc = S.toJsonSchemaDocument(S.Email)
322
+ expect(doc).toStrictEqual({
323
+ dialect: "draft-2020-12",
324
+ schema: { "$ref": "#/$defs/Email" },
325
+ definitions: {
326
+ Email: {
327
+ type: "string",
328
+ title: "Email",
329
+ description: "an email according to RFC 5322",
330
+ format: "email",
331
+ allOf: [
332
+ { minLength: 3 },
333
+ { maxLength: 998 }
334
+ ]
335
+ }
336
+ }
337
+ })
338
+ })
339
+
340
+ test("Email specialJsonSchemaDocument flattens allOf", () => {
341
+ const doc = specialJsonSchemaDocument(S.Email)
342
+ expect(doc).toStrictEqual({
343
+ dialect: "draft-2020-12",
344
+ schema: { "$ref": "#/$defs/Email" },
345
+ definitions: {
346
+ Email: {
347
+ type: "string",
348
+ title: "Email",
349
+ description: "an email according to RFC 5322",
350
+ format: "email",
351
+ minLength: 3,
352
+ maxLength: 998
353
+ }
354
+ }
355
+ })
356
+ })
357
+
358
+ test("Date has format date-time and description", () => {
359
+ const doc = S.toJsonSchemaDocument(S.Date)
360
+ expect(doc).toStrictEqual({
361
+ dialect: "draft-2020-12",
362
+ schema: { "$ref": "#/$defs/Date" },
363
+ definitions: {
364
+ Date: {
365
+ type: "string",
366
+ description: "a string in ISO 8601 format that will be decoded as a Date",
367
+ format: "date-time"
368
+ }
369
+ }
370
+ })
371
+ })
372
+
373
+ test("DateValid has format date-time", () => {
374
+ const doc = S.toJsonSchemaDocument(S.DateValid)
375
+ expect(doc).toStrictEqual({
376
+ dialect: "draft-2020-12",
377
+ schema: { "$ref": "#/$defs/Date" },
378
+ definitions: {
379
+ Date: {
380
+ type: "string",
381
+ description: "a string in ISO 8601 format that will be decoded as a Date",
382
+ format: "date-time"
383
+ }
384
+ }
385
+ })
386
+ })
387
+
388
+ test("PhoneNumber has format phone", () => {
389
+ const doc = specialJsonSchemaDocument(S.PhoneNumber)
390
+ expect(doc).toStrictEqual({
391
+ dialect: "draft-2020-12",
392
+ schema: { "$ref": "#/$defs/PhoneNumber" },
393
+ definitions: {
394
+ PhoneNumber: {
395
+ type: "string",
396
+ title: "PhoneNumber",
397
+ description: "a phone number with at least 7 digits",
398
+ format: "phone"
399
+ }
400
+ }
401
+ })
402
+ })
403
+
404
+ test("Url has format uri", () => {
405
+ const doc = specialJsonSchemaDocument(S.Url)
406
+ expect(doc).toStrictEqual({
407
+ dialect: "draft-2020-12",
408
+ schema: { "$ref": "#/$defs/Url" },
409
+ definitions: {
410
+ Url: {
411
+ type: "string",
412
+ title: "Url",
413
+ format: "uri"
414
+ }
415
+ }
416
+ })
417
+ })
25
418
  })
@@ -0,0 +1,157 @@
1
+ import { Chunk, Config, ConfigProvider, Effect } from "effect"
2
+ import { describe, expect, test } from "vitest"
3
+ import { fromChunk, fromString, isSecretURL, make, secretURL, unsafeWipe, value } from "../src/Config/SecretURL.js"
4
+
5
+ const testUrls = [
6
+ "https://example.com/path?key=secret123",
7
+ "http://user:password@host.com:8080/resource",
8
+ "postgres://admin:p4ssw0rd@db.internal:5432/mydb",
9
+ "redis://default:token@cache.example.com:6379",
10
+ "mongodb+srv://user:pass@cluster.mongodb.net/db",
11
+ "https://api.example.com/v2/endpoint?token=abc&format=json"
12
+ ]
13
+
14
+ describe("SecretURL", () => {
15
+ describe("fromString / value roundtrip", () => {
16
+ test.each(testUrls)("preserves %s", (url) => {
17
+ const secret = fromString(url)
18
+ expect(value(secret)).toBe(url)
19
+ })
20
+ })
21
+
22
+ describe("make / value roundtrip", () => {
23
+ test.each(testUrls)("preserves %s via byte array", (url) => {
24
+ const bytes = url.split("").map((c) => c.charCodeAt(0))
25
+ const secret = make(bytes)
26
+ expect(value(secret)).toBe(url)
27
+ })
28
+ })
29
+
30
+ describe("fromChunk / value roundtrip", () => {
31
+ test.each(testUrls)("preserves %s via Chunk", (url) => {
32
+ const chunk = Chunk.fromIterable(url.split(""))
33
+ const secret = fromChunk(chunk)
34
+ expect(value(secret)).toBe(url)
35
+ })
36
+ })
37
+
38
+ describe("toString", () => {
39
+ test("redacts the URL, showing only protocol", () => {
40
+ const secret = fromString("https://example.com/secret")
41
+ expect(String(secret)).toBe("SecretURL(https://<redacted>)")
42
+ })
43
+
44
+ test("shows http protocol", () => {
45
+ const secret = fromString("http://example.com")
46
+ expect(String(secret)).toBe("SecretURL(http://<redacted>)")
47
+ })
48
+
49
+ test("shows unknown for non-URL strings", () => {
50
+ const secret = fromString("not-a-url")
51
+ expect(String(secret)).toBe("SecretURL(unknown://<redacted>)")
52
+ })
53
+ })
54
+
55
+ describe("toJSON", () => {
56
+ test("returns tag and protocol only", () => {
57
+ const secret = fromString("https://example.com/secret")
58
+ expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "https" })
59
+ })
60
+
61
+ test("returns unknown protocol for non-URL", () => {
62
+ const secret = fromString("not-a-url")
63
+ expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "unknown" })
64
+ })
65
+ })
66
+
67
+ describe("isSecretURL", () => {
68
+ test("returns true for SecretURL", () => {
69
+ const secret = fromString("https://example.com")
70
+ expect(isSecretURL(secret)).toBe(true)
71
+ })
72
+
73
+ test("returns false for plain string", () => {
74
+ expect(isSecretURL("https://example.com")).toBe(false)
75
+ })
76
+
77
+ test("returns false for plain object", () => {
78
+ expect(isSecretURL({ raw: [1, 2, 3] })).toBe(false)
79
+ })
80
+ })
81
+
82
+ describe("unsafeWipe", () => {
83
+ test("zeroes out raw bytes", () => {
84
+ const secret = fromString("https://example.com")
85
+ unsafeWipe(secret)
86
+ expect(value(secret)).toBe("\0".repeat("https://example.com".length))
87
+ })
88
+ })
89
+
90
+ describe("non-URL strings", () => {
91
+ const nonUrls = [
92
+ "just-a-secret-token",
93
+ "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc",
94
+ "sk-1234567890abcdef",
95
+ "some random string with spaces and symbols!@#$%"
96
+ ]
97
+
98
+ test.each(nonUrls)("preserves non-URL value: %s", (input) => {
99
+ const secret = fromString(input)
100
+ expect(value(secret)).toBe(input)
101
+ expect(String(secret)).toBe("SecretURL(unknown://<redacted>)")
102
+ expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "unknown" })
103
+ })
104
+ })
105
+
106
+ describe("special characters", () => {
107
+ test("preserves URLs with encoded characters", () => {
108
+ const url = "https://example.com/path?q=hello%20world&name=%C3%A9"
109
+ expect(value(fromString(url))).toBe(url)
110
+ })
111
+
112
+ test("preserves URLs with unicode", () => {
113
+ const url = "https://example.com/café"
114
+ expect(value(fromString(url))).toBe(url)
115
+ })
116
+
117
+ test("preserves URLs with special query params", () => {
118
+ const url = "https://example.com?a=1&b=2&c=foo+bar"
119
+ expect(value(fromString(url))).toBe(url)
120
+ })
121
+ })
122
+
123
+ describe("secretURL config", () => {
124
+ const run = <A>(config: Config.Config<A>, provider: ConfigProvider.ConfigProvider) =>
125
+ Effect.runSync(config.parse(provider))
126
+
127
+ test("reads from env-style ConfigProvider (QUEUE_URL)", () => {
128
+ const url = "https://sqs.us-east-1.amazonaws.com/123456789/my-queue"
129
+ const provider = ConfigProvider.fromEnv({ env: { QUEUE_URL: url } })
130
+ const result = run(secretURL("QUEUE_URL"), provider)
131
+ expect(value(result)).toBe(url)
132
+ expect(String(result)).toBe("SecretURL(https://<redacted>)")
133
+ })
134
+
135
+ test("reads from fromUnknown with nested config ({ queue: { url } })", () => {
136
+ const url = "redis://default:token@cache.example.com:6379"
137
+ const provider = ConfigProvider.fromUnknown({ queue: { url } })
138
+ const config = secretURL("url").pipe(Config.nested("queue"))
139
+ const result = run(config, provider)
140
+ expect(value(result)).toBe(url)
141
+ expect(String(result)).toBe("SecretURL(redis://<redacted>)")
142
+ })
143
+
144
+ test("reads non-URL secret from env", () => {
145
+ const token = "sk-1234567890abcdef"
146
+ const provider = ConfigProvider.fromEnv({ env: { API_KEY: token } })
147
+ const result = run(secretURL("API_KEY"), provider)
148
+ expect(value(result)).toBe(token)
149
+ expect(String(result)).toBe("SecretURL(unknown://<redacted>)")
150
+ })
151
+
152
+ test("rejects empty string", () => {
153
+ const provider = ConfigProvider.fromEnv({ env: { QUEUE_URL: "" } })
154
+ expect(() => run(secretURL("QUEUE_URL"), provider)).toThrow()
155
+ })
156
+ })
157
+ })