effect-app 4.0.0-beta.99 → 4.0.0

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 (371) hide show
  1. package/CHANGELOG.md +2028 -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 +2 -2
  6. package/dist/Chunk.d.ts.map +1 -1
  7. package/dist/Config/SecretURL.d.ts +3 -1
  8. package/dist/Config/SecretURL.d.ts.map +1 -1
  9. package/dist/Config/SecretURL.js +3 -6
  10. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  11. package/dist/Config/internal/configSecretURL.js +2 -2
  12. package/dist/Config.d.ts +0 -5
  13. package/dist/Config.d.ts.map +1 -1
  14. package/dist/Config.js +7 -5
  15. package/dist/ConfigProvider.d.ts +0 -37
  16. package/dist/ConfigProvider.d.ts.map +1 -1
  17. package/dist/ConfigProvider.js +5 -41
  18. package/dist/Context.d.ts +6 -4
  19. package/dist/Context.d.ts.map +1 -1
  20. package/dist/Context.js +4 -3
  21. package/dist/DataDependencies.d.ts +136 -0
  22. package/dist/DataDependencies.d.ts.map +1 -0
  23. package/dist/DataDependencies.js +112 -0
  24. package/dist/Effect.d.ts +3 -3
  25. package/dist/Effect.d.ts.map +1 -1
  26. package/dist/Effect.js +6 -9
  27. package/dist/Emailer.d.ts +51 -0
  28. package/dist/Emailer.d.ts.map +1 -0
  29. package/dist/Emailer.js +7 -0
  30. package/dist/Function.d.ts.map +1 -1
  31. package/dist/Layer.d.ts +6 -3
  32. package/dist/Layer.d.ts.map +1 -1
  33. package/dist/Layer.js +3 -2
  34. package/dist/Model/Repository/Registry.d.ts +22 -0
  35. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  36. package/dist/Model/Repository/Registry.js +18 -0
  37. package/dist/Model/Repository/dependency.d.ts +6 -0
  38. package/dist/Model/Repository/dependency.d.ts.map +1 -0
  39. package/dist/Model/Repository/dependency.js +29 -0
  40. package/dist/Model/Repository/ext.d.ts +60 -0
  41. package/dist/Model/Repository/ext.d.ts.map +1 -0
  42. package/dist/Model/Repository/ext.js +124 -0
  43. package/dist/Model/Repository/internal/internal.d.ts +68 -0
  44. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  45. package/dist/Model/Repository/internal/internal.js +611 -0
  46. package/dist/Model/Repository/legacy.d.ts +21 -0
  47. package/dist/Model/Repository/legacy.d.ts.map +1 -0
  48. package/dist/Model/Repository/legacy.js +2 -0
  49. package/dist/Model/Repository/makeRepo.d.ts +71 -0
  50. package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
  51. package/dist/Model/Repository/makeRepo.js +30 -0
  52. package/dist/Model/Repository/service.d.ts +121 -0
  53. package/dist/Model/Repository/service.d.ts.map +1 -0
  54. package/dist/Model/Repository/service.js +2 -0
  55. package/dist/Model/Repository/validation.d.ts +72 -0
  56. package/dist/Model/Repository/validation.d.ts.map +1 -0
  57. package/dist/Model/Repository/validation.js +39 -0
  58. package/dist/Model/Repository.d.ts +8 -0
  59. package/dist/Model/Repository.d.ts.map +1 -0
  60. package/dist/Model/Repository.js +8 -0
  61. package/dist/Model/dsl.d.ts +33 -0
  62. package/dist/Model/dsl.d.ts.map +1 -0
  63. package/dist/Model/dsl.js +43 -0
  64. package/dist/Model/filter/filterApi.d.ts +30 -0
  65. package/dist/Model/filter/filterApi.d.ts.map +1 -0
  66. package/dist/Model/filter/filterApi.js +2 -0
  67. package/dist/Model/filter/types/errors.d.ts +29 -0
  68. package/dist/Model/filter/types/errors.d.ts.map +1 -0
  69. package/dist/Model/filter/types/errors.js +2 -0
  70. package/dist/Model/filter/types/fields.d.ts +15 -0
  71. package/dist/Model/filter/types/fields.d.ts.map +1 -0
  72. package/dist/Model/filter/types/fields.js +2 -0
  73. package/dist/Model/filter/types/path/common.d.ts +316 -0
  74. package/dist/Model/filter/types/path/common.d.ts.map +1 -0
  75. package/dist/Model/filter/types/path/common.js +2 -0
  76. package/dist/Model/filter/types/path/eager.d.ts +94 -0
  77. package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
  78. package/dist/Model/filter/types/path/eager.js +36 -0
  79. package/dist/Model/filter/types/path/index.d.ts +4 -0
  80. package/dist/Model/filter/types/path/index.d.ts.map +1 -0
  81. package/dist/Model/filter/types/path/index.js +3 -0
  82. package/dist/Model/filter/types/utils.d.ts +79 -0
  83. package/dist/Model/filter/types/utils.d.ts.map +1 -0
  84. package/dist/Model/filter/types/utils.js +2 -0
  85. package/dist/Model/filter/types/validator.d.ts +30 -0
  86. package/dist/Model/filter/types/validator.d.ts.map +1 -0
  87. package/dist/Model/filter/types/validator.js +2 -0
  88. package/dist/Model/filter/types.d.ts +5 -0
  89. package/dist/Model/filter/types.d.ts.map +1 -0
  90. package/dist/Model/filter/types.js +7 -0
  91. package/dist/Model/query/dsl.d.ts +630 -0
  92. package/dist/Model/query/dsl.d.ts.map +1 -0
  93. package/dist/Model/query/dsl.js +376 -0
  94. package/dist/Model/query/new-kid-interpreter.d.ts +132 -0
  95. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
  96. package/dist/Model/query/new-kid-interpreter.js +370 -0
  97. package/dist/Model/query.d.ts +15 -0
  98. package/dist/Model/query.d.ts.map +1 -0
  99. package/dist/Model/query.js +3 -0
  100. package/dist/Model.d.ts +6 -0
  101. package/dist/Model.d.ts.map +1 -0
  102. package/dist/Model.js +6 -0
  103. package/dist/NonEmptySet.d.ts +3 -1
  104. package/dist/NonEmptySet.d.ts.map +1 -1
  105. package/dist/NonEmptySet.js +2 -2
  106. package/dist/Option.d.ts +1 -0
  107. package/dist/Option.d.ts.map +1 -1
  108. package/dist/Option.js +3 -1
  109. package/dist/Pure.d.ts +5 -3
  110. package/dist/Pure.d.ts.map +1 -1
  111. package/dist/Pure.js +5 -2
  112. package/dist/QueueMaker.d.ts +11 -0
  113. package/dist/QueueMaker.d.ts.map +1 -0
  114. package/dist/QueueMaker.js +11 -0
  115. package/dist/RequestContext.d.ts +87 -0
  116. package/dist/RequestContext.d.ts.map +1 -0
  117. package/dist/RequestContext.js +45 -0
  118. package/dist/RequestScopedDependencies.d.ts +21 -0
  119. package/dist/RequestScopedDependencies.d.ts.map +1 -0
  120. package/dist/RequestScopedDependencies.js +16 -0
  121. package/dist/Schema/Class.d.ts +163 -16
  122. package/dist/Schema/Class.d.ts.map +1 -1
  123. package/dist/Schema/Class.js +193 -21
  124. package/dist/Schema/FastCheck.d.ts.map +1 -1
  125. package/dist/Schema/SchemaParser.d.ts +5 -0
  126. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  127. package/dist/Schema/SchemaParser.js +6 -0
  128. package/dist/Schema/SpecialJsonSchema.d.ts +15 -2
  129. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -1
  130. package/dist/Schema/SpecialJsonSchema.js +70 -12
  131. package/dist/Schema/SpecialOpenApi.d.ts +1 -1
  132. package/dist/Schema/SpecialOpenApi.js +3 -3
  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 +6 -4
  136. package/dist/Schema/email.d.ts.map +1 -1
  137. package/dist/Schema/email.js +8 -5
  138. package/dist/Schema/ext.d.ts +297 -72
  139. package/dist/Schema/ext.d.ts.map +1 -1
  140. package/dist/Schema/ext.js +331 -67
  141. package/dist/Schema/moreStrings.d.ts +82 -48
  142. package/dist/Schema/moreStrings.d.ts.map +1 -1
  143. package/dist/Schema/moreStrings.js +85 -51
  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 +64 -12
  147. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  148. package/dist/Schema/phoneNumber.js +6 -6
  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 +223 -6
  156. package/dist/Schema.d.ts.map +1 -1
  157. package/dist/Schema.js +189 -23
  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 +13 -8
  183. package/dist/client/apiClientFactory.d.ts.map +1 -1
  184. package/dist/client/apiClientFactory.js +148 -29
  185. package/dist/client/clientFor.d.ts +48 -13
  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 +74 -28
  189. package/dist/client/errors.d.ts.map +1 -1
  190. package/dist/client/errors.js +41 -10
  191. package/dist/client/makeClient.d.ts +487 -32
  192. package/dist/client/makeClient.d.ts.map +1 -1
  193. package/dist/client/makeClient.js +47 -21
  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 +2 -2
  199. package/dist/http/Request.d.ts.map +1 -1
  200. package/dist/http/Request.js +2 -2
  201. package/dist/http/internal/lib.d.ts +12 -12
  202. package/dist/http/internal/lib.d.ts.map +1 -1
  203. package/dist/http/internal/lib.js +13 -13
  204. package/dist/ids.d.ts +32 -14
  205. package/dist/ids.d.ts.map +1 -1
  206. package/dist/ids.js +38 -7
  207. package/dist/index.d.ts +3 -0
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +5 -1
  210. package/dist/middleware.d.ts +12 -6
  211. package/dist/middleware.d.ts.map +1 -1
  212. package/dist/middleware.js +13 -7
  213. package/dist/rpc/Invalidation.d.ts +1124 -0
  214. package/dist/rpc/Invalidation.d.ts.map +1 -0
  215. package/dist/rpc/Invalidation.js +178 -0
  216. package/dist/rpc/MiddlewareMaker.d.ts +10 -7
  217. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  218. package/dist/rpc/MiddlewareMaker.js +57 -37
  219. package/dist/rpc/RpcContextMap.d.ts +2 -2
  220. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  221. package/dist/rpc/RpcMiddleware.d.ts +13 -10
  222. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  223. package/dist/rpc/RpcMiddleware.js +2 -2
  224. package/dist/rpc.d.ts +1 -1
  225. package/dist/rpc.d.ts.map +1 -1
  226. package/dist/rpc.js +2 -2
  227. package/dist/runtime.d.ts +19 -0
  228. package/dist/runtime.d.ts.map +1 -0
  229. package/dist/runtime.js +40 -0
  230. package/dist/setupRequest.d.ts +20 -0
  231. package/dist/setupRequest.d.ts.map +1 -0
  232. package/dist/setupRequest.js +72 -0
  233. package/dist/toast.d.ts +51 -0
  234. package/dist/toast.d.ts.map +1 -0
  235. package/dist/toast.js +34 -0
  236. package/dist/transform.d.ts +1 -1
  237. package/dist/transform.d.ts.map +1 -1
  238. package/dist/transform.js +4 -5
  239. package/dist/utils/effectify.d.ts +1 -1
  240. package/dist/utils/effectify.d.ts.map +1 -1
  241. package/dist/utils/effectify.js +2 -2
  242. package/dist/utils/extend.d.ts +6 -1
  243. package/dist/utils/extend.d.ts.map +1 -1
  244. package/dist/utils/extend.js +24 -6
  245. package/dist/utils/gen.d.ts +4 -4
  246. package/dist/utils/gen.d.ts.map +1 -1
  247. package/dist/utils/logLevel.d.ts +2 -2
  248. package/dist/utils/logLevel.d.ts.map +1 -1
  249. package/dist/utils/logger.d.ts +2 -1
  250. package/dist/utils/logger.d.ts.map +1 -1
  251. package/dist/utils/logger.js +2 -2
  252. package/dist/utils.d.ts +34 -39
  253. package/dist/utils.d.ts.map +1 -1
  254. package/dist/utils.js +33 -37
  255. package/dist/validation/validators.d.ts.map +1 -1
  256. package/dist/validation.d.ts.map +1 -1
  257. package/dist/withToast.d.ts +30 -0
  258. package/dist/withToast.d.ts.map +1 -0
  259. package/dist/withToast.js +64 -0
  260. package/package.json +26 -266
  261. package/src/Array.ts +5 -5
  262. package/src/Chunk.ts +3 -3
  263. package/src/Config/SecretURL.ts +6 -3
  264. package/src/Config/internal/configSecretURL.ts +2 -2
  265. package/src/Config.ts +6 -13
  266. package/src/ConfigProvider.ts +4 -47
  267. package/src/Context.ts +9 -8
  268. package/src/DataDependencies.ts +171 -0
  269. package/src/Effect.ts +9 -12
  270. package/src/Emailer.ts +51 -0
  271. package/src/Inputify.type.ts +1 -1
  272. package/src/Layer.ts +8 -5
  273. package/src/Model/Repository/Registry.ts +35 -0
  274. package/src/Model/Repository/dependency.ts +32 -0
  275. package/src/Model/Repository/ext.ts +374 -0
  276. package/src/Model/Repository/internal/internal.ts +988 -0
  277. package/src/Model/Repository/legacy.ts +29 -0
  278. package/src/Model/Repository/makeRepo.ts +168 -0
  279. package/src/Model/Repository/service.ts +676 -0
  280. package/src/Model/Repository/validation.ts +38 -0
  281. package/src/Model/Repository.ts +7 -0
  282. package/src/Model/dsl.ts +129 -0
  283. package/src/Model/filter/filterApi.ts +78 -0
  284. package/src/Model/filter/types/errors.ts +47 -0
  285. package/src/Model/filter/types/fields.ts +50 -0
  286. package/src/Model/filter/types/path/common.ts +404 -0
  287. package/src/Model/filter/types/path/eager.ts +329 -0
  288. package/src/Model/filter/types/path/index.ts +4 -0
  289. package/src/Model/filter/types/utils.ts +128 -0
  290. package/src/Model/filter/types/validator.ts +46 -0
  291. package/src/Model/filter/types.ts +6 -0
  292. package/src/Model/query/dsl.ts +3092 -0
  293. package/src/Model/query/new-kid-interpreter.ts +532 -0
  294. package/src/Model/query.ts +13 -0
  295. package/src/Model.ts +5 -0
  296. package/src/NonEmptySet.ts +6 -4
  297. package/src/Option.ts +2 -0
  298. package/src/Pure.ts +6 -3
  299. package/src/QueueMaker.ts +11 -0
  300. package/src/RequestContext.ts +48 -0
  301. package/src/RequestScopedDependencies.ts +45 -0
  302. package/src/Schema/Class.ts +550 -56
  303. package/src/Schema/SchemaParser.ts +12 -0
  304. package/src/Schema/SpecialJsonSchema.ts +75 -7
  305. package/src/Schema/SpecialOpenApi.ts +5 -5
  306. package/src/Schema/brand.ts +27 -12
  307. package/src/Schema/email.ts +9 -7
  308. package/src/Schema/ext.ts +408 -91
  309. package/src/Schema/moreStrings.ts +213 -88
  310. package/src/Schema/numbers.ts +107 -29
  311. package/src/Schema/phoneNumber.ts +8 -9
  312. package/src/Schema/schema.ts +1 -1
  313. package/src/Schema/strings.ts +23 -14
  314. package/src/Schema.ts +403 -46
  315. package/src/SchemaAST.ts +1 -0
  316. package/src/Set.ts +6 -2
  317. package/src/Store.ts +325 -0
  318. package/src/_ext/Array.ts +4 -2
  319. package/src/_ext/misc.ts +4 -1
  320. package/src/_ext/ord.ext.ts +2 -1
  321. package/src/builtin-json.d.ts +24 -0
  322. package/src/builtin.ts +16 -21
  323. package/src/client/InvalidationKeys.ts +55 -0
  324. package/src/client/apiClientFactory.ts +291 -126
  325. package/src/client/clientFor.ts +101 -33
  326. package/src/client/errors.ts +66 -20
  327. package/src/client/makeClient.ts +519 -60
  328. package/src/client.ts +6 -4
  329. package/src/http/Request.ts +4 -4
  330. package/src/http/internal/lib.ts +12 -12
  331. package/src/http.ts +1 -1
  332. package/src/ids.ts +45 -8
  333. package/src/index.ts +20 -16
  334. package/src/middleware.ts +12 -8
  335. package/src/rpc/Invalidation.ts +278 -0
  336. package/src/rpc/MiddlewareMaker.ts +86 -79
  337. package/src/rpc/README.md +2 -2
  338. package/src/rpc/RpcContextMap.ts +4 -4
  339. package/src/rpc/RpcMiddleware.ts +18 -13
  340. package/src/rpc.ts +4 -4
  341. package/src/runtime.ts +57 -0
  342. package/src/setupRequest.ts +138 -0
  343. package/src/toast.ts +54 -0
  344. package/src/transform.ts +4 -4
  345. package/src/utils/effectify.ts +1 -1
  346. package/src/utils/extend.ts +24 -5
  347. package/src/utils/gen.ts +7 -7
  348. package/src/utils/logLevel.ts +1 -1
  349. package/src/utils/logger.ts +3 -2
  350. package/src/utils.ts +85 -158
  351. package/src/validation.ts +2 -2
  352. package/src/withToast.ts +133 -0
  353. package/test/dist/rpc.test.d.ts.map +1 -1
  354. package/test/dist/rpcMiddlewareContext.test.d.ts.map +1 -0
  355. package/test/moreStrings.test.ts +55 -10
  356. package/test/rpc.test.ts +28 -11
  357. package/test/rpcMiddlewareContext.test.ts +63 -0
  358. package/test/schema.test.ts +318 -55
  359. package/test/secretURL.test.ts +4 -1
  360. package/test/special.test.ts +663 -131
  361. package/test/utils.test.ts +5 -5
  362. package/tsconfig.base.json +6 -4
  363. package/tsconfig.json +9 -1
  364. package/tsconfig.src.json +29 -29
  365. package/tsconfig.test.json +2 -2
  366. package/dist/Operations.d.ts +0 -124
  367. package/dist/Operations.d.ts.map +0 -1
  368. package/dist/Operations.js +0 -29
  369. package/eslint.config.mjs +0 -26
  370. package/src/Operations.ts +0 -55
  371. package/tsconfig.json.bak +0 -12
@@ -1,145 +1,595 @@
1
- import { Option, Predicate, Schema, SchemaGetter } from "effect"
2
1
  import { InvalidStateError, LoginError, NotFoundError, NotLoggedInError, OptimisticConcurrencyException, ServiceUnavailableError, UnauthorizedError, ValidationError } from "effect-app/client/errors"
2
+ import { RequestContext } from "effect-app/RequestContext"
3
3
  import * as AppSchema from "effect-app/Schema"
4
- import { Class, TaggedClass } from "effect-app/Schema/Class"
5
- import { flattenSimpleAllOf, specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
4
+ import { Class } from "effect-app/Schema/Class"
5
+ import { flattenNestedAnyOf, flattenSimpleAllOf, specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
6
6
  import { deduplicateOpenApiSchemas } from "effect-app/Schema/SpecialOpenApi"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Option from "effect/Option"
9
+ import * as Predicate from "effect/Predicate"
7
10
  import * as S from "effect/Schema"
11
+ import * as SchemaGetter from "effect/SchemaGetter"
8
12
  import { describe, expect, it } from "vitest"
9
13
 
14
+ class _FacadeModel extends AppSchema.Class<_FacadeModel>("FacadeModel")({ value: S.String }) {
15
+ get upperValue() {
16
+ return this.value.toUpperCase()
17
+ }
18
+
19
+ static describe(self: { readonly value: string }) {
20
+ return `value:${self.value}`
21
+ }
22
+ }
23
+
24
+ interface FacadeModelEncoded {
25
+ readonly value: string
26
+ }
27
+
28
+ interface FacadeModelMake {
29
+ readonly value: string
30
+ }
31
+
32
+ class FacadeModel extends AppSchema.OpaqueFacade<
33
+ FacadeModel,
34
+ FacadeModelEncoded,
35
+ FacadeModelMake,
36
+ never,
37
+ never
38
+ >()(_FacadeModel) {
39
+ declare readonly value: string
40
+ declare readonly upperValue: string
41
+
42
+ get decoratedValue() {
43
+ return `facade:${this.value}`
44
+ }
45
+
46
+ label() {
47
+ return this.decoratedValue
48
+ }
49
+ }
50
+
51
+ class _RenamedFacadeModel extends AppSchema.Class<_RenamedFacadeModel>("RenamedFacadeModel")({
52
+ internalName: S.String
53
+ }) {
54
+ get upperName() {
55
+ return this.internalName.toUpperCase()
56
+ }
57
+ }
58
+
59
+ interface RenamedFacadeModelEncoded {
60
+ readonly external_name: string
61
+ }
62
+
63
+ interface RenamedFacadeModelMake {
64
+ readonly internalName: string
65
+ }
66
+
67
+ class RenamedFacadeModel extends AppSchema.OpaqueFacade<
68
+ RenamedFacadeModel,
69
+ RenamedFacadeModelEncoded,
70
+ RenamedFacadeModelMake,
71
+ never,
72
+ never
73
+ >()(_RenamedFacadeModel.pipe(S.encodeKeys({ internalName: "external_name" }))) {
74
+ declare readonly internalName: string
75
+ declare readonly upperName: string
76
+
77
+ get decoratedName() {
78
+ return `facade:${this.internalName}`
79
+ }
80
+ }
81
+
82
+ const _StructFacadeModel = AppSchema.Struct({ value: S.String })
83
+
84
+ interface StructFacadeModelEncoded {
85
+ readonly value: string
86
+ }
87
+
88
+ interface StructFacadeModelMake {
89
+ readonly value: string
90
+ }
91
+
92
+ class StructFacadeModel extends AppSchema.OpaqueFacade<
93
+ StructFacadeModel,
94
+ StructFacadeModelEncoded,
95
+ StructFacadeModelMake,
96
+ never,
97
+ never
98
+ >()(_StructFacadeModel) {
99
+ declare readonly value: string
100
+ }
101
+
102
+ const _RenamedStructFacadeModel = AppSchema.Struct({ internalName: S.String }).pipe(
103
+ S.encodeKeys({ internalName: "external_name" })
104
+ )
105
+
106
+ interface RenamedStructFacadeModelEncoded {
107
+ readonly external_name: string
108
+ }
109
+
110
+ interface RenamedStructFacadeModelMake {
111
+ readonly internalName: string
112
+ }
113
+
114
+ class RenamedStructFacadeModel extends AppSchema.OpaqueFacade<
115
+ RenamedStructFacadeModel,
116
+ RenamedStructFacadeModelEncoded,
117
+ RenamedStructFacadeModelMake,
118
+ never,
119
+ never
120
+ >()(_RenamedStructFacadeModel) {
121
+ declare readonly internalName: string
122
+ }
123
+
124
+ const _RenamedOpaqueStructFacadeModel = AppSchema
125
+ .Opaque<
126
+ { readonly internalName: string },
127
+ RenamedStructFacadeModelEncoded
128
+ >()(AppSchema.Struct({ internalName: S.String }))
129
+ .pipe(S.encodeKeys({ internalName: "external_name" }))
130
+
131
+ class RenamedOpaqueStructFacadeModel extends AppSchema.OpaqueFacade<
132
+ RenamedOpaqueStructFacadeModel,
133
+ RenamedStructFacadeModelEncoded,
134
+ RenamedStructFacadeModelMake,
135
+ never,
136
+ never
137
+ >()(_RenamedOpaqueStructFacadeModel) {
138
+ declare readonly internalName: string
139
+ }
140
+
10
141
  describe("Class", () => {
11
- it("encoding accepts plain objects matching the struct (Fields argument)", () => {
12
- class A extends Class<A>("A")({ a: S.String }) {}
142
+ describe("strict", () => {
143
+ it("encoding doesnt accepts plain objects matching the struct (Fields argument)", () => {
144
+ class A extends Class<A>("A")({ a: S.String }) {}
13
145
 
14
- // Encoding a class instance still works
15
- expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
146
+ expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
147
+ expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow()
148
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
149
+ })
16
150
 
17
- // Encoding a plain object matching the struct now succeeds
18
- expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
151
+ it("encoding rejects plain objects matching the struct (Struct argument)", () => {
152
+ class A extends Class<A>("A")(S.Struct({ a: S.String })) {}
19
153
 
20
- // Encoding null still fails
21
- expect(() => S.encodeUnknownSync(A)(null)).toThrow()
22
- })
154
+ expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
155
+ expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow()
156
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
157
+ })
23
158
 
24
- it("encoding accepts plain objects matching the struct (Struct argument)", () => {
25
- class A extends Class<A>("A")(S.Struct({ a: S.String })) {}
159
+ it("decoding still works normally", () => {
160
+ class A extends Class<A>("A")({ a: S.String }) {}
26
161
 
27
- expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
28
- expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
29
- expect(() => S.encodeUnknownSync(A)(null)).toThrow()
30
- })
162
+ const decoded = S.decodeUnknownSync(A)({ a: "hello" })
163
+ expect(decoded).toBeInstanceOf(A)
164
+ expect(decoded.a).toBe("hello")
31
165
 
32
- it("decoding still works normally", () => {
33
- class A extends Class<A>("A")({ a: S.String }) {}
166
+ expect(() => S.decodeUnknownSync(A)(null)).toThrow()
167
+ expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow()
168
+ })
34
169
 
35
- const decoded = S.decodeUnknownSync(A)({ a: "hello" })
36
- expect(decoded).toBeInstanceOf(A)
37
- expect((decoded as A).a).toBe("hello")
170
+ it("S.is accepts class instances and not matching plain objects", () => {
171
+ class A extends Class<A>("A")({ a: S.String }) {}
38
172
 
39
- expect(() => S.decodeUnknownSync(A)(null)).toThrow()
40
- expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow()
41
- })
173
+ expect(S.is(A)(new A({ a: "hello" }))).toBe(true)
174
+ expect(S.is(A)({ a: "world" })).toBe(false)
175
+ expect(S.is(A)({ a: 1 })).toBe(false)
176
+ expect(S.is(A)(null)).toBe(false)
177
+ })
42
178
 
43
- it("rejects values that don't match the struct", () => {
44
- class A extends Class<A>("A")({ a: S.String }) {}
179
+ it("rejects values that don't match the struct", () => {
180
+ class A extends Class<A>("A")({ a: S.String }) {}
45
181
 
46
- expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow()
47
- expect(() => S.encodeUnknownSync(A)("not an object")).toThrow()
48
- })
182
+ expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow()
183
+ expect(() => S.encodeUnknownSync(A)("not an object")).toThrow()
184
+ })
49
185
 
50
- it("returns a class constructor — new and instanceof work", () => {
51
- class A extends Class<A>("A")({ a: S.String }) {}
186
+ it("returns a class constructor — new and instanceof work", () => {
187
+ class A extends Class<A>("A")({ a: S.String }) {}
52
188
 
53
- const instance = new A({ a: "hello" })
54
- expect(instance).toBeInstanceOf(A)
55
- expect(instance.a).toBe("hello")
56
- })
189
+ const instance = new A({ a: "hello" })
190
+ expect(instance).toBeInstanceOf(A)
191
+ expect(instance.a).toBe("hello")
192
+ })
57
193
 
58
- it("preserves fields and identifier", () => {
59
- class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
194
+ it("preserves fields and identifier", () => {
195
+ class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
60
196
 
61
- expect(A.identifier).toBe("A")
62
- expect(Object.keys(A.fields)).toStrictEqual(["a", "b"])
197
+ expect(A.identifier).toBe("A")
198
+ expect(Object.keys(A.fields)).toStrictEqual(["a", "b"])
199
+ })
63
200
  })
64
- })
65
201
 
66
- describe("Class constructor", () => {
67
- it("works as a base class — new, instanceof, encoding plain objects", () => {
68
- class A extends Class<A>("A")({ a: S.String }) {}
202
+ describe("non strict", () => {
203
+ it("encoding accepts plain objects matching the struct (Fields argument)", () => {
204
+ class A extends Class<A>("A")({ a: S.String }, undefined, { strict: false }) {}
205
+
206
+ expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
207
+ expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
208
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
209
+ })
69
210
 
70
- // Construction
71
- const instance = new A({ a: "hello" })
72
- expect(instance).toBeInstanceOf(A)
73
- expect(instance.a).toBe("hello")
211
+ it("encoding accepts plain objects matching the struct (Struct argument)", () => {
212
+ class A extends Class<A>("A")(S.Struct({ a: S.String }), undefined, { strict: false }) {}
74
213
 
75
- // Encoding a class instance
76
- expect(S.encodeUnknownSync(A)(instance)).toStrictEqual({ a: "hello" })
214
+ expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" })
215
+ expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
216
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
217
+ })
77
218
 
78
- // Encoding a plain object
79
- expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
219
+ it("S.is accepts class instances and matching plain objects", () => {
220
+ class A extends Class<A>("A")({ a: S.String }, undefined, { strict: false }) {}
80
221
 
81
- // Encoding invalid input fails
82
- expect(() => S.encodeUnknownSync(A)(null)).toThrow()
83
- expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow()
222
+ expect(S.is(A)(new A({ a: "hello" }))).toBe(true)
223
+ expect(S.is(A)({ a: "world" })).toBe(true)
224
+ expect(S.is(A)({ a: 1 })).toBe(false)
225
+ expect(S.is(A)(null)).toBe(false)
226
+ })
84
227
  })
228
+ })
85
229
 
86
- it("decoding works normally", () => {
87
- class A extends Class<A>("A")({ a: S.String }) {}
230
+ describe("Class constructor", () => {
231
+ describe("strict", () => {
232
+ it("works as a base class — new, instanceof, encoding plain objects", () => {
233
+ class A extends Class<A>("A")({ a: S.String }) {}
234
+
235
+ const instance = new A({ a: "hello" })
236
+ expect(instance).toBeInstanceOf(A)
237
+ expect(instance.a).toBe("hello")
238
+
239
+ expect(S.encodeUnknownSync(A)(instance)).toStrictEqual({ a: "hello" })
240
+ expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow()
241
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
242
+ expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow()
243
+ })
88
244
 
89
- const decoded = S.decodeUnknownSync(A)({ a: "hello" })
90
- expect(decoded).toBeInstanceOf(A)
91
- expect((decoded as A).a).toBe("hello")
245
+ it("decoding works normally", () => {
246
+ class A extends Class<A>("A")({ a: S.String }) {}
92
247
 
93
- expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow()
248
+ const decoded = S.decodeUnknownSync(A)({ a: "hello" })
249
+ expect(decoded).toBeInstanceOf(A)
250
+ expect(decoded.a).toBe("hello")
251
+
252
+ expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow()
253
+ })
254
+
255
+ it("exposes fields, identifier", () => {
256
+ class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
257
+
258
+ expect(A.identifier).toBe("A")
259
+ expect(Object.keys(A.fields)).toStrictEqual(["a", "b"])
260
+ })
94
261
  })
95
262
 
96
- it("exposes fields, identifier", () => {
97
- class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
263
+ describe("non strict", () => {
264
+ it("works as a base class — new, instanceof, encoding plain objects", () => {
265
+ class A extends Class<A>("A")({ a: S.String }, undefined, { strict: false }) {}
98
266
 
99
- expect(A.identifier).toBe("A")
100
- expect(Object.keys(A.fields)).toStrictEqual(["a", "b"])
267
+ const instance = new A({ a: "hello" })
268
+ expect(instance).toBeInstanceOf(A)
269
+ expect(instance.a).toBe("hello")
270
+
271
+ expect(S.encodeUnknownSync(A)(instance)).toStrictEqual({ a: "hello" })
272
+ expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" })
273
+ expect(() => S.encodeUnknownSync(A)(null)).toThrow()
274
+ expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow()
275
+ })
101
276
  })
102
277
  })
103
278
 
104
- describe("TaggedClass constructor", () => {
105
- it("works as a base class with _tag — new, instanceof, encoding plain objects", () => {
106
- class Circle extends TaggedClass<Circle>()("Circle", { radius: S.Number }) {}
279
+ describe("TaggedStruct constructor", () => {
280
+ it("works with _tag, make, and encoding plain objects", () => {
281
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
107
282
 
108
- // Construction
109
- const instance = new Circle({ radius: 5 })
110
- expect(instance).toBeInstanceOf(Circle)
111
- expect(instance._tag).toBe("Circle")
112
- expect(instance.radius).toBe(5)
283
+ const instance = Circle.make({ radius: 5 })
284
+ expect(instance).toEqual({ _tag: "Circle", radius: 5 })
113
285
 
114
- // Encoding a class instance
115
286
  expect(S.encodeUnknownSync(Circle)(instance)).toStrictEqual({ _tag: "Circle", radius: 5 })
116
-
117
- // Encoding a plain object
118
287
  expect(S.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 10 })).toStrictEqual({ _tag: "Circle", radius: 10 })
119
288
 
120
- // Encoding invalid input fails
121
289
  expect(() => S.encodeUnknownSync(Circle)(null)).toThrow()
122
290
  expect(() => S.encodeUnknownSync(Circle)({ _tag: "Circle", radius: "nope" })).toThrow()
123
291
  })
124
292
 
125
293
  it("decoding works normally", () => {
126
- class Circle extends TaggedClass<Circle>()("Circle", { radius: S.Number }) {}
294
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
127
295
 
128
296
  const decoded = S.decodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 })
129
- expect(decoded).toBeInstanceOf(Circle)
130
- expect((decoded as Circle).radius).toBe(5)
131
- expect((decoded as Circle)._tag).toBe("Circle")
297
+ expect(decoded).toEqual({ _tag: "Circle", radius: 5 })
298
+ })
299
+
300
+ it("S.decodeSync(S.toType(X)) should report n length schema error", () => {
301
+ class X extends S.Opaque<X>()(S.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {}
302
+
303
+ try {
304
+ S.decodeSync(S.toType(X))({ _tag: "X", n: "a" /* not length 3 */ })
305
+ expect.fail("expected decode to fail with a SchemaError")
306
+ } catch (error) {
307
+ expect(S.isSchemaError(error)).toBe(true)
308
+ if (S.isSchemaError(error)) {
309
+ expect(error.message).toContain("n")
310
+ expect(error.message.toLowerCase()).toContain("length")
311
+ }
312
+ }
132
313
  })
133
314
 
134
- it("exposes fields, identifier", () => {
135
- class Circle extends TaggedClass<Circle>()("Circle", { radius: S.Number }) {}
315
+ it("exposes fields", () => {
316
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
136
317
 
137
- expect(Circle.identifier).toBe("Circle")
138
318
  expect(Object.keys(Circle.fields)).toContain("_tag")
139
319
  expect(Object.keys(Circle.fields)).toContain("radius")
140
320
  })
141
321
  })
142
322
 
323
+ describe("strict declaration option", () => {
324
+ it("Class strict: true keeps class-level expected errors", () => {
325
+ class X extends Class<X>("X")({ n: S.String.pipe(S.check(S.isMinLength(3))) }, undefined, { strict: true }) {}
326
+
327
+ expect(() => S.decodeSync(S.toType(X))({ n: "a" })).toThrow("Expected X")
328
+ })
329
+
330
+ it("TaggedStruct preserves field-level expected errors", () => {
331
+ class X extends S.Opaque<X>()(AppSchema.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {}
332
+
333
+ expect(() => S.decodeSync(S.toType(X))({ _tag: "X", n: "a" })).toThrow()
334
+ })
335
+
336
+ it("Class with encoded override strict: true keeps class-level expected errors", () => {
337
+ class X extends Class<X, never>("X")({ n: S.String.pipe(S.check(S.isMinLength(3))) }, undefined, {
338
+ strict: true
339
+ }) {}
340
+
341
+ expect(() => S.decodeSync(S.toType(X))({ n: "a" })).toThrow("Expected X")
342
+ })
343
+
344
+ it("TaggedStruct with opaque wrapper decodes tagged input", () => {
345
+ class X extends S.Opaque<X>()(AppSchema.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {}
346
+
347
+ expect(() => S.decodeSync(S.toType(X))({ _tag: "X", n: "a" })).toThrow()
348
+ })
349
+ })
350
+
351
+ describe("Class.copy", () => {
352
+ it("creates a new instance with updated fields", () => {
353
+ class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
354
+
355
+ const instance = new A({ a: "hello", b: 1 })
356
+ const copied: A = A.copy(instance, { b: 2 })
357
+ expect(copied).toBeInstanceOf(A)
358
+ expect(copied.a).toBe("hello")
359
+ expect(copied.b).toBe(2)
360
+ })
361
+
362
+ it("accepts a function for updates", () => {
363
+ class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
364
+
365
+ const instance = new A({ a: "hello", b: 1 })
366
+ const copied: A = A.copy(instance, (a) => ({ b: a.b + 1 }))
367
+ expect(copied).toBeInstanceOf(A)
368
+ expect(copied.b).toBe(2)
369
+ })
370
+
371
+ it("is pipeable", () => {
372
+ class A extends Class<A>("A")({ a: S.String, b: S.Number }) {}
373
+
374
+ const instance = new A({ a: "hello", b: 1 })
375
+ const copied: A = A.copy({ b: 2 })(instance)
376
+ expect(copied).toBeInstanceOf(A)
377
+ expect(copied.b).toBe(2)
378
+ })
379
+ })
380
+
381
+ describe("TaggedStruct (opaque).copy", () => {
382
+ it("creates a new instance with updated fields", () => {
383
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
384
+
385
+ const instance = Circle.make({ radius: 5 })
386
+ const copied = Circle.copy(instance, { radius: 10 })
387
+ expect(copied).toEqual({ _tag: "Circle", radius: 10 })
388
+ })
389
+
390
+ it("accepts a function for updates", () => {
391
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
392
+
393
+ const instance = Circle.make({ radius: 5 })
394
+ const copied = Circle.copy(instance, (c) => ({ radius: c.radius * 2 }))
395
+ expect(copied).toEqual({ _tag: "Circle", radius: 10 })
396
+ })
397
+ })
398
+
399
+ describe("OpaqueFacade", () => {
400
+ it("keeps OpaqueFacade usable with struct schemas", () => {
401
+ const decoded = S.decodeSync(StructFacadeModel)({ value: "hello" })
402
+
403
+ expect(decoded).toStrictEqual({ value: "hello" })
404
+ expect(S.encodeSync(StructFacadeModel)(decoded)).toStrictEqual({ value: "hello" })
405
+ expect(S.is(StructFacadeModel)(decoded)).toBe(true)
406
+ expect(S.is(StructFacadeModel)({ value: 1 })).toBe(false)
407
+ })
408
+
409
+ it("keeps encoded-key struct facades on the struct path", () => {
410
+ const decoded = S.decodeSync(RenamedStructFacadeModel)({ external_name: "fran" })
411
+
412
+ expect(decoded).toStrictEqual({ internalName: "fran" })
413
+ expect(decoded).not.toBeInstanceOf(RenamedStructFacadeModel)
414
+ expect(S.encodeSync(RenamedStructFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" })
415
+ expect(S.is(RenamedStructFacadeModel)(decoded)).toBe(true)
416
+ expect(S.is(RenamedStructFacadeModel)({ external_name: "fran" })).toBe(false)
417
+ })
418
+
419
+ it("keeps encoded-key opaque struct facades on the struct path", () => {
420
+ const decoded = S.decodeSync(RenamedOpaqueStructFacadeModel)({ external_name: "fran" })
421
+
422
+ expect(decoded).toStrictEqual({ internalName: "fran" })
423
+ expect(decoded).not.toBeInstanceOf(RenamedOpaqueStructFacadeModel)
424
+ expect(S.encodeSync(RenamedOpaqueStructFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" })
425
+ expect(S.is(RenamedOpaqueStructFacadeModel)(decoded)).toBe(true)
426
+ expect(S.is(RenamedOpaqueStructFacadeModel)({ external_name: "fran" })).toBe(false)
427
+ })
428
+
429
+ it("decodes through the public facade class", () => {
430
+ const decoded = S.decodeSync(FacadeModel)({ value: "hello" })
431
+ const decodedEffect = Effect.runSync(S.decodeUnknownEffect(FacadeModel)({ value: "effect" }))
432
+
433
+ expect(decoded).toBeInstanceOf(FacadeModel)
434
+ expect(decodedEffect).toBeInstanceOf(FacadeModel)
435
+ expect(decoded).toBeInstanceOf(_FacadeModel)
436
+ expect(decoded.value).toBe("hello")
437
+ expect(decoded.upperValue).toBe("HELLO")
438
+ expect(decoded.decoratedValue).toBe("facade:hello")
439
+ expect(decoded.label()).toBe("facade:hello")
440
+ expect(FacadeModel.describe(decoded)).toBe("value:hello")
441
+ })
442
+
443
+ it("validates facade instances with S.is", () => {
444
+ const instance = new FacadeModel({ value: "hello" })
445
+
446
+ expect(instance).toBeInstanceOf(FacadeModel)
447
+ expect(instance.upperValue).toBe("HELLO")
448
+ expect(instance.label()).toBe("facade:hello")
449
+ expect(S.is(FacadeModel)(instance)).toBe(true)
450
+ expect(S.is(FacadeModel)(S.decodeSync(FacadeModel)({ value: "world" }))).toBe(true)
451
+ expect(S.is(FacadeModel)({ value: "plain" })).toBe(false)
452
+ expect(S.is(FacadeModel)({ value: 1 })).toBe(false)
453
+ })
454
+
455
+ it("decodes renamed keys through the public facade class", () => {
456
+ const decoded = S.decodeSync(RenamedFacadeModel)({ external_name: "fran" })
457
+
458
+ expect(decoded).toBeInstanceOf(RenamedFacadeModel)
459
+ expect(decoded).toBeInstanceOf(_RenamedFacadeModel)
460
+ expect(Object.getPrototypeOf(decoded)).toBe(RenamedFacadeModel.prototype)
461
+ expect(decoded.internalName).toBe("fran")
462
+ expect(decoded.upperName).toBe("FRAN")
463
+ expect(decoded.decoratedName).toBe("facade:fran")
464
+ expect(S.encodeSync(RenamedFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" })
465
+ expect(S.is(RenamedFacadeModel)(decoded)).toBe(true)
466
+ expect(S.is(RenamedFacadeModel)({ internalName: "plain" })).toBe(false)
467
+ expect(S.is(RenamedFacadeModel)({ external_name: "plain" })).toBe(false)
468
+ })
469
+
470
+ it("keeps generated struct facades decodable with public statics", () => {
471
+ const input: typeof RequestContext.Encoded = {
472
+ span: { traceId: "trace", spanId: "span", sampled: true },
473
+ name: "operation",
474
+ locale: "en",
475
+ namespace: "tenant"
476
+ }
477
+
478
+ const decoded = S.decodeSync(RequestContext)(input)
479
+
480
+ expect(decoded).not.toBeInstanceOf(RequestContext)
481
+ expect(S.is(RequestContext)(decoded)).toBe(true)
482
+ expect(RequestContext.toMonitoring(decoded)).toStrictEqual({ operationName: "operation", locale: "en" })
483
+ })
484
+
485
+ it("keeps error facades class-only", () => {
486
+ const makeStructErrorFacade = () =>
487
+ AppSchema.OpaqueErrorFacadeClass<
488
+ { readonly value: string },
489
+ { readonly value: string },
490
+ { readonly value: string },
491
+ never,
492
+ never
493
+ >()(
494
+ // Runtime guard: only class schemas are accepted (struct schemas throw).
495
+ AppSchema.Struct({ value: S.String }) as never
496
+ )
497
+
498
+ expect(makeStructErrorFacade).toThrow("OpaqueErrorFacadeClass requires a class schema")
499
+ })
500
+ })
501
+
502
+ describe("Struct.copy", () => {
503
+ it("creates a new value with updated fields", () => {
504
+ const A = AppSchema.Struct({ a: S.String, b: S.Number })
505
+
506
+ const instance = A.make({ a: "hello", b: 1 })
507
+ const copied = A.copy(instance, { b: 2 })
508
+
509
+ expect(copied).toEqual({ a: "hello", b: 2 })
510
+ expect(copied).not.toBe(instance)
511
+ })
512
+
513
+ it("accepts a function for updates", () => {
514
+ const A = AppSchema.Struct({ a: S.String, b: S.Number })
515
+
516
+ const instance = A.make({ a: "hello", b: 1 })
517
+ const copied = A.copy(instance, (a) => ({ b: a.b + 1 }))
518
+
519
+ expect(copied).toEqual({ a: "hello", b: 2 })
520
+ })
521
+
522
+ it("allows widening updates against the full struct type", () => {
523
+ const A = AppSchema.Struct({
524
+ name: S.String,
525
+ state: S.Union([
526
+ S.Struct({ _tag: S.tag("a"), a: S.String }),
527
+ S.Struct({ _tag: S.tag("b"), b: S.Number })
528
+ ])
529
+ })
530
+
531
+ const instance = A.make({ name: "x", state: { _tag: "a", a: "a" } })
532
+ const copied = A.copy(instance, { state: { _tag: "b", b: 1 } })
533
+
534
+ expect(copied).toEqual({ name: "x", state: { _tag: "b", b: 1 } })
535
+ })
536
+
537
+ it("copy function is preserved when using .annotate()", () => {
538
+ const A = AppSchema.Struct({ a: S.String, b: S.Number }).annotate({ title: "A" })
539
+
540
+ const instance = A.make({ a: "hello", b: 1 })
541
+ const copied = A.copy(instance, { b: 2 })
542
+
543
+ expect(copied).toEqual({ a: "hello", b: 2 })
544
+ expect(copied).not.toBe(instance)
545
+ })
546
+
547
+ it("copy function is preserved when using .annotateKey()", () => {
548
+ const A = AppSchema.Struct({ a: S.String, b: S.Number }).annotateKey({ title: "A" })
549
+
550
+ const instance = A.make({ a: "hello", b: 1 })
551
+ const copied = A.copy(instance, { b: 2 })
552
+
553
+ expect(copied).toEqual({ a: "hello", b: 2 })
554
+ expect(copied).not.toBe(instance)
555
+ })
556
+
557
+ it("copy function is preserved when using .mapFields()", () => {
558
+ const A = AppSchema.Struct({ a: S.String, b: S.Number }).mapFields((f) => ({ ...f }))
559
+
560
+ const instance = A.make({ a: "hello", b: 1 })
561
+ const copied = A.copy(instance, { b: 2 })
562
+
563
+ expect(copied).toEqual({ a: "hello", b: 2 })
564
+ expect(copied).not.toBe(instance)
565
+ })
566
+
567
+ it("copy function is preserved through chained calls", () => {
568
+ const A = AppSchema
569
+ .Struct({ a: S.String, b: S.Number })
570
+ .annotate({ title: "A" })
571
+ .annotateKey({ description: "test" })
572
+
573
+ const instance = A.make({ a: "hello", b: 1 })
574
+ const copied = A.copy(instance, { b: 2 })
575
+
576
+ expect(copied).toEqual({ a: "hello", b: 2 })
577
+ expect(copied).not.toBe(instance)
578
+ })
579
+ })
580
+
581
+ describe("TaggedStruct.copy", () => {
582
+ it("creates a new tagged value with updated fields", () => {
583
+ const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number })
584
+
585
+ const instance = Circle.make({ radius: 5 })
586
+ const copied = Circle.copy(instance, { radius: 10 })
587
+
588
+ expect(copied).toEqual({ _tag: "Circle", radius: 10 })
589
+ expect(copied).not.toBe(instance)
590
+ })
591
+ })
592
+
143
593
  describe("TaggedError", () => {
144
594
  it("InvalidStateError toString includes the message", () => {
145
595
  const error = new InvalidStateError("something went wrong")
@@ -191,36 +641,36 @@ describe("TaggedError", () => {
191
641
 
192
642
  describe("SpecialJsonSchema", () => {
193
643
  it("nullable to optional — from NullOr", () => {
194
- const nullableDecodedUndefinedEncoded = (schema: Schema.Top) => {
644
+ const nullableDecodedUndefinedEncoded = (schema: S.Top) => {
195
645
  const isNullableSchema = "members" in schema
196
646
  && globalThis.Array.isArray((schema as any).members)
197
647
  && (schema as any).members.length === 2
198
648
  && (schema as any).members.some((member: any) => member.ast._tag === "Null")
199
649
 
200
- const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray<Schema.Top> : undefined
650
+ const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray<S.Top> : undefined
201
651
  const innerSchema = nullableMembers
202
652
  ? nullableMembers.find((member: any) => member.ast._tag !== "Null")!
203
653
  : schema
204
654
 
205
- const nullableSchema = isNullableSchema ? schema : Schema.NullOr(schema)
655
+ const nullableSchema = isNullableSchema ? schema : S.NullOr(schema)
206
656
 
207
657
  return nullableSchema.pipe(
208
- Schema.encodeTo(Schema.optionalKey(innerSchema), {
658
+ S.encodeTo(S.optionalKey(innerSchema), {
209
659
  decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
210
660
  encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
211
661
  })
212
662
  )
213
663
  }
214
664
 
215
- const fromNullOr = nullableDecodedUndefinedEncoded(Schema.NullOr(Schema.String))
216
- const structFromNullOr = Schema.Struct({ status: fromNullOr })
665
+ const fromNullOr = nullableDecodedUndefinedEncoded(S.NullOr(S.String))
666
+ const structFromNullOr = S.Struct({ status: fromNullOr })
217
667
 
218
- const encode = Schema.encodeUnknownSync(structFromNullOr as any)
219
- const encodedNull = encode({ status: null }) as any
668
+ const encode = S.encodeUnknownSync(structFromNullOr as any)
669
+ const encodedNull = encode({ status: null })
220
670
  expect("status" in encodedNull).toBe(false)
221
671
  expect(encode({ status: "test" })).toStrictEqual({ status: "test" })
222
672
 
223
- const decode = Schema.decodeUnknownSync(structFromNullOr as any)
673
+ const decode = S.decodeUnknownSync(structFromNullOr as any)
224
674
  expect(decode({})).toStrictEqual({ status: null })
225
675
  expect(decode({ status: "test" })).toStrictEqual({ status: "test" })
226
676
 
@@ -229,34 +679,34 @@ describe("SpecialJsonSchema", () => {
229
679
  dialect: "draft-2020-12",
230
680
  schema: {
231
681
  "type": "object",
682
+ "additionalProperties": true,
232
683
  "properties": {
233
684
  "status": { "type": "string" }
234
- },
235
- "additionalProperties": false
685
+ }
236
686
  },
237
687
  definitions: {}
238
688
  })
239
689
  })
240
690
 
241
691
  it("identifies X universally — deduplicates same-fingerprint references", () => {
242
- const X = Schema.String.annotate({ title: "X", identifier: "X" })
692
+ const X = S.String.annotate({ title: "X", identifier: "X" })
243
693
 
244
- const s = Schema.Struct({
245
- a: Schema.NullOr(X).pipe(
246
- Schema.encodeTo(Schema.optionalKey(X), {
694
+ const s = S.Struct({
695
+ a: S.NullOr(X).pipe(
696
+ S.encodeTo(S.optionalKey(X), {
247
697
  decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
248
698
  encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
249
699
  })
250
700
  ),
251
- b: Schema.NullOr(X).pipe(
252
- Schema.encodeTo(Schema.optionalKey(X), {
701
+ b: S.NullOr(X).pipe(
702
+ S.encodeTo(S.optionalKey(X), {
253
703
  decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
254
704
  encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
255
705
  })
256
706
  ),
257
- c: Schema.NullOr(X),
707
+ c: S.NullOr(X),
258
708
  d: X,
259
- e: X.pipe(Schema.optionalKey)
709
+ e: X.pipe(S.optionalKey)
260
710
  })
261
711
 
262
712
  const doc = specialJsonSchemaDocument(s)
@@ -264,6 +714,7 @@ describe("SpecialJsonSchema", () => {
264
714
  dialect: "draft-2020-12",
265
715
  schema: {
266
716
  "type": "object",
717
+ "additionalProperties": true,
267
718
  "properties": {
268
719
  "a": { "$ref": "#/$defs/X" },
269
720
  "b": { "$ref": "#/$defs/X" },
@@ -276,8 +727,7 @@ describe("SpecialJsonSchema", () => {
276
727
  "d": { "$ref": "#/$defs/X" },
277
728
  "e": { "$ref": "#/$defs/X" }
278
729
  },
279
- "required": ["c", "d"],
280
- "additionalProperties": false
730
+ "required": ["c", "d"]
281
731
  },
282
732
  definitions: {
283
733
  X: {
@@ -289,16 +739,16 @@ describe("SpecialJsonSchema", () => {
289
739
  })
290
740
 
291
741
  it("shared annotated schema via helper — deduplicates", () => {
292
- const X = Schema.String.annotate({ title: "X", identifier: "X" })
742
+ const X = S.String.annotate({ title: "X", identifier: "X" })
293
743
 
294
744
  const cache = new WeakMap()
295
- const nullableDecodedUndefinedEncoded = (schema: Schema.Top) => {
745
+ const nullableDecodedUndefinedEncoded = (schema: S.Top) => {
296
746
  const isNullableSchema = "members" in schema
297
747
  && globalThis.Array.isArray((schema as any).members)
298
748
  && (schema as any).members.length === 2
299
749
  && (schema as any).members.some((member: any) => member.ast._tag === "Null")
300
750
 
301
- const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray<Schema.Top> : undefined
751
+ const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray<S.Top> : undefined
302
752
  const innerSchema = nullableMembers
303
753
  ? nullableMembers.find((member: any) => member.ast._tag !== "Null")!
304
754
  : schema
@@ -306,9 +756,9 @@ describe("SpecialJsonSchema", () => {
306
756
  const cached = cache.get(innerSchema.ast)
307
757
  if (cached !== undefined) return cached
308
758
 
309
- const nullableSchema = isNullableSchema ? schema : Schema.NullOr(schema)
759
+ const nullableSchema = isNullableSchema ? schema : S.NullOr(schema)
310
760
  const out = nullableSchema.pipe(
311
- Schema.encodeTo(Schema.optionalKey(innerSchema), {
761
+ S.encodeTo(S.optionalKey(innerSchema), {
312
762
  decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
313
763
  encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
314
764
  })
@@ -318,12 +768,12 @@ describe("SpecialJsonSchema", () => {
318
768
  return out
319
769
  }
320
770
 
321
- const structWithShared = Schema.Struct({
771
+ const structWithShared = S.Struct({
322
772
  a: nullableDecodedUndefinedEncoded(X),
323
- b: nullableDecodedUndefinedEncoded(Schema.NullOr(X)),
324
- c: Schema.NullOr(X),
773
+ b: nullableDecodedUndefinedEncoded(S.NullOr(X)),
774
+ c: S.NullOr(X),
325
775
  d: X,
326
- e: X.pipe(Schema.optionalKey)
776
+ e: X.pipe(S.optionalKey)
327
777
  })
328
778
 
329
779
  const doc = specialJsonSchemaDocument(structWithShared)
@@ -331,6 +781,7 @@ describe("SpecialJsonSchema", () => {
331
781
  dialect: "draft-2020-12",
332
782
  schema: {
333
783
  "type": "object",
784
+ "additionalProperties": true,
334
785
  "properties": {
335
786
  "a": { "$ref": "#/$defs/X" },
336
787
  "b": { "$ref": "#/$defs/X" },
@@ -343,8 +794,7 @@ describe("SpecialJsonSchema", () => {
343
794
  "d": { "$ref": "#/$defs/X" },
344
795
  "e": { "$ref": "#/$defs/X" }
345
796
  },
346
- "required": ["c", "d"],
347
- "additionalProperties": false
797
+ "required": ["c", "d"]
348
798
  },
349
799
  definitions: {
350
800
  X: {
@@ -658,8 +1108,7 @@ describe("Post-processing integration — real Effect Schema types", () => {
658
1108
  const doc = specialJsonSchemaDocument(AppSchema.PositiveInt)
659
1109
  expect(doc.definitions["PositiveInt"]).toStrictEqual({
660
1110
  type: "integer",
661
- exclusiveMinimum: 0,
662
- title: "PositiveInt"
1111
+ exclusiveMinimum: 0
663
1112
  })
664
1113
  })
665
1114
 
@@ -668,8 +1117,7 @@ describe("Post-processing integration — real Effect Schema types", () => {
668
1117
  expect(doc.definitions["NonEmptyString255"]).toStrictEqual({
669
1118
  type: "string",
670
1119
  minLength: 1,
671
- maxLength: 255,
672
- title: "NonEmptyString255"
1120
+ maxLength: 255
673
1121
  })
674
1122
  })
675
1123
 
@@ -680,6 +1128,7 @@ describe("Post-processing integration — real Effect Schema types", () => {
680
1128
  // null variant preserved (correct JSON Schema for NullOr)
681
1129
  expect(doc.schema).toStrictEqual({
682
1130
  type: "object",
1131
+ additionalProperties: true,
683
1132
  properties: {
684
1133
  note: {
685
1134
  anyOf: [
@@ -688,16 +1137,14 @@ describe("Post-processing integration — real Effect Schema types", () => {
688
1137
  ]
689
1138
  }
690
1139
  },
691
- required: ["note"],
692
- additionalProperties: false
1140
+ required: ["note"]
693
1141
  })
694
1142
 
695
1143
  // allOf flattened in the referenced definition
696
1144
  expect(doc.definitions["NonEmptyString64k"]).toStrictEqual({
697
1145
  type: "string",
698
1146
  minLength: 1,
699
- maxLength: 65536,
700
- title: "NonEmptyString64k"
1147
+ maxLength: 65536
701
1148
  })
702
1149
  })
703
1150
 
@@ -705,14 +1152,13 @@ describe("Post-processing integration — real Effect Schema types", () => {
705
1152
  const doc = specialJsonSchemaDocument(AppSchema.NonNegativeInt)
706
1153
  expect(doc.definitions["NonNegativeInt"]).toStrictEqual({
707
1154
  type: "integer",
708
- minimum: 0,
709
- title: "NonNegativeInt"
1155
+ minimum: 0
710
1156
  })
711
1157
  })
712
1158
 
713
- it("NullOr union preserves null and non-null members in anyOf", () => {
714
- const A = S.String.annotate({ identifier: "A", title: "A" })
715
- const B = S.Boolean.annotate({ identifier: "B", title: "B" })
1159
+ it("NullOr union flattens nested anyOf members", () => {
1160
+ const A = S.String.annotate({ identifier: "A" })
1161
+ const B = S.Boolean.annotate({ identifier: "B" })
716
1162
  const schema = S.Struct({
717
1163
  value: S.NullOr(S.Union([A, B]))
718
1164
  })
@@ -721,9 +1167,95 @@ describe("Post-processing integration — real Effect Schema types", () => {
721
1167
 
722
1168
  expect(valueProp).toStrictEqual({
723
1169
  anyOf: [
724
- { anyOf: [{ $ref: "#/$defs/A" }, { $ref: "#/$defs/B" }] },
1170
+ { $ref: "#/$defs/A" },
1171
+ { $ref: "#/$defs/B" },
725
1172
  { type: "null" }
726
1173
  ]
727
1174
  })
728
1175
  })
729
1176
  })
1177
+
1178
+ describe("flattenNestedAnyOf", () => {
1179
+ it("flattens nested anyOf with no sibling keys", () => {
1180
+ const input = {
1181
+ anyOf: [
1182
+ { anyOf: [{ type: "string" }, { type: "number" }] },
1183
+ { type: "null" }
1184
+ ]
1185
+ }
1186
+ expect(flattenNestedAnyOf(input)).toStrictEqual({
1187
+ anyOf: [
1188
+ { type: "string" },
1189
+ { type: "number" },
1190
+ { type: "null" }
1191
+ ]
1192
+ })
1193
+ })
1194
+
1195
+ it("does not flatten anyOf entry with sibling keys", () => {
1196
+ const input = {
1197
+ anyOf: [
1198
+ { anyOf: [{ type: "string" }], title: "X" },
1199
+ { type: "null" }
1200
+ ]
1201
+ }
1202
+ // The inner anyOf is not flattened into the outer (sibling "title" prevents it),
1203
+ // but the single-element inner anyOf is unwrapped within the entry itself
1204
+ expect(flattenNestedAnyOf(input)).toStrictEqual({
1205
+ anyOf: [
1206
+ { type: "string", title: "X" },
1207
+ { type: "null" }
1208
+ ]
1209
+ })
1210
+ })
1211
+
1212
+ it("unwraps anyOf with single item after flattening", () => {
1213
+ const input = {
1214
+ anyOf: [
1215
+ { anyOf: [{ type: "string" }] }
1216
+ ]
1217
+ }
1218
+ expect(flattenNestedAnyOf(input)).toStrictEqual({ type: "string" })
1219
+ })
1220
+
1221
+ it("unwraps anyOf with single item, merging sibling properties", () => {
1222
+ const input = {
1223
+ title: "MyField",
1224
+ anyOf: [{ type: "string" }]
1225
+ }
1226
+ expect(flattenNestedAnyOf(input)).toStrictEqual({
1227
+ title: "MyField",
1228
+ type: "string"
1229
+ })
1230
+ })
1231
+
1232
+ it("recurses into nested objects", () => {
1233
+ const input = {
1234
+ properties: {
1235
+ field: {
1236
+ anyOf: [
1237
+ { anyOf: [{ $ref: "#/defs/A" }, { $ref: "#/defs/B" }] },
1238
+ { type: "null" }
1239
+ ]
1240
+ }
1241
+ }
1242
+ }
1243
+ expect(flattenNestedAnyOf(input)).toStrictEqual({
1244
+ properties: {
1245
+ field: {
1246
+ anyOf: [
1247
+ { $ref: "#/defs/A" },
1248
+ { $ref: "#/defs/B" },
1249
+ { type: "null" }
1250
+ ]
1251
+ }
1252
+ }
1253
+ })
1254
+ })
1255
+
1256
+ it("passes through non-objects unchanged", () => {
1257
+ expect(flattenNestedAnyOf(null)).toBe(null)
1258
+ expect(flattenNestedAnyOf(42)).toBe(42)
1259
+ expect(flattenNestedAnyOf("hello")).toBe("hello")
1260
+ })
1261
+ })