effect-app 4.0.0-beta.6 → 4.0.0-beta.60

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 (130) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/dist/Config.d.ts +7 -0
  3. package/dist/Config.d.ts.map +1 -0
  4. package/dist/Config.js +6 -0
  5. package/dist/ConfigProvider.d.ts +39 -0
  6. package/dist/ConfigProvider.d.ts.map +1 -0
  7. package/dist/ConfigProvider.js +42 -0
  8. package/dist/{ServiceMap.d.ts → Context.d.ts} +9 -12
  9. package/dist/Context.d.ts.map +1 -0
  10. package/dist/Context.js +87 -0
  11. package/dist/Effect.d.ts +8 -7
  12. package/dist/Effect.d.ts.map +1 -1
  13. package/dist/Effect.js +3 -2
  14. package/dist/Layer.d.ts +5 -4
  15. package/dist/Layer.d.ts.map +1 -1
  16. package/dist/Layer.js +1 -1
  17. package/dist/Operations.d.ts +51 -15
  18. package/dist/Operations.d.ts.map +1 -1
  19. package/dist/Pure.d.ts +2 -2
  20. package/dist/Pure.d.ts.map +1 -1
  21. package/dist/Pure.js +13 -13
  22. package/dist/Schema/Class.d.ts +39 -1
  23. package/dist/Schema/Class.d.ts.map +1 -1
  24. package/dist/Schema/Class.js +89 -12
  25. package/dist/Schema/SpecialJsonSchema.d.ts +40 -0
  26. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  27. package/dist/Schema/SpecialJsonSchema.js +199 -0
  28. package/dist/Schema/SpecialOpenApi.d.ts +30 -0
  29. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  30. package/dist/Schema/SpecialOpenApi.js +120 -0
  31. package/dist/Schema/brand.d.ts +8 -5
  32. package/dist/Schema/brand.d.ts.map +1 -1
  33. package/dist/Schema/brand.js +1 -1
  34. package/dist/Schema/email.d.ts.map +1 -1
  35. package/dist/Schema/email.js +4 -3
  36. package/dist/Schema/ext.d.ts +142 -44
  37. package/dist/Schema/ext.d.ts.map +1 -1
  38. package/dist/Schema/ext.js +145 -35
  39. package/dist/Schema/moreStrings.d.ts.map +1 -1
  40. package/dist/Schema/moreStrings.js +6 -4
  41. package/dist/Schema/numbers.d.ts +8 -8
  42. package/dist/Schema/numbers.js +2 -2
  43. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  44. package/dist/Schema/phoneNumber.js +3 -2
  45. package/dist/Schema.d.ts +21 -54
  46. package/dist/Schema.d.ts.map +1 -1
  47. package/dist/Schema.js +43 -64
  48. package/dist/client/apiClientFactory.d.ts +3 -3
  49. package/dist/client/apiClientFactory.d.ts.map +1 -1
  50. package/dist/client/apiClientFactory.js +12 -13
  51. package/dist/client/errors.d.ts +8 -0
  52. package/dist/client/errors.d.ts.map +1 -1
  53. package/dist/client/errors.js +35 -10
  54. package/dist/client/makeClient.d.ts +13 -12
  55. package/dist/client/makeClient.d.ts.map +1 -1
  56. package/dist/client/makeClient.js +5 -2
  57. package/dist/http/Request.d.ts.map +1 -1
  58. package/dist/http/Request.js +5 -5
  59. package/dist/ids.d.ts +1 -1
  60. package/dist/ids.d.ts.map +1 -1
  61. package/dist/ids.js +1 -1
  62. package/dist/index.d.ts +6 -8
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +7 -9
  65. package/dist/middleware.d.ts +2 -2
  66. package/dist/middleware.d.ts.map +1 -1
  67. package/dist/middleware.js +3 -3
  68. package/dist/rpc/MiddlewareMaker.d.ts +4 -3
  69. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  70. package/dist/rpc/MiddlewareMaker.js +6 -5
  71. package/dist/rpc/RpcContextMap.d.ts +2 -2
  72. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  73. package/dist/rpc/RpcContextMap.js +4 -4
  74. package/dist/rpc/RpcMiddleware.d.ts +4 -3
  75. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  76. package/dist/rpc/RpcMiddleware.js +1 -1
  77. package/dist/utils/gen.d.ts +1 -1
  78. package/dist/utils/gen.d.ts.map +1 -1
  79. package/dist/utils/logger.d.ts +2 -2
  80. package/dist/utils/logger.d.ts.map +1 -1
  81. package/dist/utils/logger.js +3 -3
  82. package/dist/utils.d.ts +18 -0
  83. package/dist/utils.d.ts.map +1 -1
  84. package/dist/utils.js +24 -5
  85. package/package.json +29 -17
  86. package/src/Config.ts +14 -0
  87. package/src/ConfigProvider.ts +48 -0
  88. package/src/{ServiceMap.ts → Context.ts} +16 -17
  89. package/src/Effect.ts +11 -9
  90. package/src/Layer.ts +5 -4
  91. package/src/Pure.ts +17 -18
  92. package/src/Schema/Class.ts +114 -16
  93. package/src/Schema/SpecialJsonSchema.ts +216 -0
  94. package/src/Schema/SpecialOpenApi.ts +126 -0
  95. package/src/Schema/brand.ts +13 -7
  96. package/src/Schema/email.ts +4 -2
  97. package/src/Schema/ext.ts +222 -57
  98. package/src/Schema/moreStrings.ts +10 -6
  99. package/src/Schema/numbers.ts +2 -2
  100. package/src/Schema/phoneNumber.ts +3 -1
  101. package/src/Schema.ts +79 -103
  102. package/src/client/apiClientFactory.ts +16 -19
  103. package/src/client/errors.ts +46 -12
  104. package/src/client/makeClient.ts +32 -12
  105. package/src/http/Request.ts +7 -4
  106. package/src/ids.ts +1 -1
  107. package/src/index.ts +6 -9
  108. package/src/middleware.ts +2 -2
  109. package/src/rpc/MiddlewareMaker.ts +7 -6
  110. package/src/rpc/RpcContextMap.ts +6 -5
  111. package/src/rpc/RpcMiddleware.ts +5 -4
  112. package/src/utils/gen.ts +1 -1
  113. package/src/utils/logger.ts +2 -2
  114. package/src/utils.ts +26 -4
  115. package/test/dist/moreStrings.test.d.ts.map +1 -0
  116. package/test/dist/rpc.test.d.ts.map +1 -1
  117. package/test/dist/special.test.d.ts.map +1 -0
  118. package/test/moreStrings.test.ts +17 -0
  119. package/test/rpc.test.ts +26 -5
  120. package/test/schema.test.ts +292 -1
  121. package/test/special.test.ts +525 -0
  122. package/test/utils.test.ts +1 -1
  123. package/tsconfig.base.json +0 -1
  124. package/tsconfig.json +0 -1
  125. package/dist/ServiceMap.d.ts.map +0 -1
  126. package/dist/ServiceMap.js +0 -91
  127. package/dist/Struct.d.ts +0 -44
  128. package/dist/Struct.d.ts.map +0 -1
  129. package/dist/Struct.js +0 -29
  130. package/src/Struct.ts +0 -54
@@ -1,20 +1,19 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /**
3
- * We're doing the long way around here with assignTag, TagBase & TagBaseTagged,
4
- * because there's a typescript compiler issue where it will complain about Equal.symbol, and Hash.symbol not being accessible.
5
- * https://github.com/microsoft/TypeScript/issues/52644
6
- */
7
2
 
8
3
  import { type Effect, Layer, type Scope, type Types } from "effect"
9
- import * as ServiceMap from "effect/ServiceMap"
10
- import { Yieldable } from "./Effect.js"
4
+ import * as SM from "effect/ServiceMap"
5
+ import { type Yieldable } from "./Effect.js"
11
6
 
12
7
  export * from "effect/ServiceMap"
13
8
 
14
- export interface Opaque<Self extends object, in out Shape extends object> extends ServiceMap.Key<Self, Self>, Yieldable<Opaque<Self, Shape>, Self, never, Self> {
15
- // temp while sorting out https://github.com/Effect-TS/effect-smol/pull/1534
16
- of(self: Shape): Self
17
- serviceMap(self: Shape): ServiceMap.ServiceMap<Self>
9
+ export { type ServiceMap as Context } from "effect/ServiceMap"
10
+ export { isServiceMap as isContext } from "effect/ServiceMap"
11
+
12
+ export interface Opaque<Self extends object, in out Shape extends object>
13
+ extends SM.Key<Self, Self>, Yieldable<Opaque<Self, Shape>, Self, never, Self>
14
+ {
15
+ of(this: void, self: Shape): Self
16
+ serviceMap(self: Shape): SM.ServiceMap<Self>
18
17
  // a version that leverages the Shape -> Self conversion
19
18
  toLayer: <E, R>(
20
19
  eff: Effect.Effect<Shape, E, R>
@@ -24,11 +23,11 @@ export interface Opaque<Self extends object, in out Shape extends object> extend
24
23
  }
25
24
 
26
25
  // export interface OpaqueMake<Self extends object, in out Shape extends object, E, R>
27
- // extends ServiceMap.Service<Self, Self>
26
+ // extends SM.Service<Self, Self>
28
27
  // {
29
28
  // // temp while sorting out https://github.com/Effect-TS/effect-smol/pull/1534
30
29
  // of(self: Shape): Self
31
- // serviceMap2(self: Shape): ServiceMap.ServiceMap<Self>
30
+ // serviceMap2(self: Shape): SM.ServiceMap<Self>
32
31
  // // a version that leverages the Shape -> Self conversion
33
32
  // toLayer: {
34
33
  // <E, R>(
@@ -43,7 +42,7 @@ export function assignTag<Identifier extends object, Shape extends object = Iden
43
42
  creationError?: Error
44
43
  ) {
45
44
  return <S extends object>(cls: S): S & Opaque<Identifier, Shape> => {
46
- const tag = ServiceMap.Service<Identifier, Shape>(key)
45
+ const tag = SM.Service<Identifier, Shape>(key)
47
46
  let fields = tag
48
47
  if (Reflect.ownKeys(cls).includes("key")) {
49
48
  const { key, ...rest } = tag
@@ -151,7 +150,7 @@ export const Opaque: {
151
150
  id: Identifier,
152
151
  options?: {
153
152
  readonly make: ((...args: Args) => Effect.Effect<Shape, E, R>) | Effect.Effect<Shape, E, R> | undefined
154
- } | undefined
153
+ }
155
154
  ) =>
156
155
  & OpaqueClass<Self, Identifier, Shape>
157
156
  & ([Types.unassigned] extends [R] ? unknown
@@ -178,10 +177,10 @@ export const Opaque: {
178
177
  >
179
178
  & { readonly make: Make }
180
179
  } = () => (id: string, options: any) => {
181
- const svc = ServiceMap.Service()(id, options) as any
180
+ const svc = SM.Service()(id, options) as any
182
181
  return Object.assign(svc, {
183
182
  toLayer: (eff: Effect.Effect<any, any, any>) => {
184
- return Layer.effect(svc as any, eff)
183
+ return Layer.effect(svc, eff)
185
184
  }
186
185
  })
187
186
  }
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,7 +197,7 @@ 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
@@ -210,10 +209,10 @@ 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
- _.env.state = s as any
215
+ _.env.state = s
217
216
  return a
218
217
  })
219
218
  ) as any
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { pipe, Struct as Struct2 } from "effect"
2
+ import { Effect, Option, pipe, Schema, SchemaAST, SchemaIssue, Struct as Struct2 } from "effect"
3
3
  import type { Struct } from "effect/Schema"
4
4
  import * as S from "effect/Schema"
5
5
 
@@ -13,9 +13,6 @@ type MissingSelfGeneric<Usage extends string, Params extends string = ""> =
13
13
  `Missing \`Self\` generic - use \`class Self extends ${Usage}<Self>()(${Params}{ ... })\``
14
14
 
15
15
  export interface PropsExtensions<Fields> {
16
- // include: <NewProps extends S.Struct.Fields>(
17
- // fnc: (fields: Fields) => NewProps
18
- // ) => NewProps
19
16
  pick: <P extends keyof Fields>(...keys: readonly P[]) => Pick<Fields, P>
20
17
  omit: <P extends keyof Fields>(...keys: readonly P[]) => Omit<Fields, P>
21
18
  }
@@ -26,6 +23,57 @@ type HasFields<Fields extends Struct.Fields> = {
26
23
  readonly from: HasFields<Fields>
27
24
  }
28
25
 
26
+ export type Class<Self, S extends S.Top & { readonly fields: Struct.Fields }, Inherited> = S.Class<Self, S, Inherited>
27
+
28
+ /**
29
+ * Build a modified Declaration that accepts struct-matching values during
30
+ * encoding, given the original Declaration and the class's fields.
31
+ */
32
+ function makeRelaxedDeclaration(
33
+ ast: SchemaAST.Declaration,
34
+ fields: Schema.Struct.Fields,
35
+ cls: any
36
+ ): SchemaAST.Declaration {
37
+ const structSchema = Schema.Struct(fields)
38
+ const isStructValue = Schema.is(structSchema)
39
+ return new SchemaAST.Declaration(
40
+ ast.typeParameters,
41
+ () => (input: unknown, self: SchemaAST.Declaration) => {
42
+ if (input instanceof cls || isStructValue(input)) {
43
+ return Effect.succeed(input)
44
+ }
45
+ return Effect.fail(new SchemaIssue.InvalidType(self, Option.some(input)))
46
+ },
47
+ ast.annotations,
48
+ ast.checks,
49
+ ast.encoding,
50
+ ast.context
51
+ )
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Class — like Schema.Class but with relaxed encoding
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Like `Schema.Class`, but the resulting class accepts plain objects matching
60
+ * the struct schema during encoding — not only `instanceof` or type-id
61
+ * checks.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { Schema } from "effect"
66
+ * import { Class } from "./Class.js"
67
+ *
68
+ * class A extends Class<A>("A")({ a: Schema.String }) {}
69
+ *
70
+ * // Construction works as normal:
71
+ * new A({ a: "hello" })
72
+ *
73
+ * // Encoding accepts plain objects:
74
+ * Schema.encodeUnknownSync(A)({ a: "hello" }) // { a: "hello" }
75
+ * ```
76
+ */
29
77
  export const Class: <Self = never>(identifier: string) => <Fields extends S.Struct.Fields>(
30
78
  fieldsOr: Fields | HasFields<Fields>,
31
79
  annotations?: ClassAnnotations<Self>
@@ -35,38 +83,84 @@ export const Class: <Self = never>(identifier: string) => <Fields extends S.Stru
35
83
  S.Struct<Fields>,
36
84
  {}
37
85
  > = (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)
86
+ // Build the original Schema.Class
87
+ const Base = (S.Class as any)(identifier)(fields, annotations)
88
+ // Get the original ast getter from the base class
89
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
90
+
91
+ // Cache per-class to avoid recomputing
92
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
93
+
94
+ return class extends Base {
95
+ static get ast(): SchemaAST.Declaration {
96
+ let cached = astCache.get(this)
97
+ if (cached !== undefined) return cached
98
+ // Call the original getter with `this` bound to the actual user class,
99
+ // so getClassSchema(this) creates a schema that uses `new this(...)`.
100
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
101
+ cached = makeRelaxedDeclaration(originalAst, Base.fields, this)
102
+ astCache.set(this, cached)
103
+ return cached
42
104
  }
43
- // static readonly include = include(fields)
44
105
  static readonly pick = (...selection: any[]) => pipe(this["fields"], Struct2.pick(selection))
45
106
  static readonly omit = (...selection: any[]) => pipe(this["fields"], Struct2.omit(selection))
46
107
  } as any
47
108
  }
48
109
 
49
- export const TaggedClass: <Self = never>(identifier?: string) => <Tag extends string, Fields extends S.Struct.Fields>(
110
+ // ---------------------------------------------------------------------------
111
+ // TaggedClass — like Schema.TaggedClass but with relaxed encoding
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Like `Schema.TaggedClass`, but the resulting class accepts plain objects
116
+ * matching the struct schema during encoding.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { Schema } from "effect"
121
+ * import { TaggedClass } from "./Class.js"
122
+ *
123
+ * class Circle extends TaggedClass<Circle>()("Circle", {
124
+ * radius: Schema.Number
125
+ * }) {}
126
+ *
127
+ * Schema.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 })
128
+ * ```
129
+ */
130
+ export const TaggedClass: <Self = never>(
131
+ identifier?: string
132
+ ) => <Tag extends string, Fields extends S.Struct.Fields>(
50
133
  tag: Tag,
51
134
  fieldsOr: Fields | HasFields<Fields>,
52
135
  annotations?: ClassAnnotations<Self>
53
- ) => [Self] extends [never] ? MissingSelfGeneric<"Class">
136
+ ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedClass">
54
137
  : EnhancedClass<
55
138
  Self,
56
139
  S.Struct<{ readonly _tag: S.tag<Tag> } & Fields>,
57
140
  {}
58
141
  > = (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)
142
+ const Base = (S.TaggedClass as any)(identifier)(tag, fields, annotations)
143
+ const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")!
144
+ const astCache = new WeakMap<any, SchemaAST.Declaration>()
145
+
146
+ return class extends Base {
147
+ static get ast(): SchemaAST.Declaration {
148
+ let cached = astCache.get(this)
149
+ if (cached !== undefined) return cached
150
+ const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration
151
+ cached = makeRelaxedDeclaration(originalAst, Base.fields, this)
152
+ astCache.set(this, cached)
153
+ return cached
63
154
  }
64
- // static readonly include = include(fields)
65
155
  static readonly pick = (...selection: any[]) => pipe(this["fields"], Struct2.pick(selection))
66
156
  static readonly omit = (...selection: any[]) => pipe(this["fields"], Struct2.omit(selection))
67
157
  } as any
68
158
  }
69
159
 
160
+ // ---------------------------------------------------------------------------
161
+ // ExtendedClass — like Class but with extra type parameter for hierarchies
162
+ // ---------------------------------------------------------------------------
163
+
70
164
  export const ExtendedClass: <Self, _SelfFrom>(identifier: string) => <Fields extends S.Struct.Fields>(
71
165
  fieldsOr: Fields | HasFields<Fields>,
72
166
  annotations?: ClassAnnotations<Self>
@@ -76,6 +170,10 @@ export const ExtendedClass: <Self, _SelfFrom>(identifier: string) => <Fields ext
76
170
  {}
77
171
  > = Class as any
78
172
 
173
+ // ---------------------------------------------------------------------------
174
+ // ExtendedTaggedClass — like TaggedClass but with extra type parameter for hierarchies
175
+ // ---------------------------------------------------------------------------
176
+
79
177
  export interface EnhancedTaggedClass<Self, Tag extends string, Fields extends Struct.Fields, SelfFrom>
80
178
  extends
81
179
  EnhancedClass<
@@ -0,0 +1,216 @@
1
+ /**
2
+ * SpecialJsonSchema — A variant of Schema.toJsonSchemaDocument that deduplicates
3
+ * references sharing the same identifier when they produce identical
4
+ * representations (based on a string fingerprint).
5
+ *
6
+ * Without this, two different AST nodes that have the same identifier and
7
+ * resolve to the same JSON Schema representation can end up as separate $defs
8
+ * entries (e.g. "X" and "X1"). This converter collapses them into one.
9
+ */
10
+ import { Formatter, type JsonSchema, type Schema, SchemaRepresentation } from "effect"
11
+
12
+ /**
13
+ * Converts a schema to a JSON Schema Document (draft-2020-12), with an
14
+ * extra deduplication pass that collapses references sharing the same
15
+ * base identifier when their representations are identical.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { Schema, SchemaGetter, Option, Predicate } from "effect"
20
+ * import { specialJsonSchemaDocument } from "./SpecialJsonSchema.js"
21
+ *
22
+ * const X = Schema.String.annotate({ title: "X", identifier: "X" })
23
+ *
24
+ * const s = Schema.Struct({
25
+ * a: Schema.NullOr(X).pipe(
26
+ * Schema.encodeTo(Schema.optionalKey(X), {
27
+ * decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
28
+ * encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
29
+ * })
30
+ * ),
31
+ * b: Schema.NullOr(X),
32
+ * c: X
33
+ * })
34
+ *
35
+ * // Without dedup: $defs would contain both "X" and "X1" (identical).
36
+ * // With specialJsonSchemaDocument: only "X" is emitted.
37
+ * const doc = specialJsonSchemaDocument(s)
38
+ * ```
39
+ */
40
+ export function specialJsonSchemaDocument(
41
+ schema: Schema.Top,
42
+ options?: Schema.ToJsonSchemaOptions
43
+ ): JsonSchema.Document<"draft-2020-12"> {
44
+ const doc = SchemaRepresentation.fromAST(schema.ast)
45
+ const deduped = deduplicateReferences(doc)
46
+ const jd = SchemaRepresentation.toJsonSchemaDocument(deduped, options)
47
+ return {
48
+ dialect: "draft-2020-12",
49
+ schema: jd.schema,
50
+ definitions: jd.definitions
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Deduplicates references in a Document: when multiple $ref keys share
56
+ * the same base identifier (e.g. "X" and "X1") and their representations
57
+ * are identical (by string fingerprint), the duplicates are collapsed into
58
+ * the first entry found for that identifier.
59
+ */
60
+ function deduplicateReferences(
61
+ doc: SchemaRepresentation.Document
62
+ ): SchemaRepresentation.Document {
63
+ const refs = doc.references
64
+ const refKeys = Object.keys(refs)
65
+ if (refKeys.length === 0) return doc
66
+
67
+ // Group references by base identifier (strip trailing digits added by gen())
68
+ const identifierGroups = new Map<string, Array<{ key: string; fingerprint: string }>>()
69
+ for (const key of refKeys) {
70
+ const base = getBaseIdentifier(key)
71
+ const fingerprint = Formatter.format(refs[key])
72
+ const group = identifierGroups.get(base)
73
+ if (group === undefined) {
74
+ identifierGroups.set(base, [{ key, fingerprint }])
75
+ } else {
76
+ group.push({ key, fingerprint })
77
+ }
78
+ }
79
+
80
+ // Build a mapping from duplicate keys to their canonical key
81
+ const remapping = new Map<string, string>()
82
+ for (const [, group] of identifierGroups) {
83
+ const seen = new Map<string, string>() // fingerprint -> canonical key
84
+ for (const entry of group) {
85
+ const canonical = seen.get(entry.fingerprint)
86
+ if (canonical !== undefined) {
87
+ remapping.set(entry.key, canonical)
88
+ } else {
89
+ seen.set(entry.fingerprint, entry.key)
90
+ }
91
+ }
92
+ }
93
+
94
+ if (remapping.size === 0) return doc
95
+
96
+ // Build new references, excluding duplicates
97
+ const newRefs: Record<string, SchemaRepresentation.Representation> = {}
98
+ for (const key of refKeys) {
99
+ if (!remapping.has(key)) {
100
+ newRefs[key] = refs[key]!
101
+ }
102
+ }
103
+
104
+ // Rewrite $ref pointers throughout the document
105
+ const newRepresentation = rewriteRefs(doc.representation, remapping)
106
+ const rewrittenRefs: Record<string, SchemaRepresentation.Representation> = {}
107
+ for (const [key, rep] of Object.entries(newRefs)) {
108
+ rewrittenRefs[key] = rewriteRefs(rep, remapping)
109
+ }
110
+
111
+ return { representation: newRepresentation, references: rewrittenRefs }
112
+ }
113
+
114
+ /**
115
+ * Extracts the base identifier from a reference key by stripping trailing
116
+ * digits appended by the gen() function during fromASTs.
117
+ * E.g. "X1" -> "X", "X" -> "X", "MyType2" -> "MyType"
118
+ */
119
+ function getBaseIdentifier(key: string): string {
120
+ const match = key.match(/^(.+?)(\d+)$/)
121
+ return match ? match[1]! : key
122
+ }
123
+
124
+ /**
125
+ * Recursively rewrites $ref pointers in a Representation tree.
126
+ */
127
+ function rewriteRefs(
128
+ rep: SchemaRepresentation.Representation,
129
+ remapping: Map<string, string>
130
+ ): SchemaRepresentation.Representation {
131
+ switch (rep._tag) {
132
+ case "Reference": {
133
+ const target = remapping.get(rep.$ref)
134
+ return target !== undefined ? { ...rep, $ref: target } : rep
135
+ }
136
+ case "Declaration":
137
+ return {
138
+ ...rep,
139
+ typeParameters: rep.typeParameters.map((tp) => rewriteRefs(tp, remapping)),
140
+ encodedSchema: rewriteRefs(rep.encodedSchema, remapping)
141
+ }
142
+ case "Suspend":
143
+ return {
144
+ ...rep,
145
+ thunk: rewriteRefs(rep.thunk, remapping)
146
+ }
147
+ case "Arrays":
148
+ return {
149
+ ...rep,
150
+ elements: rep.elements.map((e) => ({ ...e, type: rewriteRefs(e.type, remapping) })),
151
+ rest: rep.rest.map((r) => rewriteRefs(r, remapping))
152
+ }
153
+ case "Objects":
154
+ return {
155
+ ...rep,
156
+ propertySignatures: rep.propertySignatures.map((ps) => ({
157
+ ...ps,
158
+ type: rewriteRefs(ps.type, remapping)
159
+ })),
160
+ indexSignatures: rep.indexSignatures.map((is) => ({
161
+ ...is,
162
+ parameter: rewriteRefs(is.parameter, remapping),
163
+ type: rewriteRefs(is.type, remapping)
164
+ })),
165
+ checks: rewriteChecks(rep.checks, remapping)
166
+ }
167
+ case "Union":
168
+ return {
169
+ ...rep,
170
+ types: rep.types.map((t) => rewriteRefs(t, remapping))
171
+ }
172
+ case "TemplateLiteral":
173
+ return {
174
+ ...rep,
175
+ parts: rep.parts.map((p) => rewriteRefs(p, remapping))
176
+ }
177
+ case "String": {
178
+ if (rep.contentSchema !== undefined) {
179
+ return {
180
+ ...rep,
181
+ contentSchema: rewriteRefs(rep.contentSchema, remapping)
182
+ }
183
+ }
184
+ return rep
185
+ }
186
+ default:
187
+ // Leaf nodes: Null, Undefined, Void, Never, Unknown, Any, Boolean,
188
+ // Symbol, Number, BigInt, Literal, UniqueSymbol, ObjectKeyword, Enum
189
+ return rep
190
+ }
191
+ }
192
+
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ function rewriteChecks<M>(
195
+ checks: ReadonlyArray<SchemaRepresentation.Check<M>>,
196
+ remapping: Map<string, string>
197
+ ): ReadonlyArray<SchemaRepresentation.Check<M>> {
198
+ return checks.map((c) => {
199
+ switch (c._tag) {
200
+ case "Filter": {
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ const meta = c.meta as any
203
+ if (meta && meta._tag === "isPropertyNames" && meta.propertyNames) {
204
+ return {
205
+ ...c,
206
+ meta: { ...meta, propertyNames: rewriteRefs(meta.propertyNames, remapping) }
207
+ }
208
+ }
209
+ return c
210
+ }
211
+ case "FilterGroup":
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ return { ...c, checks: rewriteChecks(c.checks, remapping) as any }
214
+ }
215
+ })
216
+ }