effect-app 4.0.0-beta.33 → 4.0.0-beta.331

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 (369) hide show
  1. package/CHANGELOG.md +1628 -0
  2. package/dist/Array.d.ts +2 -1
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +4 -4
  5. package/dist/Chunk.d.ts.map +1 -1
  6. package/dist/Config/SecretURL.d.ts +3 -1
  7. package/dist/Config/SecretURL.d.ts.map +1 -1
  8. package/dist/Config/SecretURL.js +3 -6
  9. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  10. package/dist/Config/internal/configSecretURL.js +2 -2
  11. package/dist/Config.d.ts +2 -0
  12. package/dist/Config.d.ts.map +1 -0
  13. package/dist/Config.js +8 -0
  14. package/dist/ConfigProvider.d.ts +2 -0
  15. package/dist/ConfigProvider.d.ts.map +1 -0
  16. package/dist/ConfigProvider.js +6 -0
  17. package/dist/{ServiceMap.d.ts → Context.d.ts} +18 -20
  18. package/dist/Context.d.ts.map +1 -0
  19. package/dist/Context.js +67 -0
  20. package/dist/DataDependencies.d.ts +136 -0
  21. package/dist/DataDependencies.d.ts.map +1 -0
  22. package/dist/DataDependencies.js +112 -0
  23. package/dist/Effect.d.ts +10 -9
  24. package/dist/Effect.d.ts.map +1 -1
  25. package/dist/Effect.js +6 -9
  26. package/dist/Emailer.d.ts +51 -0
  27. package/dist/Emailer.d.ts.map +1 -0
  28. package/dist/Emailer.js +7 -0
  29. package/dist/Function.d.ts.map +1 -1
  30. package/dist/Layer.d.ts +10 -6
  31. package/dist/Layer.d.ts.map +1 -1
  32. package/dist/Layer.js +3 -2
  33. package/dist/Model/Repository/Registry.d.ts +22 -0
  34. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  35. package/dist/Model/Repository/Registry.js +18 -0
  36. package/dist/Model/Repository/dependency.d.ts +6 -0
  37. package/dist/Model/Repository/dependency.d.ts.map +1 -0
  38. package/dist/Model/Repository/dependency.js +29 -0
  39. package/dist/Model/Repository/ext.d.ts +60 -0
  40. package/dist/Model/Repository/ext.d.ts.map +1 -0
  41. package/dist/Model/Repository/ext.js +124 -0
  42. package/dist/Model/Repository/internal/internal.d.ts +68 -0
  43. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  44. package/dist/Model/Repository/internal/internal.js +611 -0
  45. package/dist/Model/Repository/legacy.d.ts +21 -0
  46. package/dist/Model/Repository/legacy.d.ts.map +1 -0
  47. package/dist/Model/Repository/legacy.js +2 -0
  48. package/dist/Model/Repository/makeRepo.d.ts +71 -0
  49. package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
  50. package/dist/Model/Repository/makeRepo.js +30 -0
  51. package/dist/Model/Repository/service.d.ts +121 -0
  52. package/dist/Model/Repository/service.d.ts.map +1 -0
  53. package/dist/Model/Repository/service.js +2 -0
  54. package/dist/Model/Repository/validation.d.ts +72 -0
  55. package/dist/Model/Repository/validation.d.ts.map +1 -0
  56. package/dist/Model/Repository/validation.js +39 -0
  57. package/dist/Model/Repository.d.ts +8 -0
  58. package/dist/Model/Repository.d.ts.map +1 -0
  59. package/dist/Model/Repository.js +8 -0
  60. package/dist/Model/dsl.d.ts +33 -0
  61. package/dist/Model/dsl.d.ts.map +1 -0
  62. package/dist/Model/dsl.js +43 -0
  63. package/dist/Model/filter/filterApi.d.ts +30 -0
  64. package/dist/Model/filter/filterApi.d.ts.map +1 -0
  65. package/dist/Model/filter/filterApi.js +2 -0
  66. package/dist/Model/filter/types/errors.d.ts +29 -0
  67. package/dist/Model/filter/types/errors.d.ts.map +1 -0
  68. package/dist/Model/filter/types/errors.js +2 -0
  69. package/dist/Model/filter/types/fields.d.ts +15 -0
  70. package/dist/Model/filter/types/fields.d.ts.map +1 -0
  71. package/dist/Model/filter/types/fields.js +2 -0
  72. package/dist/Model/filter/types/path/common.d.ts +316 -0
  73. package/dist/Model/filter/types/path/common.d.ts.map +1 -0
  74. package/dist/Model/filter/types/path/common.js +2 -0
  75. package/dist/Model/filter/types/path/eager.d.ts +94 -0
  76. package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
  77. package/dist/Model/filter/types/path/eager.js +36 -0
  78. package/dist/Model/filter/types/path/index.d.ts +4 -0
  79. package/dist/Model/filter/types/path/index.d.ts.map +1 -0
  80. package/dist/Model/filter/types/path/index.js +3 -0
  81. package/dist/Model/filter/types/utils.d.ts +79 -0
  82. package/dist/Model/filter/types/utils.d.ts.map +1 -0
  83. package/dist/Model/filter/types/utils.js +2 -0
  84. package/dist/Model/filter/types/validator.d.ts +30 -0
  85. package/dist/Model/filter/types/validator.d.ts.map +1 -0
  86. package/dist/Model/filter/types/validator.js +2 -0
  87. package/dist/Model/filter/types.d.ts +5 -0
  88. package/dist/Model/filter/types.d.ts.map +1 -0
  89. package/dist/Model/filter/types.js +7 -0
  90. package/dist/Model/query/dsl.d.ts +630 -0
  91. package/dist/Model/query/dsl.d.ts.map +1 -0
  92. package/dist/Model/query/dsl.js +376 -0
  93. package/dist/Model/query/new-kid-interpreter.d.ts +132 -0
  94. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
  95. package/dist/Model/query/new-kid-interpreter.js +370 -0
  96. package/dist/Model/query.d.ts +15 -0
  97. package/dist/Model/query.d.ts.map +1 -0
  98. package/dist/Model/query.js +3 -0
  99. package/dist/Model.d.ts +6 -0
  100. package/dist/Model.d.ts.map +1 -0
  101. package/dist/Model.js +6 -0
  102. package/dist/NonEmptySet.d.ts +3 -1
  103. package/dist/NonEmptySet.d.ts.map +1 -1
  104. package/dist/NonEmptySet.js +2 -2
  105. package/dist/Option.d.ts +1 -0
  106. package/dist/Option.d.ts.map +1 -1
  107. package/dist/Option.js +3 -1
  108. package/dist/Pure.d.ts +7 -5
  109. package/dist/Pure.d.ts.map +1 -1
  110. package/dist/Pure.js +7 -4
  111. package/dist/QueueMaker.d.ts +11 -0
  112. package/dist/QueueMaker.d.ts.map +1 -0
  113. package/dist/QueueMaker.js +11 -0
  114. package/dist/RequestContext.d.ts +87 -0
  115. package/dist/RequestContext.d.ts.map +1 -0
  116. package/dist/RequestContext.js +45 -0
  117. package/dist/RequestScopedDependencies.d.ts +21 -0
  118. package/dist/RequestScopedDependencies.d.ts.map +1 -0
  119. package/dist/RequestScopedDependencies.js +16 -0
  120. package/dist/Schema/Class.d.ts +200 -20
  121. package/dist/Schema/Class.d.ts.map +1 -1
  122. package/dist/Schema/Class.js +268 -17
  123. package/dist/Schema/FastCheck.d.ts.map +1 -1
  124. package/dist/Schema/SchemaParser.d.ts +5 -0
  125. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  126. package/dist/Schema/SchemaParser.js +6 -0
  127. package/dist/Schema/SpecialJsonSchema.d.ts +34 -0
  128. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  129. package/dist/Schema/SpecialJsonSchema.js +117 -0
  130. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  131. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  132. package/dist/Schema/SpecialOpenApi.js +123 -0
  133. package/dist/Schema/brand.d.ts +5 -5
  134. package/dist/Schema/brand.d.ts.map +1 -1
  135. package/dist/Schema/brand.js +3 -1
  136. package/dist/Schema/email.d.ts.map +1 -1
  137. package/dist/Schema/email.js +12 -6
  138. package/dist/Schema/ext.d.ts +335 -61
  139. package/dist/Schema/ext.d.ts.map +1 -1
  140. package/dist/Schema/ext.js +388 -63
  141. package/dist/Schema/moreStrings.d.ts +82 -36
  142. package/dist/Schema/moreStrings.d.ts.map +1 -1
  143. package/dist/Schema/moreStrings.js +90 -53
  144. package/dist/Schema/numbers.d.ts +33 -21
  145. package/dist/Schema/numbers.d.ts.map +1 -1
  146. package/dist/Schema/numbers.js +65 -12
  147. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  148. package/dist/Schema/phoneNumber.js +11 -7
  149. package/dist/Schema/schema.d.ts +1 -1
  150. package/dist/Schema/schema.d.ts.map +1 -1
  151. package/dist/Schema/schema.js +2 -2
  152. package/dist/Schema/strings.d.ts +18 -4
  153. package/dist/Schema/strings.d.ts.map +1 -1
  154. package/dist/Schema/strings.js +1 -5
  155. package/dist/Schema.d.ts +225 -7
  156. package/dist/Schema.d.ts.map +1 -1
  157. package/dist/Schema.js +193 -26
  158. package/dist/SchemaAST.d.ts +2 -0
  159. package/dist/SchemaAST.d.ts.map +1 -0
  160. package/dist/SchemaAST.js +2 -0
  161. package/dist/Set.d.ts +4 -1
  162. package/dist/Set.d.ts.map +1 -1
  163. package/dist/Set.js +3 -2
  164. package/dist/Store.d.ts +215 -0
  165. package/dist/Store.d.ts.map +1 -0
  166. package/dist/Store.js +113 -0
  167. package/dist/_ext/Array.d.ts +1 -1
  168. package/dist/_ext/Array.d.ts.map +1 -1
  169. package/dist/_ext/Array.js +4 -2
  170. package/dist/_ext/misc.d.ts +4 -1
  171. package/dist/_ext/misc.d.ts.map +1 -1
  172. package/dist/_ext/misc.js +4 -2
  173. package/dist/_ext/ord.ext.d.ts +2 -1
  174. package/dist/_ext/ord.ext.d.ts.map +1 -1
  175. package/dist/_ext/ord.ext.js +2 -2
  176. package/dist/builtin.d.ts +2 -14
  177. package/dist/builtin.d.ts.map +1 -1
  178. package/dist/builtin.js +1 -1
  179. package/dist/client/InvalidationKeys.d.ts +30 -0
  180. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  181. package/dist/client/InvalidationKeys.js +37 -0
  182. package/dist/client/apiClientFactory.d.ts +19 -31
  183. package/dist/client/apiClientFactory.d.ts.map +1 -1
  184. package/dist/client/apiClientFactory.js +157 -39
  185. package/dist/client/clientFor.d.ts +54 -18
  186. package/dist/client/clientFor.d.ts.map +1 -1
  187. package/dist/client/clientFor.js +9 -1
  188. package/dist/client/errors.d.ts +82 -27
  189. package/dist/client/errors.d.ts.map +1 -1
  190. package/dist/client/errors.js +75 -19
  191. package/dist/client/makeClient.d.ts +490 -34
  192. package/dist/client/makeClient.d.ts.map +1 -1
  193. package/dist/client/makeClient.js +62 -30
  194. package/dist/client.d.ts +2 -0
  195. package/dist/client.d.ts.map +1 -1
  196. package/dist/client.js +3 -1
  197. package/dist/faker.d.ts.map +1 -1
  198. package/dist/http/Request.d.ts +1 -1
  199. package/dist/http/Request.d.ts.map +1 -1
  200. package/dist/http/Request.js +2 -2
  201. package/dist/ids.d.ts +32 -14
  202. package/dist/ids.d.ts.map +1 -1
  203. package/dist/ids.js +38 -6
  204. package/dist/index.d.ts +6 -7
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +8 -8
  207. package/dist/middleware.d.ts +13 -7
  208. package/dist/middleware.d.ts.map +1 -1
  209. package/dist/middleware.js +14 -8
  210. package/dist/rpc/Invalidation.d.ts +1124 -0
  211. package/dist/rpc/Invalidation.d.ts.map +1 -0
  212. package/dist/rpc/Invalidation.js +178 -0
  213. package/dist/rpc/MiddlewareMaker.d.ts +11 -7
  214. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  215. package/dist/rpc/MiddlewareMaker.js +59 -38
  216. package/dist/rpc/RpcContextMap.d.ts +3 -3
  217. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  218. package/dist/rpc/RpcContextMap.js +4 -4
  219. package/dist/rpc/RpcMiddleware.d.ts +14 -10
  220. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  221. package/dist/rpc/RpcMiddleware.js +1 -1
  222. package/dist/rpc.d.ts +1 -1
  223. package/dist/rpc.d.ts.map +1 -1
  224. package/dist/rpc.js +2 -2
  225. package/dist/runtime.d.ts +19 -0
  226. package/dist/runtime.d.ts.map +1 -0
  227. package/dist/runtime.js +40 -0
  228. package/dist/setupRequest.d.ts +20 -0
  229. package/dist/setupRequest.d.ts.map +1 -0
  230. package/dist/setupRequest.js +72 -0
  231. package/dist/toast.d.ts +51 -0
  232. package/dist/toast.d.ts.map +1 -0
  233. package/dist/toast.js +34 -0
  234. package/dist/transform.d.ts +1 -1
  235. package/dist/transform.d.ts.map +1 -1
  236. package/dist/transform.js +4 -5
  237. package/dist/utils/effectify.d.ts +1 -1
  238. package/dist/utils/effectify.d.ts.map +1 -1
  239. package/dist/utils/effectify.js +2 -2
  240. package/dist/utils/extend.d.ts.map +1 -1
  241. package/dist/utils/gen.d.ts +4 -4
  242. package/dist/utils/gen.d.ts.map +1 -1
  243. package/dist/utils/logLevel.d.ts +2 -2
  244. package/dist/utils/logLevel.d.ts.map +1 -1
  245. package/dist/utils/logger.d.ts +4 -3
  246. package/dist/utils/logger.d.ts.map +1 -1
  247. package/dist/utils/logger.js +4 -4
  248. package/dist/utils.d.ts +34 -39
  249. package/dist/utils.d.ts.map +1 -1
  250. package/dist/utils.js +33 -37
  251. package/dist/validation/validators.d.ts.map +1 -1
  252. package/dist/validation.d.ts.map +1 -1
  253. package/dist/withToast.d.ts +30 -0
  254. package/dist/withToast.d.ts.map +1 -0
  255. package/dist/withToast.js +64 -0
  256. package/package.json +27 -251
  257. package/src/Array.ts +5 -5
  258. package/src/Chunk.ts +3 -3
  259. package/src/Config/SecretURL.ts +6 -3
  260. package/src/Config/internal/configSecretURL.ts +2 -2
  261. package/src/Config.ts +7 -0
  262. package/src/ConfigProvider.ts +5 -0
  263. package/src/{ServiceMap.ts → Context.ts} +56 -63
  264. package/src/DataDependencies.ts +171 -0
  265. package/src/Effect.ts +15 -17
  266. package/src/Emailer.ts +51 -0
  267. package/src/Inputify.type.ts +1 -1
  268. package/src/Layer.ts +11 -7
  269. package/src/Model/Repository/Registry.ts +35 -0
  270. package/src/Model/Repository/dependency.ts +32 -0
  271. package/src/Model/Repository/ext.ts +374 -0
  272. package/src/Model/Repository/internal/internal.ts +988 -0
  273. package/src/Model/Repository/legacy.ts +29 -0
  274. package/src/Model/Repository/makeRepo.ts +168 -0
  275. package/src/Model/Repository/service.ts +676 -0
  276. package/src/Model/Repository/validation.ts +38 -0
  277. package/src/Model/Repository.ts +7 -0
  278. package/src/Model/dsl.ts +129 -0
  279. package/src/Model/filter/filterApi.ts +78 -0
  280. package/src/Model/filter/types/errors.ts +47 -0
  281. package/src/Model/filter/types/fields.ts +50 -0
  282. package/src/Model/filter/types/path/common.ts +404 -0
  283. package/src/Model/filter/types/path/eager.ts +329 -0
  284. package/src/Model/filter/types/path/index.ts +4 -0
  285. package/src/Model/filter/types/utils.ts +128 -0
  286. package/src/Model/filter/types/validator.ts +46 -0
  287. package/src/Model/filter/types.ts +6 -0
  288. package/src/Model/query/dsl.ts +3092 -0
  289. package/src/Model/query/new-kid-interpreter.ts +532 -0
  290. package/src/Model/query.ts +13 -0
  291. package/src/Model.ts +5 -0
  292. package/src/NonEmptySet.ts +6 -4
  293. package/src/Option.ts +2 -0
  294. package/src/Pure.ts +10 -7
  295. package/src/QueueMaker.ts +11 -0
  296. package/src/RequestContext.ts +48 -0
  297. package/src/RequestScopedDependencies.ts +45 -0
  298. package/src/Schema/Class.ts +648 -60
  299. package/src/Schema/SchemaParser.ts +12 -0
  300. package/src/Schema/SpecialJsonSchema.ts +137 -0
  301. package/src/Schema/SpecialOpenApi.ts +130 -0
  302. package/src/Schema/brand.ts +24 -9
  303. package/src/Schema/email.ts +14 -7
  304. package/src/Schema/ext.ts +508 -102
  305. package/src/Schema/moreStrings.ts +217 -86
  306. package/src/Schema/numbers.ts +107 -28
  307. package/src/Schema/phoneNumber.ts +13 -9
  308. package/src/Schema/schema.ts +1 -1
  309. package/src/Schema/strings.ts +23 -14
  310. package/src/Schema.ts +403 -46
  311. package/src/SchemaAST.ts +1 -0
  312. package/src/Set.ts +6 -2
  313. package/src/Store.ts +325 -0
  314. package/src/_ext/Array.ts +4 -2
  315. package/src/_ext/misc.ts +4 -1
  316. package/src/_ext/ord.ext.ts +2 -1
  317. package/src/builtin-json.d.ts +24 -0
  318. package/src/builtin.ts +16 -21
  319. package/src/client/InvalidationKeys.ts +55 -0
  320. package/src/client/apiClientFactory.ts +301 -141
  321. package/src/client/clientFor.ts +107 -34
  322. package/src/client/errors.ts +100 -29
  323. package/src/client/makeClient.ts +594 -80
  324. package/src/client.ts +6 -4
  325. package/src/http/Request.ts +3 -3
  326. package/src/http.ts +1 -1
  327. package/src/ids.ts +45 -7
  328. package/src/index.ts +20 -23
  329. package/src/middleware.ts +13 -9
  330. package/src/rpc/Invalidation.ts +278 -0
  331. package/src/rpc/MiddlewareMaker.ts +88 -80
  332. package/src/rpc/README.md +2 -2
  333. package/src/rpc/RpcContextMap.ts +7 -6
  334. package/src/rpc/RpcMiddleware.ts +19 -13
  335. package/src/rpc.ts +4 -4
  336. package/src/runtime.ts +57 -0
  337. package/src/setupRequest.ts +138 -0
  338. package/src/toast.ts +54 -0
  339. package/src/transform.ts +4 -4
  340. package/src/utils/effectify.ts +1 -1
  341. package/src/utils/gen.ts +8 -8
  342. package/src/utils/logLevel.ts +1 -1
  343. package/src/utils/logger.ts +4 -3
  344. package/src/utils.ts +85 -158
  345. package/src/validation.ts +2 -2
  346. package/src/withToast.ts +133 -0
  347. package/test/dist/rpc.test.d.ts.map +1 -1
  348. package/test/dist/rpcMiddlewareContext.test.d.ts.map +1 -0
  349. package/test/dist/secretURL.test.d.ts.map +1 -0
  350. package/test/dist/special.test.d.ts.map +1 -0
  351. package/test/moreStrings.test.ts +55 -10
  352. package/test/rpc.test.ts +38 -10
  353. package/test/rpcMiddlewareContext.test.ts +63 -0
  354. package/test/schema.test.ts +505 -26
  355. package/test/secretURL.test.ts +160 -0
  356. package/test/special.test.ts +1261 -0
  357. package/test/utils.test.ts +7 -7
  358. package/tsconfig.base.json +6 -5
  359. package/tsconfig.json +9 -2
  360. package/tsconfig.src.json +29 -29
  361. package/tsconfig.test.json +2 -2
  362. package/dist/Operations.d.ts +0 -123
  363. package/dist/Operations.d.ts.map +0 -1
  364. package/dist/Operations.js +0 -29
  365. package/dist/ServiceMap.d.ts.map +0 -1
  366. package/dist/ServiceMap.js +0 -91
  367. package/eslint.config.mjs +0 -26
  368. package/src/Operations.ts +0 -55
  369. package/tsconfig.json.bak +0 -12
@@ -1,6 +1,10 @@
1
1
  // import { generateFromArbitrary } from "@effect-app/infra/test"
2
- import { Array, S } from "effect-app"
3
- import { expect, expectTypeOf, test } from "vitest"
2
+ import * as Array from "effect-app/Array"
3
+ import * as S from "effect-app/Schema"
4
+ import { specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
5
+ import * as Effect from "effect/Effect"
6
+ import * as Struct from "effect/Struct"
7
+ import { describe, expect, expectTypeOf, test } from "vitest"
4
8
 
5
9
  const A = S.Struct({ a: S.NonEmptyString255, email: S.NullOr(S.Email) })
6
10
  test("works", () => {
@@ -14,22 +18,170 @@ test("works", () => {
14
18
  })
15
19
 
16
20
  test("literal default works", () => {
17
- const l = S.Literal("a", "b")
21
+ const l = S.Literals(["a", "b"])
18
22
  expect(l.Default).toBe("a")
19
- const s = S.Struct({ l: l.withDefault })
20
- expect(s.makeUnsafe({}).l).toBe("a")
23
+ expectTypeOf(l.Default).toEqualTypeOf<"a">()
24
+ const s = S.Struct({ l: l.withConstructorDefault })
25
+ expect(s.make({}).l).toBe("a")
21
26
 
22
27
  const l2 = l.changeDefault("b")
23
- const s2 = S.Struct({ l: l2.withDefault })
24
- expect(s2.makeUnsafe({}).l).toBe("b")
28
+ const s2 = S.Struct({ l: l2.withConstructorDefault })
29
+ expect(s2.make({}).l).toBe("b")
30
+ })
31
+
32
+ // Toy schema for `dropConstructorDefault`: a field with a constructor
33
+ // default that should NOT auto-populate when deriving a partial-update
34
+ // schema from a "create" schema.
35
+ const Person = S.Struct({
36
+ name: S.String,
37
+ notes: S.NullOr(S.String).pipe(S.optionalKey, S.withConstructorDefault(Effect.succeed(null)))
38
+ })
39
+
40
+ test("plain Struct.omit leaks the inherited constructor default (the bug)", () => {
41
+ const UpdatePersonBuggy = S.Struct(Struct.omit(Person.fields, [] as const))
42
+ expect(UpdatePersonBuggy.make({ name: "Mario" })).toEqual({ name: "Mario", notes: null })
43
+ })
44
+
45
+ test("Struct.map(S.dropConstructorDefault) strips the inherited constructor default (the fix)", () => {
46
+ const UpdatePersonFixed = S.Struct(Struct.map(Struct.omit(Person.fields, [] as const), S.dropConstructorDefault))
47
+ expect(UpdatePersonFixed.make({ name: "Mario" })).toEqual({ name: "Mario" })
48
+ })
49
+
50
+ test("Struct.map(S.dropConstructorDefault) still omits the requested keys, like Struct.omit", () => {
51
+ const NameOnly = S.Struct(Struct.map(Struct.omit(Person.fields, ["notes"] as const), S.dropConstructorDefault))
52
+ expect(NameOnly.make({ name: "Mario" })).toEqual({ name: "Mario" })
53
+ expectTypeOf<typeof NameOnly.Type>().toEqualTypeOf<{ readonly name: string }>()
54
+ })
55
+
56
+ test("makeExactOptional drops the inherited constructor default too", () => {
57
+ const UpdatePerson = S.Struct(S.makeExactOptional(Person.fields))
58
+ expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" })
59
+ })
60
+
61
+ test("dropConstructorDefault stays 'no-default' after annotate/annotateKey (Rebuild fix)", () => {
62
+ const notesDropped = S.dropConstructorDefault(Person.fields.notes)
63
+ expectTypeOf<(typeof notesDropped)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
64
+
65
+ const notesAnnotated = notesDropped.annotate({ description: "no longer defaulted" })
66
+ expectTypeOf<(typeof notesAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
67
+
68
+ const notesKeyAnnotated = notesDropped.annotateKey({ description: "no longer defaulted (key)" })
69
+ expectTypeOf<(typeof notesKeyAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
70
+
71
+ const UpdatePerson = S.Struct({ name: Person.fields.name, notes: notesAnnotated })
72
+ expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" })
73
+ })
74
+
75
+ test("NonEmptyString255.Type uses the named brand alias", () => {
76
+ type A = typeof S.NonEmptyString255.Type
77
+ type B = string & S.NonEmptyString255Brand
78
+ expectTypeOf<A>().toEqualTypeOf<B>()
79
+ })
80
+
81
+ test("Opaque accepts an explicit Encoded type", () => {
82
+ interface User {
83
+ readonly id: string
84
+ readonly _tag: "User"
85
+ }
86
+
87
+ interface UserEncoded {
88
+ readonly id: string
89
+ }
90
+
91
+ const baseSchema = S.Struct({ id: S.String, name: S.String })
92
+ const _UserSchema = S.Opaque<User, UserEncoded>()(baseSchema)
93
+
94
+ expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().toEqualTypeOf<UserEncoded>()
95
+ expectTypeOf<S.Schema.Type<typeof _UserSchema>>().toEqualTypeOf<User>()
96
+ expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().not.toEqualTypeOf<S.Codec.Encoded<typeof baseSchema>>()
97
+ })
98
+
99
+ test("Opaque with one generic keeps the base encoded shape", () => {
100
+ interface User {
101
+ readonly id: string
102
+ }
103
+
104
+ const baseSchema = S.Struct({ id: S.String })
105
+ const _UserSchema = S.Opaque<User>()(baseSchema)
106
+
107
+ expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().toEqualTypeOf<{ readonly id: string }>()
108
+ expectTypeOf<S.Schema.Type<typeof _UserSchema>>().toEqualTypeOf<User>()
109
+ })
110
+
111
+ test("Opaque preserves optional Struct.make input", () => {
112
+ interface User {
113
+ readonly a?: string | undefined
114
+ readonly b?: number | undefined
115
+ }
116
+
117
+ const schema = S.Opaque<User>()(S.Struct({
118
+ a: S.optional(S.String),
119
+ b: S.optional(S.Number)
120
+ }))
121
+
122
+ const made = schema.make()
123
+ expect(made).toEqual({})
124
+ expectTypeOf(made).toEqualTypeOf<User>()
125
+ })
126
+
127
+ test("Opaque preserves optional TaggedStruct.make input", () => {
128
+ interface OnlyTag {
129
+ readonly _tag: "OnlyTag"
130
+ }
131
+
132
+ const schema = S.Opaque<OnlyTag>()(S.TaggedStruct("OnlyTag", {}))
133
+
134
+ const made = schema.make()
135
+ expect(made).toEqual({ _tag: "OnlyTag" })
136
+ expectTypeOf(made).toEqualTypeOf<OnlyTag>()
137
+ })
138
+
139
+ test("S.Literals([\"A\", \"B\"]).Default is typed as \"A\"", () => {
140
+ const l = S.Literals(["A", "B"])
141
+ expect(l.Default).toBe("A")
142
+ expectTypeOf(l.Default).toEqualTypeOf<"A">()
143
+ })
144
+
145
+ test("Struct.make accepts void when all fields are optional", () => {
146
+ const schema = S.Struct({
147
+ a: S.optional(S.String),
148
+ b: S.optional(S.Number)
149
+ })
150
+
151
+ const made = schema.make()
152
+ expect(made).toEqual({})
153
+ expectTypeOf(made).toEqualTypeOf<{ readonly a?: string | undefined; readonly b?: number | undefined }>()
154
+ })
155
+
156
+ test("StructNestedEncoded resolves Struct and from.Encoded", () => {
157
+ const plain = S.Struct({ a: S.String, b: S.NullOr(S.String) })
158
+ const encodedKeys = plain.pipe(S.encodeKeys({ b: "b_encoded" }))
159
+
160
+ expectTypeOf<S.StructNestedEncoded<typeof plain>>().toEqualTypeOf<{
161
+ readonly a: string
162
+ readonly b: string | null
163
+ }>()
164
+
165
+ expectTypeOf<S.StructNestedEncoded<typeof encodedKeys>>().toEqualTypeOf<{
166
+ readonly a: string
167
+ readonly b_encoded: string | null
168
+ }>()
169
+ })
170
+
171
+ test("TaggedStruct.make accepts void when only constructor-default fields exist", () => {
172
+ const schema = S.TaggedStruct("OnlyTag", {})
173
+
174
+ const made = schema.make()
175
+ expect(made).toEqual({ _tag: "OnlyTag" })
176
+ expectTypeOf(made).toEqualTypeOf<{ readonly _tag: "OnlyTag" }>()
25
177
  })
26
178
 
27
179
  test("tagged union derives tag map and tags from v4 literal ast", () => {
28
- const schema = S.TaggedUnion(
180
+ const schema = S.TaggedUnion([
29
181
  S.TaggedStruct("A", { a: S.String }),
30
182
  S.TaggedStruct("B", { b: S.Finite }),
31
183
  S.TaggedStruct("C", { c: S.Boolean })
32
- )
184
+ ])
33
185
  const caseA = schema.cases["A"]
34
186
  const caseB = schema.cases["B"]
35
187
  const caseC = schema.cases["C"]
@@ -56,21 +208,21 @@ test("tagged union derives tag map and tags from v4 literal ast", () => {
56
208
  })
57
209
 
58
210
  test("TaggedUnion tags returns a Literals schema with correct literal values", () => {
59
- const schema = S.TaggedUnion(
211
+ const schema = S.TaggedUnion([
60
212
  S.TaggedStruct("X", { x: S.String }),
61
213
  S.TaggedStruct("Y", { y: S.Finite })
62
- )
214
+ ])
63
215
 
64
216
  expect(schema.tags.literals).toEqual(["X", "Y"])
65
217
  expectTypeOf(schema.tags.literals).toMatchTypeOf<readonly ["X", "Y"]>()
66
218
  })
67
219
 
68
220
  test("TaggedUnion tags.pick returns a subset of the tag literals", () => {
69
- const schema = S.TaggedUnion(
221
+ const schema = S.TaggedUnion([
70
222
  S.TaggedStruct("A", { a: S.String }),
71
223
  S.TaggedStruct("B", { b: S.Finite }),
72
224
  S.TaggedStruct("C", { c: S.Boolean })
73
- )
225
+ ])
74
226
 
75
227
  const subset = schema.tags.pick(["A", "C"])
76
228
  expect(subset.literals).toEqual(["A", "C"])
@@ -109,24 +261,23 @@ test("ExtendTaggedUnion adds tags to an existing Union", () => {
109
261
  })
110
262
 
111
263
  test("TaggedUnion match dispatches on _tag", () => {
112
- const schema = S.TaggedUnion(
264
+ const schema = S.TaggedUnion([
113
265
  S.TaggedStruct("A", { a: S.String }),
114
266
  S.TaggedStruct("B", { b: S.Finite })
115
- )
116
- type T = S.Schema.Type<typeof schema>
267
+ ])
117
268
 
118
269
  const matcher = schema.match({
119
270
  A: (v) => `got A: ${v.a}`,
120
271
  B: (v) => `got B: ${v.b}`
121
272
  })
122
- expect(matcher({ _tag: "A", a: "hello" } as T)).toBe("got A: hello")
123
- expect(matcher({ _tag: "B", b: 42 } as T)).toBe("got B: 42")
273
+ expect(matcher({ _tag: "A", a: "hello" })).toBe("got A: hello")
274
+ expect(matcher({ _tag: "B", b: 42 })).toBe("got B: 42")
124
275
  })
125
276
 
126
277
  test("TaggedUnion with single member", () => {
127
- const schema = S.TaggedUnion(
278
+ const schema = S.TaggedUnion([
128
279
  S.TaggedStruct("Only", { val: S.String })
129
- )
280
+ ])
130
281
 
131
282
  expect(schema.tags.literals).toEqual(["Only"])
132
283
  expect(S.decodeSync(schema.tags)("Only")).toBe("Only")
@@ -135,11 +286,11 @@ test("TaggedUnion with single member", () => {
135
286
  })
136
287
 
137
288
  test("TaggedUnion tags type is narrowed to the exact tag literals", () => {
138
- const schema = S.TaggedUnion(
289
+ const schema = S.TaggedUnion([
139
290
  S.TaggedStruct("Alpha", { a: S.String }),
140
291
  S.TaggedStruct("Beta", { b: S.Finite }),
141
292
  S.TaggedStruct("Gamma", { c: S.Boolean })
142
- )
293
+ ])
143
294
 
144
295
  type Tags = S.Schema.Type<typeof schema.tags>
145
296
  expectTypeOf<Tags>().toEqualTypeOf<"Alpha" | "Beta" | "Gamma">()
@@ -153,7 +304,7 @@ test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
153
304
  S.encodeKeys({ lastName: "last_name" })
154
305
  )
155
306
 
156
- const schema = S.TaggedUnion(MemberA, MemberB)
307
+ const schema = S.TaggedUnion([MemberA, MemberB])
157
308
 
158
309
  expect(schema.tags.literals).toEqual(["A", "B"])
159
310
  expect(S.decodeSync(schema.tags)("A")).toBe("A")
@@ -167,8 +318,7 @@ test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
167
318
  expect(decoded2).toEqual({ _tag: "B", lastName: 42 })
168
319
 
169
320
  // encode back to snake_case
170
- type T = S.Schema.Type<typeof schema>
171
- const encoded = S.encodeSync(schema)({ _tag: "A", firstName: "Alice" } as T)
321
+ const encoded = S.encodeSync(schema)({ _tag: "A", firstName: "Alice" })
172
322
  expect(encoded).toEqual({ _tag: "A", first_name: "Alice" })
173
323
 
174
324
  // guards work on decoded values
@@ -181,7 +331,7 @@ test("TaggedUnion with TaggedClass members", () => {
181
331
  class Foo extends S.TaggedClass<Foo>()("Foo", { name: S.String }) {}
182
332
  class Bar extends S.TaggedClass<Bar>()("Bar", { count: S.Finite }) {}
183
333
 
184
- const schema = S.TaggedUnion(Foo, Bar)
334
+ const schema = S.TaggedUnion([Foo, Bar])
185
335
 
186
336
  expect(schema.tags.literals).toEqual(["Foo", "Bar"])
187
337
  expect(S.decodeSync(schema.tags)("Foo")).toBe("Foo")
@@ -200,3 +350,332 @@ test("TaggedUnion with TaggedClass members", () => {
200
350
  expect(schema.guards.Foo(new Bar({ count: 3 }))).toBe(false)
201
351
  expect(schema.guards.Bar(new Bar({ count: 3 }))).toBe(true)
202
352
  })
353
+
354
+ describe("ReadonlySetFromArray", () => {
355
+ test("decodes an array of strings to a Set", () => {
356
+ const schema = S.ReadonlySetFromArray(S.String)
357
+ const decoded = S.decodeUnknownSync(schema)(["a", "b", "c"])
358
+ expect(decoded).toEqual(new Set(["a", "b", "c"]))
359
+ })
360
+
361
+ test("encodes a Set back to an array", () => {
362
+ const schema = S.ReadonlySetFromArray(S.String)
363
+ const encoded = S.encodeSync(schema)(new Set(["a", "b"]))
364
+ expect(encoded).toEqual(["a", "b"])
365
+ })
366
+
367
+ test("decodes with NumberFromString as value", () => {
368
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
369
+ const decoded = S.decodeUnknownSync(schema)(["1", "2", "3"])
370
+ expect(decoded).toEqual(new Set([1, 2, 3]))
371
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlySet<number>>()
372
+ })
373
+
374
+ test("encodes with NumberFromString as value", () => {
375
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
376
+ const encoded = S.encodeSync(schema)(new Set([1, 2, 3]))
377
+ expect(encoded).toEqual(["1", "2", "3"])
378
+ })
379
+
380
+ test("rejects invalid input", () => {
381
+ const schema = S.ReadonlySetFromArray(S.NumberFromString)
382
+ expect(() => S.decodeUnknownSync(schema)([1, 2])).toThrow()
383
+ })
384
+ })
385
+
386
+ describe("ReadonlyMapFromArray", () => {
387
+ test("decodes an array of tuples to a Map", () => {
388
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })
389
+ const decoded = S.decodeUnknownSync(schema)([["a", 1], ["b", 2]])
390
+ expect(decoded).toEqual(new Map([["a", 1], ["b", 2]]))
391
+ })
392
+
393
+ test("encodes a Map back to an array of tuples", () => {
394
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })
395
+ const encoded = S.encodeSync(schema)(new Map([["a", 1], ["b", 2]]))
396
+ expect(encoded).toEqual([["a", 1], ["b", 2]])
397
+ })
398
+
399
+ test("decodes with NumberFromString as key", () => {
400
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
401
+ const decoded = S.decodeUnknownSync(schema)([["1", "one"], ["2", "two"]])
402
+ expect(decoded).toEqual(new Map([[1, "one"], [2, "two"]]))
403
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, string>>()
404
+ })
405
+
406
+ test("encodes with NumberFromString as key", () => {
407
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
408
+ const encoded = S.encodeSync(schema)(new Map([[1, "one"], [2, "two"]]))
409
+ expect(encoded).toEqual([["1", "one"], ["2", "two"]])
410
+ })
411
+
412
+ test("decodes with NumberFromString as value", () => {
413
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
414
+ const decoded = S.decodeUnknownSync(schema)([["a", "10"], ["b", "20"]])
415
+ expect(decoded).toEqual(new Map([["a", 10], ["b", 20]]))
416
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<string, number>>()
417
+ })
418
+
419
+ test("encodes with NumberFromString as value", () => {
420
+ const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
421
+ const encoded = S.encodeSync(schema)(new Map([["a", 10], ["b", 20]]))
422
+ expect(encoded).toEqual([["a", "10"], ["b", "20"]])
423
+ })
424
+
425
+ test("decodes with NumberFromString as both key and value", () => {
426
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.NumberFromString })
427
+ const decoded = S.decodeUnknownSync(schema)([["1", "10"], ["2", "20"]])
428
+ expect(decoded).toEqual(new Map([[1, 10], [2, 20]]))
429
+ expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, number>>()
430
+ })
431
+
432
+ test("rejects invalid input", () => {
433
+ const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
434
+ expect(() => S.decodeUnknownSync(schema)([[1, "val"]])).toThrow()
435
+ })
436
+ })
437
+
438
+ describe("ReadonlySet (with withConstructorDefault)", () => {
439
+ test("make provides withConstructorDefault", () => {
440
+ const schema = S.ReadonlySet(S.NumberFromString)
441
+ const struct = S.Struct({ items: schema.withConstructorDefault })
442
+ const made = struct.make({})
443
+ expect(made.items).toEqual(new Set())
444
+ })
445
+
446
+ test("decodes a Set with NumberFromString values", () => {
447
+ const schema = S.ReadonlySet(S.NumberFromString)
448
+ const decoded = S.decodeUnknownSync(schema)(new Set(["1", "2"]))
449
+ expect(decoded).toEqual(new Set([1, 2]))
450
+ })
451
+
452
+ test("Encoded is a Set, not an array", () => {
453
+ const schema = S.ReadonlySet(S.String)
454
+ const encoded = S.encodeSync(schema)(new Set(["a"]))
455
+ expect(encoded).toEqual(new Set(["a"]))
456
+ expectTypeOf(encoded).toEqualTypeOf<ReadonlySet<string>>()
457
+ })
458
+ })
459
+
460
+ describe("ReadonlyMap (with withConstructorDefault)", () => {
461
+ test("make provides withConstructorDefault", () => {
462
+ const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
463
+ const struct = S.Struct({ items: schema.withConstructorDefault })
464
+ const made = struct.make({})
465
+ expect(made.items).toEqual(new Map())
466
+ })
467
+
468
+ test("decodes a Map with NumberFromString keys", () => {
469
+ const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
470
+ const decoded = S.decodeUnknownSync(schema)(new Map([["1", "one"]]))
471
+ expect(decoded).toEqual(new Map([[1, "one"]]))
472
+ })
473
+
474
+ test("Encoded is a Map, not an array of tuples", () => {
475
+ const schema = S.ReadonlyMap({ key: S.String, value: S.Finite })
476
+ const encoded = S.encodeSync(schema)(new Map([["a", 1]]))
477
+ expect(encoded).toEqual(new Map([["a", 1]]))
478
+ expectTypeOf(encoded).toEqualTypeOf<ReadonlyMap<string, number>>()
479
+ })
480
+ })
481
+
482
+ describe("JSON Schema", () => {
483
+ test("Email has format, minLength, maxLength", () => {
484
+ const doc = S.toJsonSchemaDocument(S.Email)
485
+ expect(doc).toStrictEqual({
486
+ dialect: "draft-2020-12",
487
+ schema: { "$ref": "#/$defs/Email" },
488
+ definitions: {
489
+ Email: {
490
+ type: "string",
491
+ title: "Email",
492
+ description: "an email according to RFC 5322",
493
+ format: "email",
494
+ minLength: 3,
495
+ maxLength: 998
496
+ }
497
+ }
498
+ })
499
+ })
500
+
501
+ test("Email specialJsonSchemaDocument flattens allOf", () => {
502
+ const doc = specialJsonSchemaDocument(S.Email)
503
+ expect(doc).toStrictEqual({
504
+ dialect: "draft-2020-12",
505
+ schema: { "$ref": "#/$defs/Email" },
506
+ definitions: {
507
+ Email: {
508
+ type: "string",
509
+ title: "Email",
510
+ description: "an email according to RFC 5322",
511
+ format: "email",
512
+ minLength: 3,
513
+ maxLength: 998
514
+ }
515
+ }
516
+ })
517
+ })
518
+
519
+ test("Date Encoded is Date; JSON codec encodes ISO strings", () => {
520
+ const d = new Date("2024-01-01T00:00:00.000Z")
521
+ expect(S.decodeUnknownSync(S.Date)(d)).toBe(d)
522
+ expect(S.encodeSync(S.Date)(d)).toBe(d)
523
+ expect(S.encodeSync(S.toCodecJson(S.Date))(d)).toBe("2024-01-01T00:00:00.000Z")
524
+ const doc = S.toJsonSchemaDocument(S.Date)
525
+ expect(doc.dialect).toBe("draft-2020-12")
526
+ expect(doc.schema).toEqual({ type: "string" })
527
+ })
528
+
529
+ test("DateFromString keeps string Encoded", () => {
530
+ const doc = S.toJsonSchemaDocument(S.DateFromString)
531
+ expect(doc).toStrictEqual({
532
+ dialect: "draft-2020-12",
533
+ schema: { "$ref": "#/$defs/DateOrInvalid" },
534
+ definitions: {
535
+ DateOrInvalid: {
536
+ type: "string",
537
+ description: "an ISO 8601 date string that will be decoded as a Date (may be invalid)",
538
+ format: "date-time"
539
+ }
540
+ }
541
+ })
542
+ })
543
+
544
+ test("PhoneNumber has format phone", () => {
545
+ const doc = specialJsonSchemaDocument(S.PhoneNumber)
546
+ expect(doc).toStrictEqual({
547
+ dialect: "draft-2020-12",
548
+ schema: { "$ref": "#/$defs/PhoneNumber" },
549
+ definitions: {
550
+ PhoneNumber: {
551
+ type: "string",
552
+ title: "PhoneNumber",
553
+ description: "a phone number with at least 7 digits",
554
+ format: "phone"
555
+ }
556
+ }
557
+ })
558
+ })
559
+
560
+ test("Url has format uri", () => {
561
+ const doc = specialJsonSchemaDocument(S.Url)
562
+ expect(doc).toStrictEqual({
563
+ dialect: "draft-2020-12",
564
+ schema: { "$ref": "#/$defs/Url" },
565
+ definitions: {
566
+ Url: {
567
+ type: "string",
568
+ title: "Url",
569
+ format: "uri"
570
+ }
571
+ }
572
+ })
573
+ })
574
+ })
575
+
576
+ describe("generateGuards", () => {
577
+ const StateSchema = S.TaggedUnion([
578
+ S.TaggedStruct("Active", { since: S.String }),
579
+ S.TaggedStruct("Inactive", { reason: S.String }),
580
+ S.TaggedStruct("Pending", { eta: S.Finite })
581
+ ])
582
+
583
+ type State = S.Schema.Type<typeof StateSchema>
584
+ type Entity = { readonly state: State; readonly name: string }
585
+
586
+ const { isActive, isAnyOf, isInactive, isPending } = StateSchema.generateGuards("state")
587
+
588
+ test("isActive narrows to Active member", () => {
589
+ const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
590
+ expect(isActive(entity)).toBe(true)
591
+ if (isActive(entity)) {
592
+ expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
593
+ }
594
+ })
595
+
596
+ test("isActive returns false for non-Active", () => {
597
+ const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
598
+ expect(isActive(entity)).toBe(false)
599
+ })
600
+
601
+ test("isInactive narrows to Inactive member", () => {
602
+ const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
603
+ expect(isInactive(entity)).toBe(true)
604
+ })
605
+
606
+ test("isPending narrows to Pending member", () => {
607
+ const entity: Entity = { state: { _tag: "Pending", eta: 42 }, name: "foo" }
608
+ expect(isPending(entity)).toBe(true)
609
+ })
610
+
611
+ test("isAnyOf narrows to union of specified members", () => {
612
+ const isActiveOrPending = isAnyOf(["Active", "Pending"])
613
+ const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
614
+ const pending: Entity = { state: { _tag: "Pending", eta: 5 }, name: "bar" }
615
+ const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
616
+
617
+ expect(isActiveOrPending(active)).toBe(true)
618
+ expect(isActiveOrPending(pending)).toBe(true)
619
+ expect(isActiveOrPending(inactive)).toBe(false)
620
+
621
+ if (isActiveOrPending(active)) {
622
+ expectTypeOf(active.state).toEqualTypeOf<
623
+ { readonly _tag: "Active"; readonly since: string } | { readonly _tag: "Pending"; readonly eta: number }
624
+ >()
625
+ }
626
+ })
627
+
628
+ test("guards use schema-based validation (built-in guards)", () => {
629
+ expect(StateSchema.guards.Active({ _tag: "Active" })).toBe(false)
630
+ expect(StateSchema.guards.Active({ _tag: "Active", since: "2024-01-01" })).toBe(true)
631
+ })
632
+ })
633
+
634
+ describe("generateGuardsFor", () => {
635
+ const StateSchema = S.TaggedUnion([
636
+ S.TaggedStruct("Active", { since: S.String }),
637
+ S.TaggedStruct("Inactive", { reason: S.String }),
638
+ S.TaggedStruct("Pending", { eta: S.Finite })
639
+ ])
640
+
641
+ type State = S.Schema.Type<typeof StateSchema>
642
+ type Entity = { readonly state: State; readonly name: string }
643
+
644
+ const { isActive, isAnyOf } = StateSchema.generateGuardsFor<Entity>()("state")
645
+
646
+ test("isActive narrows to Active member", () => {
647
+ const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
648
+ expect(isActive(entity)).toBe(true)
649
+ if (isActive(entity)) {
650
+ expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
651
+ }
652
+ })
653
+
654
+ test("isActive returns false for non-Active", () => {
655
+ const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
656
+ expect(isActive(entity)).toBe(false)
657
+ })
658
+
659
+ test("isAnyOf narrows to union of specified members", () => {
660
+ const isActiveOrPending = isAnyOf(["Active", "Pending"])
661
+ const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
662
+ const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
663
+
664
+ expect(isActiveOrPending(active)).toBe(true)
665
+ expect(isActiveOrPending(inactive)).toBe(false)
666
+ })
667
+
668
+ test("ExtendTaggedUnion also exposes generateGuardsFor", () => {
669
+ const union = S.Union([
670
+ S.TaggedStruct("X", { x: S.String }),
671
+ S.TaggedStruct("Y", { y: S.Finite })
672
+ ])
673
+ const extended = S.ExtendTaggedUnion(union)
674
+ type Obj = { readonly field: S.Schema.Type<typeof extended> }
675
+ const { isX, isY } = extended.generateGuardsFor<Obj>()("field")
676
+
677
+ expect(isX({ field: { _tag: "X", x: "hi" } })).toBe(true)
678
+ expect(isX({ field: { _tag: "Y", y: 1 } })).toBe(false)
679
+ expect(isY({ field: { _tag: "Y", y: 1 } })).toBe(true)
680
+ })
681
+ })