effect-app 4.0.0-beta.12 → 4.0.0-beta.121

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 +466 -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 +104 -27
  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 +48 -10
  24. package/dist/Schema/Class.d.ts.map +1 -1
  25. package/dist/Schema/Class.js +120 -16
  26. package/dist/Schema/SpecialJsonSchema.d.ts +33 -0
  27. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  28. package/dist/Schema/SpecialJsonSchema.js +122 -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 +111 -46
  38. package/dist/Schema/ext.d.ts.map +1 -1
  39. package/dist/Schema/ext.js +114 -53
  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 +74 -55
  51. package/dist/Schema.d.ts.map +1 -1
  52. package/dist/Schema.js +85 -64
  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 +24 -0
  90. package/dist/utils.d.ts.map +1 -1
  91. package/dist/utils.js +28 -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 +157 -30
  101. package/src/Schema/SpecialJsonSchema.ts +137 -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 +191 -85
  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 +195 -104
  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 +30 -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 +504 -4
  131. package/test/secretURL.test.ts +157 -0
  132. package/test/special.test.ts +862 -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
@@ -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
  )
package/src/Schema/ext.ts CHANGED
@@ -1,56 +1,94 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
- import { Effect, Option, pipe, Schema, type SchemaAST, SchemaGetter, SchemaIssue, SchemaParser, SchemaTransformation, type ServiceMap } from "effect"
3
+ import { Effect, Function, Option, pipe, type SchemaAST, SchemaIssue, SchemaTransformation } from "effect"
4
4
  import * as S from "effect/Schema"
5
+ import { isDateValid } from "effect/Schema"
5
6
  import { type NonEmptyReadonlyArray } from "../Array.js"
7
+ import * as Context from "../Context.js"
6
8
  import { extendM, typedKeysOf } from "../utils.js"
7
9
  import { type AST } from "./schema.js"
8
10
 
9
- // TODO: v4 migration withConstructorDefault signature changed, propertySignature removed
10
- // Constraint relaxed from `Self extends S.Top & S.WithoutConstructorDefault` to `Self extends S.Top`
11
- // because `.pipe()` widens the schema type to `Top` which doesn't satisfy `WithoutConstructorDefault`.
12
- // The narrowing assertions below are safe — we're asserting "this schema hasn't had a default applied yet".
13
- export const withDefaultConstructor = <A>(
14
- makeDefault: () => NoInfer<A>
15
- ) =>
16
- <Self extends S.Top>(self: Self): S.withConstructorDefault<Self & S.WithoutConstructorDefault> => {
17
- type Narrowed = Self & S.WithoutConstructorDefault
18
- return S.withConstructorDefault<Narrowed>(
19
- () => Option.some(makeDefault() as Narrowed["~type.make.in"])
20
- )(self as Narrowed)
21
- }
11
+ type ProvidedCodec<Self extends S.Top, R> = S.Codec<
12
+ Self["Type"],
13
+ Self["Encoded"],
14
+ Exclude<Self["DecodingServices"], R>,
15
+ Exclude<Self["EncodingServices"], R>
16
+ >
22
17
 
23
18
  // TODO: v4 migration - Date is no longer by default encoded to string.
24
- const DateFromString = Schema.Date.pipe(
25
- Schema.encodeTo(Schema.String, {
26
- decode: SchemaGetter.Date(),
27
- encode: SchemaGetter.transform((_) => _.toISOString())
28
- })
29
- )
19
+
20
+ const DateString = S.String.annotate({
21
+ identifier: "Date",
22
+ description: "a string in ISO 8601 format that will be decoded as a Date",
23
+ format: "date-time"
24
+ })
25
+
26
+ /**
27
+ * Schema type for {@link DateFromString}.
28
+ *
29
+ * @category Schemas
30
+ * @since 4.0.0
31
+ */
32
+ export interface DateFromString extends S.decodeTo<S.Date, S.String> {}
33
+
34
+ /**
35
+ * A transformation schema that parses an ISO 8601 string into a `Date`.
36
+ *
37
+ * Decoding:
38
+ * - A `string` is decoded as a `Date`.
39
+ *
40
+ * Encoding:
41
+ * - A `Date` is encoded as a `string`.
42
+ *
43
+ * @since 4.0.0
44
+ */
45
+ export const DateFromString: DateFromString = DateString.pipe(S.decodeTo(S.Date, SchemaTransformation.dateFromString))
30
46
 
31
47
  /**
32
48
  * Like the default Schema `Date` but from String with `withDefault` => now
33
49
  */
34
50
  export const Date = Object.assign(DateFromString, {
35
- withDefault: DateFromString.pipe(withDefaultConstructor(() => new global.Date()))
51
+ withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
52
+ withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
53
+ })
54
+
55
+ /**
56
+ * Like the default Schema `DateValid` but from String with `withDefault` => now
57
+ */
58
+ export const DateValid = Object.assign(Date.check(isDateValid()), {
59
+ withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
60
+ withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
36
61
  })
37
62
 
38
63
  /**
39
64
  * Like the default Schema `Boolean` but with `withDefault` => false
40
65
  */
41
66
  export const Boolean = Object.assign(S.Boolean, {
42
- withDefault: S.Boolean.pipe(withDefaultConstructor(() => false))
67
+ withDefault: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(false))),
68
+ withDecodingDefaultType: S.Boolean.pipe(S.withDecodingDefaultType(Effect.succeed(false)))
43
69
  })
44
70
 
45
71
  /**
72
+ * You probably want to use `Finite` instead of this.
46
73
  * Like the default Schema `Number` but with `withDefault` => 0
47
74
  */
48
- export const Number = Object.assign(S.Number, { withDefault: S.Number.pipe(withDefaultConstructor(() => 0)) })
75
+ export const Number = Object.assign(S.Number, {
76
+ withDefault: S.Number.pipe(S.withConstructorDefault(Effect.succeed(0))),
77
+ withDecodingDefaultType: S.Number.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
78
+ })
79
+
80
+ /**
81
+ * Like the default Schema `Finite` but with `withDefault` => 0
82
+ */
83
+ export const Finite = Object.assign(S.Finite, {
84
+ withDefault: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))),
85
+ withDecodingDefaultType: S.Finite.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
86
+ })
49
87
 
50
88
  /**
51
- * Like the default Schema `Literal` but with `withDefault` => literals[0]
89
+ * Like the default Schema `Literals` but with `withDefault` => literals[0]
52
90
  */
53
- export const Literal = <Literals extends NonEmptyReadonlyArray<AST.LiteralValue>>(...literals: Literals) =>
91
+ export const Literals = <const Literals extends NonEmptyReadonlyArray<AST.LiteralValue>>(literals: Literals) =>
54
92
  pipe(
55
93
  S.Literals(literals),
56
94
  (s) =>
@@ -58,79 +96,143 @@ export const Literal = <Literals extends NonEmptyReadonlyArray<AST.LiteralValue>
58
96
  changeDefault: <A extends Literals[number]>(a: A) => {
59
97
  return Object.assign(S.Literals(literals), {
60
98
  Default: a,
61
- withDefault: s.pipe(withDefaultConstructor(() => a))
99
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(a))),
100
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(a)))
62
101
  }) // todo: copy annotations from original?
63
102
  },
64
103
  Default: literals[0] as typeof literals[0],
65
- withDefault: s.pipe(withDefaultConstructor(() => literals[0]))
104
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))),
105
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(literals[0])))
66
106
  })
67
107
  )
68
108
 
69
109
  /**
70
110
  * Like the default Schema `Array` but with `withDefault` => []
71
111
  */
72
- export function Array<Value extends S.Top>(value: Value) {
112
+ const co = { parseOptions: { concurrency: "unbounded" as const } }
113
+ export { co as concurrencyUnbounded }
114
+
115
+ export function Array<ValueSchema extends S.Top>(value: ValueSchema) {
73
116
  return pipe(
74
- S.Array(value),
75
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => [])) })
117
+ S.Array(value).annotate(co),
118
+ (s) =>
119
+ Object.assign(s, {
120
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => []))),
121
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => [])))
122
+ })
76
123
  )
77
124
  }
78
125
 
79
126
  /**
80
- * Like the default Schema `Map` but with `withDefault` => []
127
+ * An annotated `S.Array` of unique items that decodes to a `ReadonlySet`.
81
128
  */
82
- function Map_<Key extends S.Top, Value extends S.Top>(input: { key: Key; value: Value }) {
83
- return pipe(
84
- S.ReadonlyMap(input.key, input.value),
85
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new global.Map())) })
129
+ export const ReadonlySetFromArray = <ValueSchema extends S.Top>(value: ValueSchema) => {
130
+ const from = S
131
+ .Array(value)
132
+ .annotate({ ...co, expected: "an array of unique items that will be decoded as a ReadonlySet" })
133
+ const to = S.instanceOf(Set) as S.instanceOf<ReadonlySet<S.Schema.Type<ValueSchema>>>
134
+ const schema = from.pipe(
135
+ S.decodeTo(
136
+ to,
137
+ SchemaTransformation.transform({
138
+ decode: (arr) => new Set(arr) as ReadonlySet<S.Schema.Type<ValueSchema>>,
139
+ encode: (set) => [...set]
140
+ })
141
+ )
86
142
  )
143
+ return schema
87
144
  }
88
145
 
89
- export { Map_ as Map }
146
+ /**
147
+ * An annotated `S.Array` of key-value tuples that decodes to a `ReadonlyMap`.
148
+ */
149
+ export const ReadonlyMapFromArray = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
150
+ readonly key: KeySchema
151
+ readonly value: ValueSchema
152
+ }) => {
153
+ const from = S
154
+ .Array(S.Tuple([pair.key, pair.value]))
155
+ .annotate({ ...co, expected: "an array of key-value tuples that will be decoded as a ReadonlyMap" })
156
+ const to = S.instanceOf(Map) as S.instanceOf<
157
+ ReadonlyMap<S.Schema.Type<KeySchema>, S.Schema.Type<ValueSchema>>
158
+ >
159
+ const schema = from.pipe(
160
+ S.decodeTo(
161
+ to,
162
+ SchemaTransformation.transform({
163
+ decode: (
164
+ arr
165
+ ) => new Map(arr) as ReadonlyMap<S.Schema.Type<KeySchema>, S.Schema.Type<ValueSchema>>,
166
+ encode: (
167
+ map
168
+ ) => [...map.entries()] as any // fu
169
+ })
170
+ )
171
+ )
172
+ return schema
173
+ }
90
174
 
91
175
  /**
92
- * Like the default Schema `ReadonlySet` but with `withDefault` => new Set()
176
+ * Like the default Schema `ReadonlySet` but from Array with `withDefault` => new Set()
93
177
  */
94
- export const ReadonlySet = <Value extends S.Top>(value: Value) =>
178
+ export const ReadonlySet = <ValueSchema extends S.Top>(value: ValueSchema) =>
95
179
  pipe(
96
- S.ReadonlySet(value),
97
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new Set<S.Schema.Type<Value>>())) })
180
+ ReadonlySetFromArray(value),
181
+ (s) =>
182
+ Object.assign(s, {
183
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))),
184
+ withDecodingDefaultType: s.pipe(
185
+ S.withDecodingDefaultType(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))
186
+ )
187
+ })
98
188
  )
99
189
 
100
190
  /**
101
- * Like the default Schema `ReadonlyMap` but with `withDefault` => new Map()
191
+ * Like the default Schema `ReadonlyMap` but from Array with `withDefault` => new Map()
102
192
  */
103
- export const ReadonlyMap = <K extends S.Top, V extends S.Top>(pair: {
104
- readonly key: K
105
- readonly value: V
193
+ export const ReadonlyMap = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
194
+ readonly key: KeySchema
195
+ readonly value: ValueSchema
106
196
  }) =>
107
197
  pipe(
108
- S.ReadonlyMap(pair.key, pair.value),
109
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new Map())) })
198
+ ReadonlyMapFromArray(pair),
199
+ (s) =>
200
+ Object.assign(s, {
201
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))),
202
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new Map())))
203
+ })
110
204
  )
111
205
 
112
206
  /**
113
207
  * Like the default Schema `NullOr` but with `withDefault` => null
114
208
  */
115
- export const NullOr = <S extends S.Top>(self: S) =>
209
+ export const NullOr = <Schema extends S.Top>(self: Schema) =>
116
210
  pipe(
117
211
  S.NullOr(self),
118
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => null)) })
212
+ (s) =>
213
+ Object.assign(s, {
214
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(null))),
215
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(null)))
216
+ })
119
217
  )
120
218
 
121
- export const defaultDate = (s: S.Top) => s.pipe(withDefaultConstructor(() => new global.Date()))
219
+ export const defaultDate = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
220
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date())))
122
221
 
123
- export const defaultBool = (s: S.Top) => s.pipe(withDefaultConstructor(() => false))
222
+ export const defaultBool = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
223
+ schema.pipe(S.withConstructorDefault(Effect.succeed(false)))
124
224
 
125
- export const defaultNullable = (
126
- s: S.Top
127
- ) => s.pipe(withDefaultConstructor(() => null))
225
+ export const defaultNullable = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
226
+ schema.pipe(S.withConstructorDefault(Effect.succeed(null)))
128
227
 
129
- export const defaultArray = (s: S.Top) => s.pipe(withDefaultConstructor(() => []))
228
+ export const defaultArray = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
229
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => [])))
130
230
 
131
- export const defaultMap = (s: S.Top) => s.pipe(withDefaultConstructor(() => new Map()))
231
+ export const defaultMap = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
232
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new Map())))
132
233
 
133
- export const defaultSet = (s: S.Top) => s.pipe(withDefaultConstructor(() => new Set()))
234
+ export const defaultSet = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
235
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new Set())))
134
236
 
135
237
  export const withDefaultMake = <Self extends S.Top>(s: Self) => {
136
238
  const a = Object.assign(S.decodeSync(s as any) as WithDefaults<Self>, s)
@@ -160,8 +262,11 @@ export type WithDefaults<Self extends S.Top> = (
160
262
  // : never
161
263
 
162
264
  export const inputDate = extendM(
163
- S.Union([S.DateValid, S.Date]),
164
- (s) => ({ withDefault: s.pipe(withDefaultConstructor(() => new globalThis.Date())) })
265
+ S.Union([S.DateValid, Date]),
266
+ (s) => ({
267
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))),
268
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new globalThis.Date())))
269
+ })
165
270
  )
166
271
 
167
272
  export interface UnionBrand {}
@@ -211,7 +316,7 @@ export const transformTo = <To extends S.Top, From extends S.Top>(
211
316
  { message: "One way schema transformation, encoding is not allowed" }
212
317
  )
213
318
  )
214
- }) as any
319
+ })
215
320
  )
216
321
  )
217
322
 
@@ -228,7 +333,7 @@ export const transformToOrFail = <To extends S.Top, From extends S.Top, RD>(
228
333
  S.decodeTo(
229
334
  to,
230
335
  SchemaTransformation.transformOrFail({
231
- decode: decode as any,
336
+ decode,
232
337
  encode: (i: any) =>
233
338
  Effect.fail(
234
339
  new SchemaIssue.Forbidden(
@@ -236,33 +341,34 @@ export const transformToOrFail = <To extends S.Top, From extends S.Top, RD>(
236
341
  { message: "One way schema transformation, encoding is not allowed" }
237
342
  )
238
343
  )
239
- }) as any
344
+ })
240
345
  )
241
346
  )
242
347
 
243
- // TODO: v4 migration — S.declare API changed (no [self] + decode/encode pattern)
244
- // Need to find v4 equivalent for contextual schema wrapping
245
- export const provide = <Self extends S.Top, R>(
246
- self: Self,
247
- context: ServiceMap.ServiceMap<R>
248
- ): any => {
348
+ export const provide: {
349
+ <R>(context: Context.Context<R>): <Self extends S.Top>(self: Self) => ProvidedCodec<Self, R>
350
+ <Self extends S.Top, R>(self: Self, context: Context.Context<R>): ProvidedCodec<Self, R>
351
+ } = Function.dual(2, <Self extends S.Top, R>(self: Self, context: Context.Context<R>): ProvidedCodec<Self, R> => {
249
352
  const prov = Effect.provide(context)
250
- return S
251
- .declare((_u: unknown): _u is unknown => true) // placeholder — needs proper v4 declare
252
- .pipe(
253
- S.decodeTo(
254
- self,
255
- SchemaTransformation.transformOrFail({
256
- decode: (n: any) => prov(SchemaParser.decodeUnknownEffect(self)(n)),
257
- encode: (n: any) => prov(SchemaParser.encodeUnknownEffect(self)(n))
258
- }) as any
259
- ) as any
353
+ return self.pipe(
354
+ S.middlewareDecoding((effect) => prov(effect)),
355
+ S.middlewareEncoding((effect) => prov(effect))
356
+ ) as ProvidedCodec<Self, R>
357
+ })
358
+ export const contextFromServices = <
359
+ Self extends S.Top,
360
+ Tags extends ReadonlyArray<Context.Key<any, any>>
361
+ >(
362
+ self: Self,
363
+ ...services: Tags
364
+ ): Effect.Effect<
365
+ ProvidedCodec<Self, Context.Service.Identifier<Tags[number]>>,
366
+ never,
367
+ Context.Service.Identifier<Tags[number]>
368
+ > =>
369
+ Effect.gen(function*() {
370
+ const context: Context.Context<Context.Service.Identifier<Tags[number]>> = Context.pick(...services)(
371
+ yield* Effect.context<Context.Service.Identifier<Tags[number]>>()
260
372
  )
261
- }
262
- // TODO: v4 migration — ServiceMap.pick and S.declare pattern removed
263
- export const contextFromServices = <Self extends S.Top, Tags extends readonly any[]>(
264
- _self: Self,
265
- ..._services: Tags
266
- ): any => {
267
- throw new Error("contextFromServices: not yet migrated to v4")
268
- }
373
+ return provide(self, context)
374
+ })
@@ -1,4 +1,4 @@
1
- import { pipe } from "effect"
1
+ import { Effect, pipe } from "effect"
2
2
  import type { Refinement } from "effect-app/Function"
3
3
  import { extendM } from "effect-app/utils"
4
4
  import * as S from "effect/Schema"
@@ -6,7 +6,7 @@ import type { Simplify } from "effect/Types"
6
6
  import { customRandom, nanoid, urlAlphabet } from "nanoid"
7
7
  import validator from "validator"
8
8
  import { fromBrand, nominal } from "./brand.js"
9
- import { withDefaultConstructor, withDefaultMake, type WithDefaults } from "./ext.js"
9
+ import { withDefaultMake, type WithDefaults } from "./ext.js"
10
10
  import { type B } from "./schema.js"
11
11
  import type { NonEmptyString255Brand, NonEmptyStringBrand } from "./strings.js"
12
12
 
@@ -140,10 +140,10 @@ const minLength = 6
140
140
  const maxLength = 50
141
141
  const size = 21
142
142
  const length = 10 * size
143
- const StringIdArb = (): S.LazyArbitrary<string> => (fc) =>
143
+ const StringIdArb = (): S.LazyArbitrary<StringId> => (fc) =>
144
144
  fc
145
145
  .uint8Array({ minLength: length, maxLength: length })
146
- .map((_) => customRandom(urlAlphabet, size, (size) => _.subarray(0, size))())
146
+ .map((_) => customRandom(urlAlphabet, size, (size) => _.subarray(0, size))() as StringId)
147
147
  /**
148
148
  * A string that is at least 6 characters long and a maximum of 50.
149
149
  */
@@ -154,13 +154,13 @@ export const StringId = extendM(
154
154
  fromBrand(nominal<StringId>(), {
155
155
  identifier: "StringId",
156
156
  title: "StringId",
157
- arbitrary: StringIdArb,
157
+ toArbitrary: () => (fc) => StringIdArb()(fc),
158
158
  jsonSchema: {}
159
159
  })
160
160
  ),
161
161
  (s) => ({
162
162
  make: makeStringId,
163
- withDefault: s.pipe(withDefaultConstructor(makeStringId))
163
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(makeStringId)))
164
164
  })
165
165
  )
166
166
  .pipe(withDefaultMake)
@@ -182,14 +182,16 @@ export function prefixedStringId<Brand extends StringId>() {
182
182
  (x) => (pref + x.substring(0, 50 - pref.length)) as Brand
183
183
  )
184
184
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
185
- const s: S.Codec<string & Brand, string> = StringId
185
+ const s = StringId
186
186
  .pipe(
187
187
  S.refine((x: string): x is string & Brand => x.startsWith(pref), {
188
- arbitrary: arb,
189
188
  identifier: name,
190
189
  title: name
190
+ }),
191
+ S.annotate({
192
+ toArbitrary: () => (fc) => arb()(fc)
191
193
  })
192
- ) as any
194
+ )
193
195
  const schema = s.pipe(withDefaultMake)
194
196
  const make = () => (pref + StringId.make().substring(0, 50 - pref.length)) as Brand
195
197
 
@@ -206,7 +208,9 @@ export function prefixedStringId<Brand extends StringId>() {
206
208
  */
207
209
  prefixSafe: <REST extends string>(str: `${Prefix}${Separator}${REST}`) => ex(str),
208
210
  prefix,
209
- withDefault: schema.pipe(withDefaultConstructor(make))
211
+ withDefault: schema.pipe(S.withConstructorDefault<S.Codec<Brand, string> & S.WithoutConstructorDefault>(
212
+ Effect.sync(make)
213
+ ))
210
214
  })
211
215
  )
212
216
  }
@@ -245,11 +249,17 @@ const isUrl: Refinement<string, Url> = (s: string): s is Url => {
245
249
  export const Url = S
246
250
  .String
247
251
  .pipe(
252
+ S.annotate({
253
+ title: "Url",
254
+ format: "uri"
255
+ }),
248
256
  S.refine(isUrl, {
249
- arbitrary: (): S.LazyArbitrary<Url> => (fc) => fc.webUrl().map((_) => _ as Url),
250
257
  identifier: "Url",
251
258
  title: "Url",
252
259
  jsonSchema: { format: "uri" }
253
260
  }),
261
+ S.annotate({
262
+ toArbitrary: () => (fc) => fc.webUrl().map((_) => _ as Url)
263
+ }),
254
264
  withDefaultMake
255
265
  )