effect-app 4.0.0-beta.18 → 4.0.0-beta.180

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 (218) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/Array.d.ts +1 -1
  3. package/dist/Chunk.d.ts +1 -1
  4. package/dist/Chunk.d.ts.map +1 -1
  5. package/dist/Config/SecretURL.d.ts +1 -1
  6. package/dist/Config/SecretURL.d.ts.map +1 -1
  7. package/dist/Config/SecretURL.js +2 -2
  8. package/dist/Config/internal/configSecretURL.d.ts +1 -1
  9. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  10. package/dist/Config.d.ts +7 -0
  11. package/dist/Config.d.ts.map +1 -0
  12. package/dist/Config.js +6 -0
  13. package/dist/ConfigProvider.d.ts +39 -0
  14. package/dist/ConfigProvider.d.ts.map +1 -0
  15. package/dist/ConfigProvider.js +42 -0
  16. package/dist/Context.d.ts +40 -0
  17. package/dist/Context.d.ts.map +1 -0
  18. package/dist/Context.js +67 -0
  19. package/dist/Effect.d.ts +9 -10
  20. package/dist/Effect.d.ts.map +1 -1
  21. package/dist/Effect.js +3 -6
  22. package/dist/Function.d.ts +1 -1
  23. package/dist/Function.d.ts.map +1 -1
  24. package/dist/Inputify.type.d.ts +1 -1
  25. package/dist/Layer.d.ts +6 -5
  26. package/dist/Layer.d.ts.map +1 -1
  27. package/dist/Layer.js +1 -1
  28. package/dist/NonEmptySet.d.ts +1 -1
  29. package/dist/NonEmptySet.d.ts.map +1 -1
  30. package/dist/Operations.d.ts +369 -47
  31. package/dist/Operations.d.ts.map +1 -1
  32. package/dist/Operations.js +10 -10
  33. package/dist/Option.d.ts +1 -1
  34. package/dist/Option.d.ts.map +1 -1
  35. package/dist/Pure.d.ts +5 -5
  36. package/dist/Pure.d.ts.map +1 -1
  37. package/dist/Pure.js +13 -13
  38. package/dist/Schema/Class.d.ts +69 -20
  39. package/dist/Schema/Class.d.ts.map +1 -1
  40. package/dist/Schema/Class.js +190 -22
  41. package/dist/Schema/FastCheck.d.ts +1 -1
  42. package/dist/Schema/FastCheck.d.ts.map +1 -1
  43. package/dist/Schema/Methods.d.ts +1 -1
  44. package/dist/Schema/SchemaParser.d.ts +5 -0
  45. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  46. package/dist/Schema/SchemaParser.js +6 -0
  47. package/dist/Schema/SpecialJsonSchema.d.ts +33 -0
  48. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  49. package/dist/Schema/SpecialJsonSchema.js +122 -0
  50. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  51. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  52. package/dist/Schema/SpecialOpenApi.js +123 -0
  53. package/dist/Schema/brand.d.ts +7 -2
  54. package/dist/Schema/brand.d.ts.map +1 -1
  55. package/dist/Schema/brand.js +1 -1
  56. package/dist/Schema/email.d.ts +1 -1
  57. package/dist/Schema/email.d.ts.map +1 -1
  58. package/dist/Schema/email.js +7 -4
  59. package/dist/Schema/ext.d.ts +118 -45
  60. package/dist/Schema/ext.d.ts.map +1 -1
  61. package/dist/Schema/ext.js +129 -53
  62. package/dist/Schema/moreStrings.d.ts +111 -11
  63. package/dist/Schema/moreStrings.d.ts.map +1 -1
  64. package/dist/Schema/moreStrings.js +14 -15
  65. package/dist/Schema/numbers.d.ts +127 -15
  66. package/dist/Schema/numbers.d.ts.map +1 -1
  67. package/dist/Schema/numbers.js +10 -12
  68. package/dist/Schema/phoneNumber.d.ts +1 -1
  69. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  70. package/dist/Schema/phoneNumber.js +6 -3
  71. package/dist/Schema/schema.d.ts +1 -1
  72. package/dist/Schema/strings.d.ts +37 -5
  73. package/dist/Schema/strings.d.ts.map +1 -1
  74. package/dist/Schema/strings.js +1 -5
  75. package/dist/Schema.d.ts +102 -56
  76. package/dist/Schema.d.ts.map +1 -1
  77. package/dist/Schema.js +128 -64
  78. package/dist/Set.d.ts +1 -1
  79. package/dist/Set.d.ts.map +1 -1
  80. package/dist/TypeTest.d.ts +1 -1
  81. package/dist/Types.d.ts +1 -1
  82. package/dist/Widen.type.d.ts +1 -1
  83. package/dist/_ext/Array.d.ts +1 -1
  84. package/dist/_ext/Array.d.ts.map +1 -1
  85. package/dist/_ext/date.d.ts +1 -1
  86. package/dist/_ext/misc.d.ts +1 -1
  87. package/dist/_ext/ord.ext.d.ts +1 -1
  88. package/dist/_ext/ord.ext.d.ts.map +1 -1
  89. package/dist/builtin.d.ts +1 -1
  90. package/dist/builtin.d.ts.map +1 -1
  91. package/dist/client/InvalidationKeys.d.ts +29 -0
  92. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  93. package/dist/client/InvalidationKeys.js +33 -0
  94. package/dist/client/apiClientFactory.d.ts +17 -31
  95. package/dist/client/apiClientFactory.d.ts.map +1 -1
  96. package/dist/client/apiClientFactory.js +81 -26
  97. package/dist/client/clientFor.d.ts +63 -10
  98. package/dist/client/clientFor.d.ts.map +1 -1
  99. package/dist/client/clientFor.js +9 -1
  100. package/dist/client/errors.d.ts +49 -25
  101. package/dist/client/errors.d.ts.map +1 -1
  102. package/dist/client/errors.js +43 -17
  103. package/dist/client/makeClient.d.ts +360 -30
  104. package/dist/client/makeClient.d.ts.map +1 -1
  105. package/dist/client/makeClient.js +63 -23
  106. package/dist/client.d.ts +2 -1
  107. package/dist/client.d.ts.map +1 -1
  108. package/dist/client.js +2 -1
  109. package/dist/faker.d.ts +1 -1
  110. package/dist/faker.d.ts.map +1 -1
  111. package/dist/http/Request.d.ts +2 -2
  112. package/dist/http/Request.d.ts.map +1 -1
  113. package/dist/http/Request.js +5 -5
  114. package/dist/http/internal/lib.d.ts +1 -1
  115. package/dist/http.d.ts +1 -1
  116. package/dist/ids.d.ts +3 -3
  117. package/dist/ids.d.ts.map +1 -1
  118. package/dist/ids.js +3 -2
  119. package/dist/index.d.ts +5 -8
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +6 -8
  122. package/dist/logger.d.ts +1 -1
  123. package/dist/middleware.d.ts +16 -9
  124. package/dist/middleware.d.ts.map +1 -1
  125. package/dist/middleware.js +13 -9
  126. package/dist/rpc/Invalidation.d.ts +490 -0
  127. package/dist/rpc/Invalidation.d.ts.map +1 -0
  128. package/dist/rpc/Invalidation.js +129 -0
  129. package/dist/rpc/MiddlewareMaker.d.ts +5 -4
  130. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  131. package/dist/rpc/MiddlewareMaker.js +26 -27
  132. package/dist/rpc/RpcContextMap.d.ts +3 -3
  133. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  134. package/dist/rpc/RpcContextMap.js +4 -4
  135. package/dist/rpc/RpcMiddleware.d.ts +5 -4
  136. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  137. package/dist/rpc/RpcMiddleware.js +1 -1
  138. package/dist/rpc.d.ts +2 -2
  139. package/dist/rpc.d.ts.map +1 -1
  140. package/dist/rpc.js +2 -2
  141. package/dist/transform.d.ts +1 -1
  142. package/dist/transform.d.ts.map +1 -1
  143. package/dist/transform.js +3 -3
  144. package/dist/utils/effectify.d.ts +1 -1
  145. package/dist/utils/extend.d.ts +1 -1
  146. package/dist/utils/extend.d.ts.map +1 -1
  147. package/dist/utils/gen.d.ts +2 -2
  148. package/dist/utils/gen.d.ts.map +1 -1
  149. package/dist/utils/logLevel.d.ts +2 -2
  150. package/dist/utils/logLevel.d.ts.map +1 -1
  151. package/dist/utils/logger.d.ts +3 -3
  152. package/dist/utils/logger.d.ts.map +1 -1
  153. package/dist/utils/logger.js +3 -3
  154. package/dist/utils.d.ts +30 -10
  155. package/dist/utils.d.ts.map +1 -1
  156. package/dist/utils.js +10 -4
  157. package/dist/validation/validators.d.ts +1 -1
  158. package/dist/validation/validators.d.ts.map +1 -1
  159. package/dist/validation.d.ts +1 -1
  160. package/dist/validation.d.ts.map +1 -1
  161. package/eslint.config.mjs +2 -2
  162. package/package.json +47 -19
  163. package/src/Config/SecretURL.ts +2 -1
  164. package/src/Config.ts +14 -0
  165. package/src/ConfigProvider.ts +48 -0
  166. package/src/{ServiceMap.ts → Context.ts} +52 -59
  167. package/src/Effect.ts +12 -14
  168. package/src/Layer.ts +5 -4
  169. package/src/Operations.ts +14 -14
  170. package/src/Pure.ts +17 -18
  171. package/src/Schema/Class.ts +279 -62
  172. package/src/Schema/SchemaParser.ts +12 -0
  173. package/src/Schema/SpecialJsonSchema.ts +137 -0
  174. package/src/Schema/SpecialOpenApi.ts +130 -0
  175. package/src/Schema/brand.ts +9 -1
  176. package/src/Schema/email.ts +7 -2
  177. package/src/Schema/ext.ts +210 -80
  178. package/src/Schema/moreStrings.ts +22 -20
  179. package/src/Schema/numbers.ts +14 -16
  180. package/src/Schema/phoneNumber.ts +5 -1
  181. package/src/Schema/strings.ts +4 -8
  182. package/src/Schema.ts +265 -105
  183. package/src/client/InvalidationKeys.ts +50 -0
  184. package/src/client/apiClientFactory.ts +203 -119
  185. package/src/client/clientFor.ts +112 -23
  186. package/src/client/errors.ts +52 -26
  187. package/src/client/makeClient.ts +339 -63
  188. package/src/client.ts +1 -0
  189. package/src/http/Request.ts +7 -4
  190. package/src/ids.ts +2 -1
  191. package/src/index.ts +5 -10
  192. package/src/middleware.ts +12 -10
  193. package/src/rpc/Invalidation.ts +174 -0
  194. package/src/rpc/MiddlewareMaker.ts +36 -47
  195. package/src/rpc/README.md +2 -2
  196. package/src/rpc/RpcContextMap.ts +6 -5
  197. package/src/rpc/RpcMiddleware.ts +5 -4
  198. package/src/rpc.ts +1 -1
  199. package/src/transform.ts +2 -2
  200. package/src/utils/gen.ts +1 -1
  201. package/src/utils/logger.ts +2 -2
  202. package/src/utils.ts +47 -11
  203. package/test/dist/rpc.test.d.ts.map +1 -1
  204. package/test/dist/secretURL.test.d.ts.map +1 -0
  205. package/test/dist/special.test.d.ts.map +1 -0
  206. package/test/rpc.test.ts +38 -6
  207. package/test/schema.test.ts +591 -17
  208. package/test/secretURL.test.ts +157 -0
  209. package/test/special.test.ts +1023 -0
  210. package/test/utils.test.ts +6 -6
  211. package/tsconfig.base.json +3 -4
  212. package/tsconfig.json +0 -1
  213. package/tsconfig.json.bak +2 -2
  214. package/tsconfig.src.json +29 -29
  215. package/tsconfig.test.json +2 -2
  216. package/dist/ServiceMap.d.ts +0 -44
  217. package/dist/ServiceMap.d.ts.map +0 -1
  218. package/dist/ServiceMap.js +0 -91
@@ -1,101 +1,318 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { pipe, Struct as Struct2 } from "effect"
3
- import type { Struct } from "effect/Schema"
2
+ import { type Cause, Effect, Option, Schema, SchemaAST, SchemaIssue } from "effect"
4
3
  import * as S from "effect/Schema"
4
+ import { copyOrigin } from "../utils.js"
5
+ import { concurrencyUnbounded } from "./ext.js"
6
+ import * as SchemaParser from "./SchemaParser.js"
5
7
 
6
8
  type ClassAnnotations<Self> = S.Annotations.Declaration<Self, readonly [any]>
7
9
 
8
- export interface EnhancedClass<Self, SchemaS extends S.Top & { readonly fields: Struct.Fields }, Inherited>
9
- extends S.Class<Self, SchemaS, Inherited>, /* Reason for enhancement */ PropsExtensions<SchemaS["fields"]>
10
+ export interface EnhancedClass<Self, SchemaS extends S.Top & { readonly fields: S.Struct.Fields }, Inherited>
11
+ extends S.Class<Self, SchemaS, Inherited>
10
12
  {
13
+ /**
14
+ * See `copyOrigin` docs in `utils.ts` for return-type design details.
15
+ */
16
+ readonly copy: ReturnType<typeof copyOrigin<new(_: any) => Self>>
11
17
  }
12
18
  type MissingSelfGeneric<Usage extends string, Params extends string = ""> =
13
19
  `Missing \`Self\` generic - use \`class Self extends ${Usage}<Self>()(${Params}{ ... })\``
14
20
 
15
- export interface PropsExtensions<Fields> {
16
- // include: <NewProps extends S.Struct.Fields>(
17
- // fnc: (fields: Fields) => NewProps
18
- // ) => NewProps
19
- pick: <P extends keyof Fields>(...keys: readonly P[]) => Pick<Fields, P>
20
- omit: <P extends keyof Fields>(...keys: readonly P[]) => Omit<Fields, P>
21
- }
22
-
23
- type HasFields<Fields extends Struct.Fields> = {
21
+ type HasFields<Fields extends S.Struct.Fields> = {
24
22
  readonly fields: Fields
25
23
  } | {
26
24
  readonly from: HasFields<Fields>
27
25
  }
28
26
 
29
- export const Class: <Self = never>(identifier: string) => <Fields extends S.Struct.Fields>(
27
+ type ClassOptions = {
28
+ readonly strict?: boolean
29
+ }
30
+
31
+ declare const ExtendedSchemaNoEncoded: unique symbol
32
+
33
+ type ExtendedSchemaNoEncoded = typeof ExtendedSchemaNoEncoded
34
+
35
+ type WithEncoded<SchemaS extends S.Top, Encoded> = Omit<SchemaS, "Encoded"> & { readonly Encoded: Encoded }
36
+
37
+ type ExtendedSchema<SchemaS extends S.Top, Encoded> = [Encoded] extends [ExtendedSchemaNoEncoded] ? SchemaS
38
+ : WithEncoded<SchemaS, Encoded>
39
+
40
+ type OptionalMakeSurface<SchemaS extends S.Top> = {} extends SchemaS["~type.make.in"] ? {
41
+ make(input?: SchemaS["~type.make.in"], options?: S.MakeOptions): SchemaS["Type"]
42
+ makeOption(input?: SchemaS["~type.make.in"], options?: S.MakeOptions): Option.Option<SchemaS["Type"]>
43
+ makeEffect(
44
+ input?: SchemaS["~type.make.in"],
45
+ options?: S.MakeOptions
46
+ ): Effect.Effect<SchemaS["Type"], S.SchemaError>
47
+ }
48
+ : {}
49
+
50
+ export type Class<Self, S extends S.Top & { readonly fields: S.Struct.Fields }, Inherited> = EnhancedClass<
51
+ Self,
52
+ S,
53
+ Inherited
54
+ >
55
+
56
+ /**
57
+ * Build a modified Declaration that accepts struct-matching values during
58
+ * encoding, given the original Declaration and the class's fields.
59
+ */
60
+ function makeRelaxedDeclaration(
61
+ ast: SchemaAST.Declaration,
62
+ fields: Schema.Struct.Fields,
63
+ cls: any
64
+ ): SchemaAST.Declaration {
65
+ const parseOptions = ast.annotations?.["parseOptions"] as SchemaAST.ParseOptions | undefined
66
+ const structSchema = Schema.Struct(fields)
67
+ const annotatedStruct = parseOptions ? S.toType(structSchema).annotate({ parseOptions }) : S.toType(structSchema)
68
+ const decodeStruct = SchemaParser.decodeUnknownEffect(annotatedStruct)
69
+
70
+ return new SchemaAST.Declaration(
71
+ ast.typeParameters,
72
+ () => (input: unknown, self: SchemaAST.Declaration, options: SchemaAST.ParseOptions) => {
73
+ if (input instanceof cls) {
74
+ return Effect.succeed(input)
75
+ }
76
+ if (input !== null && typeof input === "object") {
77
+ return decodeStruct(input, options)
78
+ }
79
+ return Effect.fail(new SchemaIssue.InvalidType(self, Option.some(input)))
80
+ },
81
+ ast.annotations,
82
+ ast.checks,
83
+ ast.encoding,
84
+ ast.context
85
+ )
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Class — like Schema.Class but with relaxed encoding
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Like `Schema.Class`, but the resulting class accepts plain objects matching
94
+ * the struct schema during encoding — not only `instanceof` or type-id
95
+ * checks.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * import { Schema } from "effect"
100
+ * import { Class } from "./Class.js"
101
+ *
102
+ * class A extends Class<A>("A")({ a: Schema.String }) {}
103
+ *
104
+ * // Construction works as normal:
105
+ * new A({ a: "hello" })
106
+ *
107
+ * // Encoding accepts plain objects:
108
+ * Schema.encodeUnknownSync(A)({ a: "hello" }) // { a: "hello" }
109
+ * ```
110
+ */
111
+ export const Class: <Self = never, Encoded = ExtendedSchemaNoEncoded>(
112
+ identifier: string
113
+ ) => <Fields extends S.Struct.Fields>(
30
114
  fieldsOr: Fields | HasFields<Fields>,
31
- annotations?: ClassAnnotations<Self>
115
+ annotations?: ClassAnnotations<Self>,
116
+ options?: ClassOptions
32
117
  ) => [Self] extends [never] ? MissingSelfGeneric<"Class">
33
118
  : EnhancedClass<
34
119
  Self,
35
- S.Struct<Fields>,
120
+ ExtendedSchema<S.Struct<Fields>, Encoded>,
36
121
  {}
37
- > = (identifier) => (fields, annotations) => {
38
- const cls = S.Class as any
39
- return class extends cls(identifier)(fields, annotations) {
40
- constructor(a: any, b = true) {
41
- super(a, b)
122
+ > = (identifier) => (fields, annotations, options) => {
123
+ const relaxed = options?.strict === false
124
+ // Build the original Schema.Class
125
+ const Base = (S.Class as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations })
126
+ // Get the original ast getter from the base class
127
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
128
+
129
+ // Cache per-class to avoid recomputing
130
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
131
+ const copyCache = new WeakMap<any, ReturnType<typeof copyOrigin>>()
132
+
133
+ return class extends Base {
134
+ static get copy() {
135
+ let cached = copyCache.get(this)
136
+ if (cached === undefined) {
137
+ cached = copyOrigin(this)
138
+ copyCache.set(this, cached)
139
+ }
140
+ return cached
141
+ }
142
+ static get ast(): SchemaAST.Declaration {
143
+ let cached = astCache.get(this)
144
+ if (cached !== undefined) return cached
145
+ // Call the original getter with `this` bound to the actual user class,
146
+ // so getClassSchema(this) creates a schema that uses `new this(...)`.
147
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
148
+ cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst
149
+ astCache.set(this, cached)
150
+ return cached
151
+ }
152
+ static mapFields(f: any, options?: any) {
153
+ return Base.mapFields(f, options).annotate(concurrencyUnbounded)
42
154
  }
43
- // static readonly include = include(fields)
44
- static readonly pick = (...selection: any[]) => pipe(this["fields"], Struct2.pick(selection))
45
- static readonly omit = (...selection: any[]) => pipe(this["fields"], Struct2.omit(selection))
46
155
  } as any
47
156
  }
48
157
 
49
- export const TaggedClass: <Self = never>(identifier?: string) => <Tag extends string, Fields extends S.Struct.Fields>(
158
+ // ---------------------------------------------------------------------------
159
+ // TaggedClass — like Schema.TaggedClass but with relaxed encoding
160
+ // ---------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Like `Schema.TaggedClass`, but the resulting class accepts plain objects
164
+ * matching the struct schema during encoding.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { Schema } from "effect"
169
+ * import { TaggedClass } from "./Class.js"
170
+ *
171
+ * class Circle extends TaggedClass<Circle>()("Circle", {
172
+ * radius: Schema.Number
173
+ * }) {}
174
+ *
175
+ * Schema.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 })
176
+ * ```
177
+ */
178
+ export const TaggedClass: <Self = never, Encoded = ExtendedSchemaNoEncoded>(
179
+ identifier?: string
180
+ ) => <Tag extends string, Fields extends S.Struct.Fields>(
50
181
  tag: Tag,
51
182
  fieldsOr: Fields | HasFields<Fields>,
52
- annotations?: ClassAnnotations<Self>
53
- ) => [Self] extends [never] ? MissingSelfGeneric<"Class">
183
+ annotations?: ClassAnnotations<Self>,
184
+ options?: ClassOptions
185
+ ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedClass">
54
186
  : EnhancedClass<
55
187
  Self,
56
- S.Struct<{ readonly _tag: S.tag<Tag> } & Fields>,
188
+ ExtendedSchema<S.Struct<{ readonly _tag: S.tag<Tag> } & Fields>, Encoded>,
57
189
  {}
58
- > = (identifier) => (tag, fields, annotations) => {
59
- const cls = S.TaggedClass as any
60
- return class extends cls(identifier)(tag, fields, annotations) {
61
- constructor(a: any, b = true) {
62
- super(a, b)
190
+ > = (identifier) => (tag, fields, annotations, options) => {
191
+ const relaxed = options?.strict === false
192
+ const Base = (S.TaggedClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations })
193
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
194
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
195
+ const copyCache = new WeakMap<any, ReturnType<typeof copyOrigin>>()
196
+
197
+ return class extends Base {
198
+ static get copy() {
199
+ let cached = copyCache.get(this)
200
+ if (cached === undefined) {
201
+ cached = copyOrigin(this)
202
+ copyCache.set(this, cached)
203
+ }
204
+ return cached
205
+ }
206
+ static get ast(): SchemaAST.Declaration {
207
+ let cached = astCache.get(this)
208
+ if (cached !== undefined) return cached
209
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
210
+ cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst
211
+ astCache.set(this, cached)
212
+ return cached
213
+ }
214
+ static mapFields(f: any, options?: any) {
215
+ return Base.mapFields(f, options).annotate(concurrencyUnbounded)
63
216
  }
64
- // static readonly include = include(fields)
65
- static readonly pick = (...selection: any[]) => pipe(this["fields"], Struct2.pick(selection))
66
- static readonly omit = (...selection: any[]) => pipe(this["fields"], Struct2.omit(selection))
67
217
  } as any
68
218
  }
69
219
 
70
- export const ExtendedClass: <Self, _SelfFrom>(identifier: string) => <Fields extends S.Struct.Fields>(
220
+ // ---------------------------------------------------------------------------
221
+ // ErrorClass — like Schema.ErrorClass but with relaxed encoding
222
+ // ---------------------------------------------------------------------------
223
+
224
+ export const ErrorClass: <Self = never, Encoded = ExtendedSchemaNoEncoded, Brand = {}>(
225
+ identifier: string
226
+ ) => <Fields extends S.Struct.Fields>(
71
227
  fieldsOr: Fields | HasFields<Fields>,
72
- annotations?: ClassAnnotations<Self>
73
- ) => EnhancedClass<
74
- Self,
75
- S.Struct<Fields>,
76
- {}
77
- > = Class as any
78
-
79
- export interface EnhancedTaggedClass<Self, Tag extends string, Fields extends Struct.Fields, SelfFrom>
80
- extends
81
- EnhancedClass<
82
- Self,
83
- S.Struct<Fields> & { readonly Encoded: SelfFrom },
84
- {}
85
- >
86
- {
87
- readonly _tag: Tag
88
- }
228
+ annotations?: ClassAnnotations<Self>,
229
+ options?: ClassOptions
230
+ ) => [Self] extends [never] ? MissingSelfGeneric<"ErrorClass">
231
+ : EnhancedClass<
232
+ Self,
233
+ ExtendedSchema<S.Struct<Fields>, Encoded>,
234
+ Cause.YieldableError & Brand
235
+ > = (identifier) => (fields, annotations, options) => {
236
+ const relaxed = options?.strict === false
237
+ const Base = (S.ErrorClass as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations })
238
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
239
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
240
+ const copyCache = new WeakMap<any, ReturnType<typeof copyOrigin>>()
241
+
242
+ return class extends Base {
243
+ static get copy() {
244
+ let cached = copyCache.get(this)
245
+ if (cached === undefined) {
246
+ cached = copyOrigin(this)
247
+ copyCache.set(this, cached)
248
+ }
249
+ return cached
250
+ }
251
+ static get ast(): SchemaAST.Declaration {
252
+ let cached = astCache.get(this)
253
+ if (cached !== undefined) return cached
254
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
255
+ cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst
256
+ astCache.set(this, cached)
257
+ return cached
258
+ }
259
+ static mapFields(f: any, options?: any) {
260
+ return Base.mapFields(f, options).annotate(concurrencyUnbounded)
261
+ }
262
+ } as any
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // TaggedErrorClass — like Schema.TaggedErrorClass but with relaxed encoding
267
+ // ---------------------------------------------------------------------------
89
268
 
90
- export const ExtendedTaggedClass: <Self, SelfFrom>(
269
+ export const TaggedErrorClass: <Self = never, Encoded = ExtendedSchemaNoEncoded, Brand = {}>(
91
270
  identifier?: string
92
271
  ) => <Tag extends string, Fields extends S.Struct.Fields>(
93
272
  tag: Tag,
94
273
  fieldsOr: Fields | HasFields<Fields>,
95
- annotations?: ClassAnnotations<Self>
96
- ) => EnhancedTaggedClass<
97
- Self,
98
- Tag,
99
- { readonly _tag: S.tag<Tag> } & Fields,
100
- SelfFrom
101
- > = TaggedClass as any
274
+ annotations?: ClassAnnotations<Self>,
275
+ options?: ClassOptions
276
+ ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedErrorClass">
277
+ : EnhancedClass<
278
+ Self,
279
+ ExtendedSchema<S.Struct<{ readonly _tag: S.tag<Tag> } & Fields>, Encoded>,
280
+ Cause.YieldableError & Brand
281
+ > = (identifier) => (tag, fields, annotations, options) => {
282
+ const relaxed = options?.strict === false
283
+ const Base = (S.TaggedErrorClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations })
284
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
285
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
286
+ const copyCache = new WeakMap<any, ReturnType<typeof copyOrigin>>()
287
+
288
+ return class extends Base {
289
+ static get copy() {
290
+ let cached = copyCache.get(this)
291
+ if (cached === undefined) {
292
+ cached = copyOrigin(this)
293
+ copyCache.set(this, cached)
294
+ }
295
+ return cached
296
+ }
297
+ static get ast(): SchemaAST.Declaration {
298
+ let cached = astCache.get(this)
299
+ if (cached !== undefined) return cached
300
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
301
+ cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst
302
+ astCache.set(this, cached)
303
+ return cached
304
+ }
305
+ static mapFields(f: any, options?: any) {
306
+ return Base.mapFields(f, options).annotate(concurrencyUnbounded)
307
+ }
308
+ } as any
309
+ }
310
+
311
+ const ExtendedOpaque: <Self, Encoded = ExtendedSchemaNoEncoded, Brand = {}>() => <SchemaS extends S.Top>(
312
+ schema: SchemaS
313
+ ) =>
314
+ & S.Opaque<Self, ExtendedSchema<SchemaS, Encoded>, Brand>
315
+ & Omit<SchemaS, keyof S.Top>
316
+ & OptionalMakeSurface<S.Opaque<Self, ExtendedSchema<SchemaS, Encoded>, Brand>> = S.Opaque as any
317
+
318
+ export const Opaque = ExtendedOpaque
@@ -0,0 +1,12 @@
1
+ import * as SchemaParser from "effect/SchemaParser"
2
+ import { withDefaultParseOptions } from "./ext.js"
3
+
4
+ export * from "effect/SchemaParser"
5
+
6
+ export const decodeEffectConcurrently: typeof SchemaParser.decodeEffect = withDefaultParseOptions(
7
+ SchemaParser.decodeEffect
8
+ )
9
+
10
+ export const decodeUnknownEffectConcurrently: typeof SchemaParser.decodeUnknownEffect = withDefaultParseOptions(
11
+ SchemaParser.decodeUnknownEffect
12
+ )
@@ -0,0 +1,137 @@
1
+ /**
2
+ * SpecialJsonSchema — A variant of Schema.toJsonSchemaDocument that
3
+ * post-processes the output (e.g. flattens simple allOf).
4
+ */
5
+ import { type JsonSchema, type Schema, SchemaRepresentation } from "effect"
6
+
7
+ /**
8
+ * Converts a schema to a JSON Schema Document (draft-2020-12), with
9
+ * post-processing that flattens simple allOf entries.
10
+ */
11
+ export function specialJsonSchemaDocument(
12
+ schema: Schema.Top,
13
+ options?: Schema.ToJsonSchemaOptions
14
+ ): JsonSchema.Document<"draft-2020-12"> {
15
+ const doc = SchemaRepresentation.fromAST(schema.ast)
16
+ const jd = SchemaRepresentation.toJsonSchemaDocument(doc, options)
17
+ const processedDefs: JsonSchema.Definitions = {}
18
+ for (const [key, def] of Object.entries(jd.definitions)) {
19
+ processedDefs[key] = postProcessJsonSchema(def)
20
+ }
21
+ return {
22
+ dialect: "draft-2020-12",
23
+ schema: postProcessJsonSchema(jd.schema),
24
+ definitions: processedDefs
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Flattens `allOf` entries into the parent when the parent already has a
30
+ * `type` and every `allOf` entry is a plain constraint object (no `$ref`,
31
+ * no `type`). Merged properties from `allOf` entries win on conflict.
32
+ */
33
+ export function flattenSimpleAllOf(obj: unknown): unknown {
34
+ if (obj === null || typeof obj !== "object") return obj
35
+
36
+ if (globalThis.Array.isArray(obj)) {
37
+ return obj.map(flattenSimpleAllOf)
38
+ }
39
+
40
+ const record = obj as Record<string, unknown>
41
+ const result: Record<string, unknown> = {}
42
+ for (const [key, value] of Object.entries(record)) {
43
+ result[key] = flattenSimpleAllOf(value)
44
+ }
45
+
46
+ if (result["type"] && globalThis.Array.isArray(result["allOf"])) {
47
+ const allOf = result["allOf"] as Array<Record<string, unknown>>
48
+ const canFlatten = allOf.every((entry) =>
49
+ typeof entry === "object" && entry !== null && !("$ref" in entry) && !("type" in entry)
50
+ )
51
+ if (canFlatten) {
52
+ const { allOf: _, ...rest } = result
53
+ const merged: Record<string, unknown> = { ...rest }
54
+ for (const entry of allOf) {
55
+ Object.assign(merged, entry)
56
+ }
57
+ return merged
58
+ }
59
+ }
60
+
61
+ return result
62
+ }
63
+
64
+ /**
65
+ * Recursively removes `additionalProperties: false` from JSON Schema objects.
66
+ * Only removes when the value is exactly `false` -- other values are left intact.
67
+ */
68
+ export function removeAdditionalPropertiesFalse(obj: unknown): unknown {
69
+ if (obj === null || typeof obj !== "object") return obj
70
+
71
+ if (globalThis.Array.isArray(obj)) {
72
+ return obj.map(removeAdditionalPropertiesFalse)
73
+ }
74
+
75
+ const record = obj as Record<string, unknown>
76
+ const result: Record<string, unknown> = {}
77
+ for (const [key, value] of Object.entries(record)) {
78
+ if (key === "additionalProperties" && value === false) continue
79
+ result[key] = removeAdditionalPropertiesFalse(value)
80
+ }
81
+
82
+ return result
83
+ }
84
+
85
+ /**
86
+ * Flattens nested `anyOf` entries: if an anyOf entry is itself just `{ anyOf: [...] }`
87
+ * with no other keys, its children are inlined. If only one item remains, the anyOf
88
+ * wrapper is removed entirely.
89
+ */
90
+ export function flattenNestedAnyOf(obj: unknown): unknown {
91
+ if (obj === null || typeof obj !== "object") return obj
92
+ if (globalThis.Array.isArray(obj)) return obj.map(flattenNestedAnyOf)
93
+
94
+ const record = obj as Record<string, unknown>
95
+ const result: Record<string, unknown> = {}
96
+ for (const [key, value] of Object.entries(record)) {
97
+ result[key] = flattenNestedAnyOf(value)
98
+ }
99
+
100
+ if (globalThis.Array.isArray(result["anyOf"])) {
101
+ const anyOf = result["anyOf"] as Array<unknown>
102
+ const flattened: Array<unknown> = []
103
+ for (const entry of anyOf) {
104
+ if (
105
+ typeof entry === "object"
106
+ && entry !== null
107
+ && !globalThis.Array.isArray(entry)
108
+ && "anyOf" in entry
109
+ && Object.keys(entry).length === 1
110
+ && globalThis.Array.isArray((entry as Record<string, unknown>)["anyOf"])
111
+ ) {
112
+ flattened.push(...(entry as Record<string, unknown>)["anyOf"] as Array<unknown>)
113
+ } else {
114
+ flattened.push(entry)
115
+ }
116
+ }
117
+ if (flattened.length === 1) {
118
+ const { anyOf: _, ...rest } = result
119
+ const single = flattened[0]
120
+ if (typeof single === "object" && single !== null && !globalThis.Array.isArray(single)) {
121
+ return { ...rest, ...single }
122
+ }
123
+ return single
124
+ }
125
+ result["anyOf"] = flattened
126
+ }
127
+
128
+ return result
129
+ }
130
+
131
+ /**
132
+ * Applies JSON Schema post-processing: flattens simple allOf,
133
+ * flattens nested anyOf, then strips additionalProperties: false.
134
+ */
135
+ export function postProcessJsonSchema(obj: JsonSchema.JsonSchema): JsonSchema.JsonSchema {
136
+ return removeAdditionalPropertiesFalse(flattenNestedAnyOf(flattenSimpleAllOf(obj))) as JsonSchema.JsonSchema
137
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SpecialOpenApi — Deduplicates `components/schemas` entries in an OpenAPI spec
3
+ * and applies JSON Schema post-processing (null removal, allOf flattening).
4
+ *
5
+ * When `OpenApi.fromApi` generates the spec, different AST nodes sharing the
6
+ * same identifier can produce duplicate entries (e.g. "X" and "X1") in
7
+ * `components.schemas`. This module provides a transform function that
8
+ * collapses those duplicates, rewrites all `$ref` pointers accordingly,
9
+ * and post-processes schemas for better codegen compatibility.
10
+ *
11
+ * Usage with the OpenApi `Transform` annotation:
12
+ *
13
+ * ```ts
14
+ * import { OpenApi } from "effect/unstable"
15
+ * import { deduplicateOpenApiSchemas } from "./SpecialOpenApi.js"
16
+ *
17
+ * const api = HttpApi.make("myApi")
18
+ * .pipe(HttpApi.annotateContext(OpenApi.annotations({ transform: deduplicateOpenApiSchemas })))
19
+ * ```
20
+ */
21
+
22
+ import { postProcessJsonSchema } from "./SpecialJsonSchema.js"
23
+
24
+ /**
25
+ * Deduplicates `components.schemas` entries in an OpenAPI spec.
26
+ *
27
+ * Entries sharing the same base identifier (e.g. "X" and "X1") whose JSON
28
+ * representations are identical are collapsed into a single canonical entry,
29
+ * and all `$ref` pointers throughout the spec are rewritten to point to
30
+ * the canonical key.
31
+ *
32
+ * Designed to be used as the `transform` option in `OpenApi.annotations`.
33
+ */
34
+ export function deduplicateOpenApiSchemas(
35
+ spec: Record<string, any>
36
+ ): Record<string, any> {
37
+ const components = spec["components"] as Record<string, any> | undefined
38
+ if (!components) return spec
39
+ const schemas = components["schemas"] as Record<string, any> | undefined
40
+ if (!schemas) return spec
41
+
42
+ const keys = Object.keys(schemas)
43
+ if (keys.length === 0) return spec
44
+
45
+ // Group keys by base identifier (strip trailing digits)
46
+ const groups = new Map<string, Array<{ key: string; fingerprint: string }>>()
47
+ for (const key of keys) {
48
+ const base = getBaseIdentifier(key)
49
+ const fingerprint = JSON.stringify(schemas[key])
50
+ const group = groups.get(base)
51
+ if (group === undefined) {
52
+ groups.set(base, [{ key, fingerprint }])
53
+ } else {
54
+ group.push({ key, fingerprint })
55
+ }
56
+ }
57
+
58
+ // Build remapping from duplicate keys to canonical keys
59
+ const remapping = new Map<string, string>()
60
+ for (const [, group] of groups) {
61
+ if (group.length <= 1) continue
62
+ const seen = new Map<string, string>() // fingerprint -> canonical key
63
+ for (const entry of group) {
64
+ const canonical = seen.get(entry.fingerprint)
65
+ if (canonical !== undefined) {
66
+ remapping.set(entry.key, canonical)
67
+ } else {
68
+ seen.set(entry.fingerprint, entry.key)
69
+ }
70
+ }
71
+ }
72
+
73
+ if (remapping.size === 0) return postProcessJsonSchema(spec)
74
+
75
+ // Build new schemas object without duplicates
76
+ const newSchemas: Record<string, any> = {}
77
+ for (const key of keys) {
78
+ if (!remapping.has(key)) {
79
+ newSchemas[key] = schemas[key]
80
+ }
81
+ }
82
+
83
+ // Deep clone the spec, replace schemas, and rewrite all $ref pointers
84
+ const newSpec = structuredClone(spec)
85
+ newSpec["components"]["schemas"] = newSchemas
86
+ rewriteRefs(newSpec, remapping)
87
+
88
+ return postProcessJsonSchema(newSpec)
89
+ }
90
+
91
+ /**
92
+ * Extracts the base identifier from a schema key by stripping trailing
93
+ * digits appended by the gen() function.
94
+ * E.g. "X1" -> "X", "X" -> "X", "MyType2" -> "MyType"
95
+ */
96
+ function getBaseIdentifier(key: string): string {
97
+ const match = key.match(/^(.+?)(\d+)$/)
98
+ return match ? match[1]! : key
99
+ }
100
+
101
+ /**
102
+ * Recursively rewrites `$ref` values in a JSON object tree.
103
+ * Mutates the object in-place (caller should pass a deep clone).
104
+ */
105
+ function rewriteRefs(obj: any, remapping: Map<string, string>): void {
106
+ if (obj === null || typeof obj !== "object") return
107
+
108
+ if (Array.isArray(obj)) {
109
+ for (const item of obj) {
110
+ rewriteRefs(item, remapping)
111
+ }
112
+ return
113
+ }
114
+
115
+ if (typeof obj.$ref === "string") {
116
+ // OpenAPI refs look like "#/components/schemas/X1"
117
+ const prefix = "#/components/schemas/"
118
+ if (obj.$ref.startsWith(prefix)) {
119
+ const refKey = obj.$ref.slice(prefix.length)
120
+ const canonical = remapping.get(refKey)
121
+ if (canonical !== undefined) {
122
+ obj.$ref = prefix + canonical
123
+ }
124
+ }
125
+ }
126
+
127
+ for (const value of Object.values(obj)) {
128
+ rewriteRefs(value, remapping)
129
+ }
130
+ }
@@ -35,11 +35,19 @@ type BrandAnnotations<C extends B.Brand<any>> =
35
35
  : {}
36
36
  )
37
37
 
38
+ type BrandedSchema<Self extends S.Top, C extends B.Brand<any>> =
39
+ & Omit<S.brand<Self["Rebuild"], B.Brand.Keys<C>>, "Type" | "Iso" | "~type.make">
40
+ & {
41
+ readonly Type: C
42
+ readonly Iso: C
43
+ readonly "~type.make": C
44
+ }
45
+
38
46
  export const fromBrand = <C extends B.Brand<any>>(
39
47
  constructor: Constructor<C>,
40
48
  options?: BrandAnnotations<C>
41
49
  ) =>
42
- <Self extends S.Top>(self: Self): S.brand<Self["~rebuild.out"], B.Brand.Keys<C>> => {
50
+ <Self extends S.Top>(self: Self): BrandedSchema<Self, C> => {
43
51
  const branded = S.fromBrand(options?.identifier ?? "Brand", constructor as any)(self as any)
44
52
  return options ? (branded as any).pipe(S.annotate(options)) : branded as any
45
53
  }