effect-app 4.0.0-beta.10 → 4.0.0-beta.100

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 (141) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/dist/Config/SecretURL.js +2 -2
  3. package/dist/Config.d.ts +7 -0
  4. package/dist/Config.d.ts.map +1 -0
  5. package/dist/Config.js +6 -0
  6. package/dist/ConfigProvider.d.ts +39 -0
  7. package/dist/ConfigProvider.d.ts.map +1 -0
  8. package/dist/ConfigProvider.js +42 -0
  9. package/dist/{ServiceMap.d.ts → Context.d.ts} +14 -18
  10. package/dist/Context.d.ts.map +1 -0
  11. package/dist/Context.js +66 -0
  12. package/dist/Effect.d.ts +8 -7
  13. package/dist/Effect.d.ts.map +1 -1
  14. package/dist/Effect.js +3 -2
  15. package/dist/Layer.d.ts +5 -4
  16. package/dist/Layer.d.ts.map +1 -1
  17. package/dist/Layer.js +1 -1
  18. package/dist/Operations.d.ts +62 -25
  19. package/dist/Operations.d.ts.map +1 -1
  20. package/dist/Pure.d.ts +2 -2
  21. package/dist/Pure.d.ts.map +1 -1
  22. package/dist/Pure.js +13 -13
  23. package/dist/Schema/Class.d.ts +43 -10
  24. package/dist/Schema/Class.d.ts.map +1 -1
  25. package/dist/Schema/Class.js +89 -16
  26. package/dist/Schema/SpecialJsonSchema.d.ts +21 -0
  27. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  28. package/dist/Schema/SpecialJsonSchema.js +59 -0
  29. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  30. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  31. package/dist/Schema/SpecialOpenApi.js +123 -0
  32. package/dist/Schema/brand.d.ts +5 -1
  33. package/dist/Schema/brand.d.ts.map +1 -1
  34. package/dist/Schema/brand.js +1 -1
  35. package/dist/Schema/email.d.ts.map +1 -1
  36. package/dist/Schema/email.js +9 -4
  37. package/dist/Schema/ext.d.ts +103 -46
  38. package/dist/Schema/ext.d.ts.map +1 -1
  39. package/dist/Schema/ext.js +110 -51
  40. package/dist/Schema/moreStrings.d.ts +19 -7
  41. package/dist/Schema/moreStrings.d.ts.map +1 -1
  42. package/dist/Schema/moreStrings.js +14 -9
  43. package/dist/Schema/numbers.d.ts +11 -11
  44. package/dist/Schema/numbers.d.ts.map +1 -1
  45. package/dist/Schema/numbers.js +10 -9
  46. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  47. package/dist/Schema/phoneNumber.js +8 -3
  48. package/dist/Schema/strings.d.ts +4 -4
  49. package/dist/Schema/strings.d.ts.map +1 -1
  50. package/dist/Schema.d.ts +22 -55
  51. package/dist/Schema.d.ts.map +1 -1
  52. package/dist/Schema.js +43 -63
  53. package/dist/client/apiClientFactory.d.ts +11 -28
  54. package/dist/client/apiClientFactory.d.ts.map +1 -1
  55. package/dist/client/apiClientFactory.js +14 -15
  56. package/dist/client/clientFor.d.ts +6 -5
  57. package/dist/client/clientFor.d.ts.map +1 -1
  58. package/dist/client/errors.d.ts +18 -9
  59. package/dist/client/errors.d.ts.map +1 -1
  60. package/dist/client/errors.js +35 -10
  61. package/dist/client/makeClient.d.ts +20 -15
  62. package/dist/client/makeClient.d.ts.map +1 -1
  63. package/dist/client/makeClient.js +32 -23
  64. package/dist/http/Request.d.ts.map +1 -1
  65. package/dist/http/Request.js +5 -5
  66. package/dist/ids.d.ts +2 -2
  67. package/dist/ids.d.ts.map +1 -1
  68. package/dist/ids.js +3 -2
  69. package/dist/index.d.ts +3 -8
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -9
  72. package/dist/middleware.d.ts +2 -2
  73. package/dist/middleware.d.ts.map +1 -1
  74. package/dist/middleware.js +3 -3
  75. package/dist/rpc/MiddlewareMaker.d.ts +4 -3
  76. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  77. package/dist/rpc/MiddlewareMaker.js +7 -6
  78. package/dist/rpc/RpcContextMap.d.ts +2 -2
  79. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  80. package/dist/rpc/RpcContextMap.js +4 -4
  81. package/dist/rpc/RpcMiddleware.d.ts +4 -3
  82. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  83. package/dist/rpc/RpcMiddleware.js +1 -1
  84. package/dist/utils/gen.d.ts +1 -1
  85. package/dist/utils/gen.d.ts.map +1 -1
  86. package/dist/utils/logger.d.ts +2 -2
  87. package/dist/utils/logger.d.ts.map +1 -1
  88. package/dist/utils/logger.js +3 -3
  89. package/dist/utils.d.ts +18 -0
  90. package/dist/utils.d.ts.map +1 -1
  91. package/dist/utils.js +22 -2
  92. package/package.json +29 -17
  93. package/src/Config/SecretURL.ts +1 -1
  94. package/src/Config.ts +14 -0
  95. package/src/ConfigProvider.ts +48 -0
  96. package/src/{ServiceMap.ts → Context.ts} +51 -60
  97. package/src/Effect.ts +11 -9
  98. package/src/Layer.ts +5 -4
  99. package/src/Pure.ts +17 -18
  100. package/src/Schema/Class.ts +118 -30
  101. package/src/Schema/SpecialJsonSchema.ts +69 -0
  102. package/src/Schema/SpecialOpenApi.ts +130 -0
  103. package/src/Schema/brand.ts +10 -3
  104. package/src/Schema/email.ts +10 -2
  105. package/src/Schema/ext.ts +185 -82
  106. package/src/Schema/moreStrings.ts +21 -11
  107. package/src/Schema/numbers.ts +9 -8
  108. package/src/Schema/phoneNumber.ts +8 -1
  109. package/src/Schema.ts +79 -103
  110. package/src/client/apiClientFactory.ts +24 -29
  111. package/src/client/clientFor.ts +6 -1
  112. package/src/client/errors.ts +34 -9
  113. package/src/client/makeClient.ts +121 -61
  114. package/src/http/Request.ts +7 -4
  115. package/src/ids.ts +2 -1
  116. package/src/index.ts +3 -11
  117. package/src/middleware.ts +2 -2
  118. package/src/rpc/MiddlewareMaker.ts +8 -7
  119. package/src/rpc/RpcContextMap.ts +6 -5
  120. package/src/rpc/RpcMiddleware.ts +5 -4
  121. package/src/utils/gen.ts +1 -1
  122. package/src/utils/logger.ts +2 -2
  123. package/src/utils.ts +24 -1
  124. package/test/dist/moreStrings.test.d.ts.map +1 -0
  125. package/test/dist/rpc.test.d.ts.map +1 -1
  126. package/test/dist/secretURL.test.d.ts.map +1 -0
  127. package/test/dist/special.test.d.ts.map +1 -0
  128. package/test/moreStrings.test.ts +17 -0
  129. package/test/rpc.test.ts +28 -6
  130. package/test/schema.test.ts +397 -4
  131. package/test/secretURL.test.ts +157 -0
  132. package/test/special.test.ts +729 -0
  133. package/test/utils.test.ts +2 -2
  134. package/tsconfig.base.json +0 -1
  135. package/tsconfig.json +0 -1
  136. package/dist/ServiceMap.d.ts.map +0 -1
  137. package/dist/ServiceMap.js +0 -91
  138. package/dist/Struct.d.ts +0 -44
  139. package/dist/Struct.d.ts.map +0 -1
  140. package/dist/Struct.js +0 -29
  141. package/src/Struct.ts +0 -54
package/src/Effect.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  /* eslint-disable prefer-destructuring */
3
3
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
4
 
5
- import { Effect, Option, Ref, type ServiceMap } from "effect"
5
+ import { Effect, Option, Ref } from "effect"
6
6
  import * as Def from "effect/Deferred"
7
7
  import * as Fiber from "effect/Fiber"
8
8
  import type { Scope } from "effect/Scope"
9
9
  import type { Semaphore } from "effect/Semaphore"
10
+ import type * as Context from "./Context.js"
10
11
  import { curry } from "./Function.js"
11
12
  import { typedKeysOf } from "./utils.js"
12
13
 
@@ -116,10 +117,10 @@ export function joinAll<E, A>(fibers: Iterable<Fiber.Fiber<A, E>>): Effect.Effec
116
117
  }
117
118
 
118
119
  type ServiceA<T> = T extends Effect.Effect<infer S, any, any> ? S
119
- : T extends ServiceMap.Service<any, infer S> ? S
120
+ : T extends Context.Service<any, infer S> ? S
120
121
  : never
121
122
  type ServiceR<T> = T extends Effect.Effect<any, any, infer R> ? R
122
- : T extends ServiceMap.Service<infer I, any> ? I
123
+ : T extends Context.Service<infer I, any> ? I
123
124
  : never
124
125
  type ServiceE<T> = T extends Effect.Effect<any, infer E, any> ? E : never
125
126
  // type Values<T> = T extends { [s: string]: infer S } ? ServiceA<S> : never
@@ -144,24 +145,25 @@ export interface EffectUnunified<R, E, A> extends Effect.Effect<R, E, A> {}
144
145
 
145
146
  export type LowerFirst<S extends PropertyKey> = S extends `${infer First}${infer Rest}` ? `${Lowercase<First>}${Rest}`
146
147
  : S
147
- export type LowerServices<T extends Record<string, ServiceMap.Service<any, any> | Effect.Effect<any, any, any>>> = {
148
+ export type LowerServices<T extends Record<string, Context.Service<any, any> | Effect.Effect<any, any, any>>> = {
148
149
  [key in keyof T as LowerFirst<key>]: ServiceA<T[key]>
149
150
  }
150
151
 
151
- export function allLower<T extends Record<string, ServiceMap.Service<any, any> | Effect.Effect<any, any, any>>>(
152
+ export function allLower<T extends Record<string, Context.Service<any, any> | Effect.Effect<any, any, any>>>(
152
153
  services: T
153
154
  ) {
154
155
  return Effect.all(
155
156
  typedKeysOf(services).reduce((prev, cur) => {
156
- const svc = services[cur]
157
- prev[((cur as string)[0]!.toLowerCase() + (cur as string).slice(1)) as unknown as LowerFirst<typeof cur>] = svc // "_id" in svc && svc._id === TagTypeId ? svc : svc
157
+ const svc = services[cur]!
158
+ prev[((cur as string)[0]!.toLowerCase() + (cur as string).slice(1)) as unknown as LowerFirst<typeof cur>] =
159
+ "asEffect" in svc ? svc.asEffect() : svc
158
160
  return prev
159
161
  }, {} as any),
160
162
  { concurrency: "inherit" }
161
163
  ) as any as Effect.Effect<LowerServices<T>, ValuesE<T>, ValuesR<T>>
162
164
  }
163
165
 
164
- export function allLowerWith<T extends Record<string, ServiceMap.Service<any, any> | Effect.Effect<any, any, any>>, A>(
166
+ export function allLowerWith<T extends Record<string, Context.Service<any, any> | Effect.Effect<any, any, any>>, A>(
165
167
  services: T,
166
168
  fn: (services: LowerServices<T>) => A
167
169
  ) {
@@ -169,7 +171,7 @@ export function allLowerWith<T extends Record<string, ServiceMap.Service<any, an
169
171
  }
170
172
 
171
173
  export function allLowerWithEffect<
172
- T extends Record<string, ServiceMap.Service<any, any> | Effect.Effect<any, any, any>>,
174
+ T extends Record<string, Context.Service<any, any> | Effect.Effect<any, any, any>>,
173
175
  R,
174
176
  E,
175
177
  A
package/src/Layer.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { type Array, Effect, Layer, type Scope, type ServiceMap, type Types } from "effect"
1
+ import { type Array, Effect, Layer, type Scope, type Types } from "effect"
2
2
  import { type Yieldable } from "effect/Effect"
3
3
  import { dual } from "effect/Function"
4
+ import type * as Context from "./Context.js"
4
5
  import { type EffectGenUtils } from "./utils/gen.js"
5
6
 
6
7
  export * from "effect/Layer"
@@ -17,7 +18,7 @@ type MakeGenNo<S> = {
17
18
  readonly make: () => Generator<unknown, S>
18
19
  }
19
20
  type MakeErr<Opts> = Opts extends { make: () => any } ? EffectGenUtils.Error<Opts["make"]> : never
20
- type MakeContext<Opts> = Opts extends { make: () => any } ? EffectGenUtils.ServiceMap<Opts["make"]> : never
21
+ type MakeContext<Opts> = Opts extends { make: () => any } ? EffectGenUtils.Context<Opts["make"]> : never
21
22
 
22
23
  type DependenciesOpt = { dependencies?: Array.NonEmptyReadonlyArray<Layer.Any> }
23
24
  type Dependencies = { dependencies: Array.NonEmptyReadonlyArray<Layer.Any> }
@@ -41,12 +42,12 @@ type PackedOrUnpackedLayer<I, Opts> = Opts extends Dependencies ? PackedLayers<I
41
42
 
42
43
  export const make: {
43
44
  <I, S>(
44
- tag: ServiceMap.Service<I, S>
45
+ tag: Context.Service<I, S>
45
46
  ): <Opts extends Make<Types.NoInfer<S>, any, any>>(
46
47
  options: Opts
47
48
  ) => PackedOrUnpackedLayer<I, Opts>
48
49
  <I, S, Opts extends Make<Types.NoInfer<S>, any, any>>(
49
- tag: ServiceMap.Service<I, S>,
50
+ tag: Context.Service<I, S>,
50
51
  options: Opts
51
52
  ): PackedOrUnpackedLayer<I, Opts>
52
53
  } = dual(2, (tag, options) => {
package/src/Pure.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { Chunk, Effect, Layer, Result } from "effect"
3
+ import * as Context from "./Context.js"
3
4
  import { tuple } from "./Function.js"
4
- import * as ServiceMap from "./ServiceMap.js"
5
5
 
6
6
  const S1 = Symbol()
7
7
  const S2 = Symbol()
@@ -86,9 +86,9 @@ export function GMU<W, S, S2, GA, MR, ME>(modify: (i: GA) => Pure<W, S, S2, MR,
86
86
  ) => GMU_(get, modify, update)
87
87
  }
88
88
 
89
- const tagg = ServiceMap.Service<{ env: PureEnv<never, unknown, never> }>("PureEnv")
89
+ const tagg = Context.Service<{ env: PureEnv<never, unknown, never> }>("PureEnv")
90
90
  function castTag<W, S, S2>() {
91
- return tagg as any as ServiceMap.Service<PureEnvEnv<W, S, S2>, PureEnvEnv<W, S, S2>>
91
+ return tagg as any as Context.Service<PureEnvEnv<W, S, S2>, PureEnvEnv<W, S, S2>>
92
92
  }
93
93
 
94
94
  export const ServiceTag = Symbol()
@@ -100,9 +100,9 @@ export abstract class PhantomTypeParameter<Identifier extends keyof any, Instant
100
100
  }
101
101
  }
102
102
 
103
- export type ServiceShape<T extends ServiceMap.ServiceClass.Shape<any, any>> = Omit<
103
+ export type ServiceShape<T extends Context.ServiceClass.Shape<any, any>> = Omit<
104
104
  T,
105
- keyof ServiceMap.ServiceClass.Shape<any, any>
105
+ keyof Context.ServiceClass.Shape<any, any>
106
106
  >
107
107
 
108
108
  export abstract class ServiceTagged<ServiceKey> extends PhantomTypeParameter<string, ServiceKey> {}
@@ -117,11 +117,11 @@ export interface PureEnvEnv<W, S, S2> extends ServiceTagged<typeof PureEnvEnv> {
117
117
  }
118
118
 
119
119
  export function get<S>(): Pure<never, S, S, never, never, S> {
120
- return (castTag<never, S, S>() as any).use((_: any) => _.env.state)
120
+ return (castTag<never, S, S>()).useSync((_) => _.env.state)
121
121
  }
122
122
 
123
123
  export function set<S>(s: S): Pure<never, S, S, never, never, void> {
124
- return (castTag<never, S, S>() as any).use((_: any) => {
124
+ return (castTag<never, S, S>()).useSync((_) => {
125
125
  _.env.state = s
126
126
  })
127
127
  }
@@ -129,13 +129,13 @@ export function set<S>(s: S): Pure<never, S, S, never, never, void> {
129
129
  export type PureLogT<W> = Pure<W, unknown, never, never, never, void>
130
130
 
131
131
  export function log<W>(w: W): PureLogT<W> {
132
- return (castTag<W, unknown, never>() as any).use((_: any) => {
132
+ return (castTag<W, unknown, never>()).useSync((_) => {
133
133
  _.env.log = Chunk.append(_.env.log, w)
134
134
  })
135
135
  }
136
136
 
137
137
  export function logMany<W>(w: Iterable<W>): PureLogT<W> {
138
- return (castTag<W, unknown, never>() as any).use((_: any) => {
138
+ return (castTag<W, unknown, never>()).useSync((_) => {
139
139
  _.env.log = Chunk.appendAll(_.env.log, Chunk.fromIterable(w))
140
140
  })
141
141
  }
@@ -150,17 +150,16 @@ export function runAll<R, E, A, W3, S1, S3, S4 extends S1>(
150
150
  > {
151
151
  const a = Effect
152
152
  .flatMap(self, (x) =>
153
- (castTag<W3, S1, S3>() as any)
154
- .use(
155
- ({ env: _ }: any) => Effect.sync(() => ({ log: _.log, state: _.state }))
153
+ (castTag<W3, S1, S3>())
154
+ .useSync(
155
+ ({ env: _ }) => ({ log: _.log, state: _.state })
156
156
  )
157
157
  .pipe(
158
- Effect.flatMap((_: any) => Effect.succeed(_)),
159
158
  Effect.map(
160
- ({ log, state }: any) => tuple(log, Result.succeed(tuple(state, x)))
159
+ ({ log, state }) => tuple(log, Result.succeed(tuple(state, x)))
161
160
  )
162
161
  ))
163
- .pipe(Effect.catch((err: any) => (tagg as any).use((env: any) => tuple(env.env.log, Result.fail(err)))))
162
+ .pipe(Effect.catch((err: any) => tagg.useSync((env) => tuple(env.env.log, Result.fail(err)))))
164
163
  return Effect.provide(a, Layer.succeed(tagg, { env: makePureEnv<W3, S3, S4>(s) as any }) as any) as any
165
164
  }
166
165
 
@@ -198,11 +197,11 @@ export function runA<R, E, A, W3, S1, S3, S4 extends S1>(
198
197
  export function modify<S2, A, S3>(
199
198
  mod: (s: S2) => readonly [S3, A]
200
199
  ): Effect.Effect<A, never, { env: PureEnv<never, S2, S3> }> {
201
- return (castTag<never, S3, S2>() as any).use((_: any) => {
200
+ return (castTag<never, S3, S2>()).useSync((_) => {
202
201
  const [s, a] = mod(_.env.state)
203
202
  _.env.state = s as any
204
203
  return a
205
- })
204
+ }) as any
206
205
  }
207
206
 
208
207
  export function modifyM<W, R, E, A, S2, S3>(
@@ -210,7 +209,7 @@ export function modifyM<W, R, E, A, S2, S3>(
210
209
  ): Effect.Effect<A, E, FixEnv<R, W, S2, S3>> {
211
210
  // return serviceWithEffect(_ => Ref.modifyM_(_.state, mod))
212
211
  return Effect.flatMap(
213
- (castTag<W, S3, S2>() as any).use((_: any) => _),
212
+ (castTag<W, S3, S2>()).useSync((_) => _),
214
213
  (_: any) =>
215
214
  Effect.map(mod(_.env.state), ([s, a]: any) => {
216
215
  _.env.state = s
@@ -1,31 +1,73 @@
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 { Effect, Option, Schema, SchemaAST, SchemaIssue } from "effect"
4
3
  import * as S from "effect/Schema"
5
4
 
6
5
  type ClassAnnotations<Self> = S.Annotations.Declaration<Self, readonly [any]>
7
6
 
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"]>
7
+ export interface EnhancedClass<Self, SchemaS extends S.Top & { readonly fields: S.Struct.Fields }, Inherited>
8
+ extends S.Class<Self, SchemaS, Inherited>
10
9
  {
11
10
  }
12
11
  type MissingSelfGeneric<Usage extends string, Params extends string = ""> =
13
12
  `Missing \`Self\` generic - use \`class Self extends ${Usage}<Self>()(${Params}{ ... })\``
14
13
 
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> = {
14
+ type HasFields<Fields extends S.Struct.Fields> = {
24
15
  readonly fields: Fields
25
16
  } | {
26
17
  readonly from: HasFields<Fields>
27
18
  }
28
19
 
20
+ export type Class<Self, S extends S.Top & { readonly fields: S.Struct.Fields }, Inherited> = S.Class<Self, S, Inherited>
21
+
22
+ /**
23
+ * Build a modified Declaration that accepts struct-matching values during
24
+ * encoding, given the original Declaration and the class's fields.
25
+ */
26
+ function makeRelaxedDeclaration(
27
+ ast: SchemaAST.Declaration,
28
+ fields: Schema.Struct.Fields,
29
+ cls: any
30
+ ): SchemaAST.Declaration {
31
+ const structSchema = Schema.Struct(fields)
32
+ const isStructValue = Schema.is(structSchema)
33
+ return new SchemaAST.Declaration(
34
+ ast.typeParameters,
35
+ () => (input: unknown, self: SchemaAST.Declaration) => {
36
+ if (input instanceof cls || isStructValue(input)) {
37
+ return Effect.succeed(input)
38
+ }
39
+ return Effect.fail(new SchemaIssue.InvalidType(self, Option.some(input)))
40
+ },
41
+ ast.annotations,
42
+ ast.checks,
43
+ ast.encoding,
44
+ ast.context
45
+ )
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Class — like Schema.Class but with relaxed encoding
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Like `Schema.Class`, but the resulting class accepts plain objects matching
54
+ * the struct schema during encoding — not only `instanceof` or type-id
55
+ * checks.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { Schema } from "effect"
60
+ * import { Class } from "./Class.js"
61
+ *
62
+ * class A extends Class<A>("A")({ a: Schema.String }) {}
63
+ *
64
+ * // Construction works as normal:
65
+ * new A({ a: "hello" })
66
+ *
67
+ * // Encoding accepts plain objects:
68
+ * Schema.encodeUnknownSync(A)({ a: "hello" }) // { a: "hello" }
69
+ * ```
70
+ */
29
71
  export const Class: <Self = never>(identifier: string) => <Fields extends S.Struct.Fields>(
30
72
  fieldsOr: Fields | HasFields<Fields>,
31
73
  annotations?: ClassAnnotations<Self>
@@ -35,38 +77,80 @@ export const Class: <Self = never>(identifier: string) => <Fields extends S.Stru
35
77
  S.Struct<Fields>,
36
78
  {}
37
79
  > = (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)
80
+ // Build the original Schema.Class
81
+ const Base = (S.Class as any)(identifier)(fields, annotations)
82
+ // Get the original ast getter from the base class
83
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
84
+
85
+ // Cache per-class to avoid recomputing
86
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
87
+
88
+ return class extends Base {
89
+ static get ast(): SchemaAST.Declaration {
90
+ let cached = astCache.get(this)
91
+ if (cached !== undefined) return cached
92
+ // Call the original getter with `this` bound to the actual user class,
93
+ // so getClassSchema(this) creates a schema that uses `new this(...)`.
94
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
95
+ cached = makeRelaxedDeclaration(originalAst, Base.fields, this)
96
+ astCache.set(this, cached)
97
+ return cached
42
98
  }
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
99
  } as any
47
100
  }
48
101
 
49
- export const TaggedClass: <Self = never>(identifier?: string) => <Tag extends string, Fields extends S.Struct.Fields>(
102
+ // ---------------------------------------------------------------------------
103
+ // TaggedClass — like Schema.TaggedClass but with relaxed encoding
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Like `Schema.TaggedClass`, but the resulting class accepts plain objects
108
+ * matching the struct schema during encoding.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { Schema } from "effect"
113
+ * import { TaggedClass } from "./Class.js"
114
+ *
115
+ * class Circle extends TaggedClass<Circle>()("Circle", {
116
+ * radius: Schema.Number
117
+ * }) {}
118
+ *
119
+ * Schema.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 })
120
+ * ```
121
+ */
122
+ export const TaggedClass: <Self = never>(
123
+ identifier?: string
124
+ ) => <Tag extends string, Fields extends S.Struct.Fields>(
50
125
  tag: Tag,
51
126
  fieldsOr: Fields | HasFields<Fields>,
52
127
  annotations?: ClassAnnotations<Self>
53
- ) => [Self] extends [never] ? MissingSelfGeneric<"Class">
128
+ ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedClass">
54
129
  : EnhancedClass<
55
130
  Self,
56
131
  S.Struct<{ readonly _tag: S.tag<Tag> } & Fields>,
57
132
  {}
58
133
  > = (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)
134
+ const Base = (S.TaggedClass as any)(identifier)(tag, fields, annotations)
135
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
136
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
137
+
138
+ return class extends Base {
139
+ static get ast(): SchemaAST.Declaration {
140
+ let cached = astCache.get(this)
141
+ if (cached !== undefined) return cached
142
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
143
+ cached = makeRelaxedDeclaration(originalAst, Base.fields, this)
144
+ astCache.set(this, cached)
145
+ return cached
63
146
  }
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
147
  } as any
68
148
  }
69
149
 
150
+ // ---------------------------------------------------------------------------
151
+ // ExtendedClass — like Class but with extra type parameter for hierarchies
152
+ // ---------------------------------------------------------------------------
153
+
70
154
  export const ExtendedClass: <Self, _SelfFrom>(identifier: string) => <Fields extends S.Struct.Fields>(
71
155
  fieldsOr: Fields | HasFields<Fields>,
72
156
  annotations?: ClassAnnotations<Self>
@@ -76,7 +160,11 @@ export const ExtendedClass: <Self, _SelfFrom>(identifier: string) => <Fields ext
76
160
  {}
77
161
  > = Class as any
78
162
 
79
- export interface EnhancedTaggedClass<Self, Tag extends string, Fields extends Struct.Fields, SelfFrom>
163
+ // ---------------------------------------------------------------------------
164
+ // ExtendedTaggedClass — like TaggedClass but with extra type parameter for hierarchies
165
+ // ---------------------------------------------------------------------------
166
+
167
+ export interface EnhancedTaggedClass<Self, Tag extends string, Fields extends S.Struct.Fields, SelfFrom>
80
168
  extends
81
169
  EnhancedClass<
82
170
  Self,
@@ -0,0 +1,69 @@
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
+ let merged: Record<string, unknown> = { ...rest }
54
+ for (const entry of allOf) {
55
+ merged = { ...merged, ...entry }
56
+ }
57
+ return merged
58
+ }
59
+ }
60
+
61
+ return result
62
+ }
63
+
64
+ /**
65
+ * Applies JSON Schema post-processing: flattens simple allOf.
66
+ */
67
+ export function postProcessJsonSchema(obj: JsonSchema.JsonSchema): JsonSchema.JsonSchema {
68
+ return flattenSimpleAllOf(obj) as JsonSchema.JsonSchema
69
+ }
@@ -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) as Record<string, any>
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) as Record<string, any>
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
+ }
@@ -28,11 +28,18 @@ export interface Constructor<in out A extends B.Brand<any>> {
28
28
  is(a: Unbranded<A>): a is Unbranded<A> & A
29
29
  }
30
30
 
31
- export const fromBrand = <C extends B.Brand<string>>(
31
+ type BrandAnnotations<C extends B.Brand<any>> =
32
+ & S.Annotations.Filter
33
+ & (
34
+ C extends string ? { readonly toArbitrary?: S.Annotations.ToArbitrary.Declaration<C, readonly []> }
35
+ : {}
36
+ )
37
+
38
+ export const fromBrand = <C extends B.Brand<any>>(
32
39
  constructor: Constructor<C>,
33
- options?: S.Annotations.Filter
40
+ options?: BrandAnnotations<C>
34
41
  ) =>
35
- <Self extends S.Top>(self: Self): S.brand<Self["~rebuild.out"], B.Brand.Keys<C>> => {
42
+ <Self extends S.Top>(self: Self): S.brand<Self["Rebuild"], B.Brand.Keys<C>> => {
36
43
  const branded = S.fromBrand(options?.identifier ?? "Brand", constructor as any)(self as any)
37
44
  return options ? (branded as any).pipe(S.annotate(options)) : branded as any
38
45
  }
@@ -12,11 +12,19 @@ export type Email = string & EmailBrand
12
12
  export const Email = S
13
13
  .String
14
14
  .pipe(
15
+ S.annotate({
16
+ title: "Email",
17
+ description: "an email according to RFC 5322",
18
+ format: "email"
19
+ }),
20
+ S.check(S.isMinLength(3), /* a@b */ S.isMaxLength(998)),
15
21
  S.refine(isValidEmail as Refinement<string, Email>, {
16
22
  identifier: "Email",
17
23
  title: "Email",
18
24
  description: "an email according to RFC 5322",
19
- jsonSchema: { format: "email", minLength: 3, /* a@b */ maxLength: 998 },
20
- arbitrary: () => (fc: any) => fc.emailAddress().map((_: any) => _ as Email)
25
+ jsonSchema: { format: "email", minLength: 3, maxLength: 998 }
26
+ }),
27
+ S.annotate({
28
+ toArbitrary: () => (fc) => fc.emailAddress().map((_) => _ as Email)
21
29
  })
22
30
  )