@effect-app/vue-components 4.0.0-beta.32 → 4.0.0-beta.321

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 (211) hide show
  1. package/README.md +13 -9
  2. package/dist/reset.css +39 -38
  3. package/dist/types/components/CommandButton.vue.d.ts +24 -5
  4. package/dist/types/components/Dialog.vue.d.ts +4 -2
  5. package/dist/types/components/FixedNuxtErrorBoundary.vue.d.ts +39 -0
  6. package/dist/types/components/OmegaForm/InputProps.d.ts +2 -1
  7. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +2 -2
  9. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +2 -3
  10. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  12. package/dist/types/components/OmegaForm/OmegaInputVuetify.vue.d.ts +12 -0
  13. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +7 -9
  14. package/dist/types/components/OmegaForm/OmegaTaggedUnion.vue.d.ts +2 -2
  15. package/dist/types/components/OmegaForm/OmegaTaggedUnionInternal.vue.d.ts +3 -3
  16. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  17. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +4 -2
  18. package/dist/types/components/OmegaForm/decimalSeparatorInput.d.ts +13 -0
  19. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  20. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  21. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  22. package/dist/types/components/OmegaForm/index.d.ts +14 -3
  23. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  24. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  25. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +33 -0
  26. package/dist/types/components/OmegaForm/meta/defaults.d.ts +21 -0
  27. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  28. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  29. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  30. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  31. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  32. package/dist/types/components/OmegaForm/types.d.ts +312 -0
  33. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +9 -213
  34. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  35. package/dist/types/components/index.d.ts +1 -0
  36. package/dist/types/index.d.ts +0 -1
  37. package/dist/types/utils/index.d.ts +8 -8
  38. package/dist/vue-components.es.js +30 -44
  39. package/dist/vue-components10.es.js +5 -0
  40. package/dist/vue-components11.es.js +56 -0
  41. package/dist/vue-components12.es.js +5 -0
  42. package/dist/vue-components13.es.js +20 -0
  43. package/dist/vue-components14.es.js +50 -0
  44. package/dist/vue-components15.es.js +128 -0
  45. package/dist/vue-components16.es.js +65 -0
  46. package/dist/vue-components17.es.js +114 -0
  47. package/dist/vue-components18.es.js +26 -0
  48. package/dist/vue-components19.es.js +5 -0
  49. package/dist/vue-components2.es.js +11 -0
  50. package/dist/vue-components20.es.js +80 -0
  51. package/dist/vue-components21.es.js +115 -0
  52. package/dist/vue-components22.es.js +74 -0
  53. package/dist/vue-components23.es.js +12 -0
  54. package/dist/vue-components24.es.js +54 -0
  55. package/dist/vue-components25.es.js +5 -0
  56. package/dist/vue-components26.es.js +44 -0
  57. package/dist/vue-components27.es.js +5 -0
  58. package/dist/vue-components28.es.js +84 -0
  59. package/dist/vue-components3.es.js +98 -0
  60. package/dist/vue-components30.es.js +8 -0
  61. package/dist/vue-components31.es.js +9 -0
  62. package/dist/vue-components32.es.js +15 -0
  63. package/dist/vue-components33.es.js +24 -0
  64. package/dist/vue-components34.es.js +344 -0
  65. package/dist/vue-components36.es.js +8 -0
  66. package/dist/vue-components37.es.js +74 -0
  67. package/dist/vue-components38.es.js +5 -0
  68. package/dist/vue-components39.es.js +64 -0
  69. package/dist/vue-components4.es.js +5 -0
  70. package/dist/vue-components40.es.js +5 -0
  71. package/dist/vue-components41.es.js +24 -0
  72. package/dist/vue-components42.es.js +5 -0
  73. package/dist/vue-components43.es.js +59 -0
  74. package/dist/vue-components44.es.js +5 -0
  75. package/dist/vue-components45.es.js +12 -0
  76. package/dist/vue-components46.es.js +22 -0
  77. package/dist/vue-components48.es.js +9 -0
  78. package/dist/vue-components49.es.js +4 -0
  79. package/dist/vue-components5.es.js +24 -0
  80. package/dist/vue-components50.es.js +38 -0
  81. package/dist/vue-components51.es.js +27 -0
  82. package/dist/vue-components52.es.js +28 -0
  83. package/dist/vue-components53.es.js +7 -0
  84. package/dist/vue-components54.es.js +18 -0
  85. package/dist/vue-components55.es.js +36 -0
  86. package/dist/vue-components56.es.js +18 -0
  87. package/dist/vue-components57.es.js +21 -0
  88. package/dist/vue-components58.es.js +30 -0
  89. package/dist/vue-components59.es.js +7 -0
  90. package/dist/vue-components6.es.js +13 -0
  91. package/dist/vue-components60.es.js +9 -0
  92. package/dist/vue-components61.es.js +38 -0
  93. package/dist/vue-components62.es.js +25 -0
  94. package/dist/vue-components63.es.js +128 -0
  95. package/dist/vue-components64.es.js +24 -0
  96. package/dist/vue-components65.es.js +21 -0
  97. package/dist/vue-components66.es.js +9 -0
  98. package/dist/vue-components67.es.js +19 -0
  99. package/dist/vue-components68.es.js +5 -0
  100. package/dist/vue-components69.es.js +29 -0
  101. package/dist/vue-components7.es.js +13 -0
  102. package/dist/vue-components70.es.js +5 -0
  103. package/dist/vue-components71.es.js +43 -0
  104. package/dist/vue-components72.es.js +101 -0
  105. package/dist/vue-components73.es.js +36 -0
  106. package/dist/vue-components74.es.js +19 -0
  107. package/dist/vue-components75.es.js +50 -0
  108. package/dist/vue-components8.es.js +35 -0
  109. package/dist/vue-components9.es.js +47 -0
  110. package/package.json +39 -33
  111. package/src/components/CommandButton.vue +96 -16
  112. package/src/components/Dialog.vue +0 -1
  113. package/src/components/FixedNuxtErrorBoundary.vue +112 -0
  114. package/src/components/OmegaForm/InputProps.ts +2 -1
  115. package/src/components/OmegaForm/OmegaArray.vue +9 -23
  116. package/src/components/OmegaForm/OmegaAutoGen.vue +3 -2
  117. package/src/components/OmegaForm/OmegaErrorsInternal.vue +20 -17
  118. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  119. package/src/components/OmegaForm/OmegaInput.vue +29 -43
  120. package/src/components/OmegaForm/OmegaInputVuetify.vue +122 -16
  121. package/src/components/OmegaForm/OmegaInternalInput.vue +33 -13
  122. package/src/components/OmegaForm/OmegaTaggedUnion.vue +10 -3
  123. package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +6 -6
  124. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  125. package/src/components/OmegaForm/blockDialog.ts +18 -6
  126. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +16 -14
  127. package/src/components/OmegaForm/decimalSeparatorInput.ts +38 -0
  128. package/src/components/OmegaForm/errors.ts +139 -0
  129. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  130. package/src/components/OmegaForm/hocs.ts +19 -0
  131. package/src/components/OmegaForm/index.ts +17 -4
  132. package/src/components/OmegaForm/inputs.ts +22 -0
  133. package/src/components/OmegaForm/meta/checks.ts +107 -0
  134. package/src/components/OmegaForm/meta/createMeta.ts +140 -0
  135. package/src/components/OmegaForm/meta/defaults.ts +263 -0
  136. package/src/components/OmegaForm/meta/redacted.ts +71 -0
  137. package/src/components/OmegaForm/meta/types.ts +78 -0
  138. package/src/components/OmegaForm/meta/walker.ts +248 -0
  139. package/src/components/OmegaForm/persistency.ts +247 -0
  140. package/src/components/OmegaForm/submit.ts +131 -0
  141. package/src/components/OmegaForm/types.ts +801 -0
  142. package/src/components/OmegaForm/useOmegaForm.ts +103 -896
  143. package/src/components/OmegaForm/useRegisterField.ts +1 -1
  144. package/src/components/OmegaForm/validation/localized.ts +242 -0
  145. package/src/components/index.ts +1 -0
  146. package/src/index.ts +0 -1
  147. package/src/reset.css +39 -38
  148. package/src/utils/index.ts +11 -8
  149. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -159
  150. package/dist/types/constants/index.d.ts +0 -1
  151. package/dist/vue-components.es10.js +0 -239
  152. package/dist/vue-components.es11.js +0 -32
  153. package/dist/vue-components.es12.js +0 -503
  154. package/dist/vue-components.es13.js +0 -49
  155. package/dist/vue-components.es14.js +0 -4
  156. package/dist/vue-components.es15.js +0 -4
  157. package/dist/vue-components.es16.js +0 -6
  158. package/dist/vue-components.es17.js +0 -13
  159. package/dist/vue-components.es18.js +0 -57
  160. package/dist/vue-components.es19.js +0 -56
  161. package/dist/vue-components.es2.js +0 -30
  162. package/dist/vue-components.es20.js +0 -8
  163. package/dist/vue-components.es21.js +0 -8
  164. package/dist/vue-components.es22.js +0 -5
  165. package/dist/vue-components.es23.js +0 -5
  166. package/dist/vue-components.es24.js +0 -4
  167. package/dist/vue-components.es25.js +0 -4
  168. package/dist/vue-components.es26.js +0 -4
  169. package/dist/vue-components.es27.js +0 -4
  170. package/dist/vue-components.es28.js +0 -19
  171. package/dist/vue-components.es29.js +0 -13
  172. package/dist/vue-components.es3.js +0 -17
  173. package/dist/vue-components.es30.js +0 -31
  174. package/dist/vue-components.es31.js +0 -6
  175. package/dist/vue-components.es32.js +0 -4
  176. package/dist/vue-components.es33.js +0 -4
  177. package/dist/vue-components.es34.js +0 -113
  178. package/dist/vue-components.es36.js +0 -9
  179. package/dist/vue-components.es37.js +0 -34
  180. package/dist/vue-components.es39.js +0 -194
  181. package/dist/vue-components.es4.js +0 -52
  182. package/dist/vue-components.es41.js +0 -6
  183. package/dist/vue-components.es42.js +0 -25
  184. package/dist/vue-components.es43.js +0 -7
  185. package/dist/vue-components.es44.js +0 -23
  186. package/dist/vue-components.es45.js +0 -32
  187. package/dist/vue-components.es46.js +0 -24
  188. package/dist/vue-components.es47.js +0 -14
  189. package/dist/vue-components.es48.js +0 -7
  190. package/dist/vue-components.es49.js +0 -21
  191. package/dist/vue-components.es5.js +0 -52
  192. package/dist/vue-components.es50.js +0 -11
  193. package/dist/vue-components.es51.js +0 -33
  194. package/dist/vue-components.es52.js +0 -50
  195. package/dist/vue-components.es53.js +0 -28
  196. package/dist/vue-components.es54.js +0 -13
  197. package/dist/vue-components.es55.js +0 -67
  198. package/dist/vue-components.es56.js +0 -58
  199. package/dist/vue-components.es57.js +0 -19
  200. package/dist/vue-components.es58.js +0 -35
  201. package/dist/vue-components.es59.js +0 -31
  202. package/dist/vue-components.es6.js +0 -69
  203. package/dist/vue-components.es60.js +0 -44
  204. package/dist/vue-components.es61.js +0 -4
  205. package/dist/vue-components.es62.js +0 -46
  206. package/dist/vue-components.es63.js +0 -4
  207. package/dist/vue-components.es7.js +0 -83
  208. package/dist/vue-components.es8.js +0 -63
  209. package/dist/vue-components.es9.js +0 -21
  210. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1276
  211. package/src/constants/index.ts +0 -1
@@ -0,0 +1,263 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import * as Effect from "effect-app/Effect"
3
+ import * as Option from "effect-app/Option"
4
+ import * as S from "effect-app/Schema"
5
+ import { isNullableOrUndefined, unwrapDeclaration } from "./createMeta"
6
+
7
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
8
+ if (!link?.transformation?.decode?.run) return undefined
9
+ try {
10
+ // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- vue-tsc needs the decoded value narrowed at this boundary
11
+ const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
12
+ return Option.isSome(result) ? result.value : undefined
13
+ } catch {
14
+ return undefined
15
+ }
16
+ }
17
+
18
+ const getDefaultFromAst = (property: S.AST.AST) => {
19
+ // 1. Check withConstructorDefault (context.constructorDefault is a single Link since beta.107;
20
+ // previously context.defaultValue was an Encoding tuple of Links)
21
+ const constructorLink = property.context?.constructorDefault
22
+ const constructorDefault = extractDefaultFromLink(constructorLink)
23
+ if (constructorDefault !== undefined) return constructorDefault
24
+
25
+ // 2. Check withDecodingDefault (stored in encoding)
26
+ const encodingLink = property.encoding?.[0]
27
+ if (encodingLink && property.context?.isOptional) {
28
+ return extractDefaultFromLink(encodingLink)
29
+ }
30
+
31
+ return undefined
32
+ }
33
+
34
+ type SchemaWithMembers = {
35
+ members: readonly S.Schema<any>[]
36
+ }
37
+
38
+ type UnknownRecord = Record<string, unknown>
39
+
40
+ function hasMembers(schema: any): schema is SchemaWithMembers {
41
+ return schema && "members" in schema && Array.isArray(schema.members)
42
+ }
43
+
44
+ const isRecord = (value: unknown): value is UnknownRecord =>
45
+ value !== null && typeof value === "object" && !Array.isArray(value)
46
+
47
+ const isNullishAst = (ast: S.AST.AST) => S.AST.isNull(ast) || S.AST.isUndefined(ast)
48
+
49
+ const unionMembers = (ast: S.AST.AST): readonly S.AST.AST[] => {
50
+ const resolved = unwrapDeclaration(ast)
51
+ return S.AST.isUnion(resolved)
52
+ ? resolved.types.flatMap(unionMembers)
53
+ : [resolved]
54
+ }
55
+
56
+ const literalValue = (ast: S.AST.AST): unknown => {
57
+ const resolved = unwrapDeclaration(ast)
58
+ if (S.AST.isLiteral(resolved)) return resolved.literal
59
+ if (S.AST.isUnion(resolved) && resolved.types.length === 1) {
60
+ return literalValue(resolved.types[0])
61
+ }
62
+ }
63
+
64
+ const findTaggedObjectMember = (
65
+ members: readonly S.AST.Objects[],
66
+ value: unknown
67
+ ): S.AST.Objects | undefined => {
68
+ if (!isRecord(value) || value._tag === undefined) return undefined
69
+
70
+ return members.find((member) => {
71
+ const tagProp = member.propertySignatures.find((prop) => prop.name.toString() === "_tag")
72
+ return tagProp ? literalValue(tagProp.type) === value._tag : false
73
+ })
74
+ }
75
+
76
+ // Internal implementation with WeakSet tracking
77
+ export const defaultsValueFromSchema = (
78
+ schema: S.Schema<any>,
79
+ record: Record<string, any> = {}
80
+ ): any => {
81
+ const ast = schema.ast
82
+ const defaultValue = getDefaultFromAst(ast)
83
+
84
+ if (defaultValue !== undefined) {
85
+ return defaultValue
86
+ }
87
+
88
+ if (isNullableOrUndefined(schema.ast) === "null") {
89
+ return null
90
+ }
91
+ if (isNullableOrUndefined(schema.ast) === "undefined") {
92
+ return undefined
93
+ }
94
+
95
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, Class, etc.)
96
+ const objectsAst = S.AST.isObjects(ast)
97
+ ? ast
98
+ : S.AST.isDeclaration(ast)
99
+ ? unwrapDeclaration(ast)
100
+ : undefined
101
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
102
+ const result: Record<string, any> = {}
103
+
104
+ for (const prop of objectsAst.propertySignatures) {
105
+ const key = prop.name.toString()
106
+ const propType = prop.type
107
+
108
+ const propDefault = getDefaultFromAst(propType)
109
+ if (propDefault !== undefined) {
110
+ result[key] = propDefault
111
+ continue
112
+ }
113
+
114
+ const propSchema = S.make<any>(propType)
115
+ const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
116
+
117
+ if (propValue !== undefined) {
118
+ result[key] = propValue
119
+ } else if (isNullableOrUndefined(propType) === "undefined") {
120
+ result[key] = undefined
121
+ }
122
+ }
123
+
124
+ return { ...result, ...record }
125
+ }
126
+
127
+ // Handle unions via AST or schema-level .members
128
+ const unionTypes = S.AST.isUnion(ast)
129
+ ? ast.types
130
+ : hasMembers(schema)
131
+ ? schema.members.map((m) => m.ast)
132
+ : undefined
133
+ if (unionTypes) {
134
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
135
+
136
+ for (const memberAstRaw of unionTypes) {
137
+ const memberAst = unwrapDeclaration(memberAstRaw)
138
+ if (!S.AST.isObjects(memberAst)) continue
139
+
140
+ for (const prop of memberAst.propertySignatures) {
141
+ const key = prop.name.toString()
142
+ const fieldDefault = getDefaultFromAst(prop.type)
143
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key].ast) : undefined
144
+
145
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
146
+ mergedFields[key] = { ast: prop.type }
147
+ }
148
+ }
149
+ }
150
+
151
+ if (Object.keys(mergedFields).length === 0) {
152
+ return Object.keys(record).length > 0 ? record : undefined
153
+ }
154
+
155
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
156
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
157
+ return acc
158
+ }, record)
159
+ }
160
+
161
+ if (Object.keys(record).length === 0) {
162
+ if (S.AST.isString(ast)) {
163
+ return ""
164
+ }
165
+
166
+ if (S.AST.isBoolean(ast)) {
167
+ return false
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Deep-fills a partial form value with schema defaults for any nullable
174
+ * struct that has **materialised**.
175
+ *
176
+ * A nullable struct (`S.NullOr(S.Struct(...))`) left `null` stays `null`, but
177
+ * once it materialises — e.g. the user filled a single child — its untouched
178
+ * children are filled with their schema default (or `null` when nullable).
179
+ * Without this, strict `S.NullOr(...)` children reject the leftover
180
+ * `undefined` with a spurious "field must not be empty" error.
181
+ *
182
+ * Only children of a struct reached *through a nullable union* are filled;
183
+ * the always-present root struct keeps whatever fields it already has, so the
184
+ * form's own default-value priority is left untouched.
185
+ *
186
+ * Reference-preserving: returns `value` unchanged (same reference) when there
187
+ * is nothing to fill, so callers can detect a no-op with `===` and the result
188
+ * is idempotent (`fill(fill(v)) === fill(v)`).
189
+ */
190
+ export const fillNestedDefaults = (
191
+ ast: S.AST.AST,
192
+ value: unknown,
193
+ fillMissing = false
194
+ ): unknown => {
195
+ const resolved = unwrapDeclaration(ast)
196
+
197
+ switch (resolved._tag) {
198
+ case "Union": {
199
+ if (value === null || value === undefined) return value
200
+ const members = unionMembers(resolved)
201
+ const objectMembers = members.filter(S.AST.isObjects)
202
+ if (objectMembers.length === 0) return value
203
+
204
+ const hasNullishMember = members.some(isNullishAst)
205
+ const taggedMember = findTaggedObjectMember(objectMembers, value)
206
+ if (taggedMember) {
207
+ return fillNestedDefaults(taggedMember, value, fillMissing || hasNullishMember)
208
+ }
209
+
210
+ if (hasNullishMember && objectMembers.length === 1) {
211
+ return fillNestedDefaults(objectMembers[0], value, true)
212
+ }
213
+
214
+ return value
215
+ }
216
+
217
+ case "Arrays": {
218
+ if (!Array.isArray(value)) return value
219
+ const element = resolved.rest[0]
220
+ if (!element) return value
221
+ let changed = false
222
+ const next = value.map((item) => {
223
+ const filled = fillNestedDefaults(element, item, fillMissing)
224
+ if (filled !== item) changed = true
225
+ return filled
226
+ })
227
+ return changed ? next : value
228
+ }
229
+
230
+ case "Objects": {
231
+ if (!isRecord(value)) return value
232
+ let result: UnknownRecord = value
233
+ let changed = false
234
+ const set = (key: string, next: unknown) => {
235
+ if (!changed) {
236
+ result = { ...value }
237
+ changed = true
238
+ }
239
+ result[key] = next
240
+ }
241
+ for (const prop of resolved.propertySignatures) {
242
+ const key = prop.name.toString()
243
+ const current = value[key]
244
+ if (current !== undefined) {
245
+ const filled = fillNestedDefaults(prop.type, current, fillMissing)
246
+ if (filled !== current) set(key, filled)
247
+ continue
248
+ }
249
+ if (!fillMissing || prop.type.context?.isOptional === true) continue
250
+ const propDefault = getDefaultFromAst(prop.type)
251
+ if (propDefault !== undefined) {
252
+ set(key, propDefault)
253
+ } else if (isNullableOrUndefined(prop.type) === "null") {
254
+ set(key, null)
255
+ }
256
+ }
257
+ return result
258
+ }
259
+
260
+ default:
261
+ return value
262
+ }
263
+ }
@@ -0,0 +1,71 @@
1
+ import * as S from "effect-app/Schema"
2
+
3
+ /*
4
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
5
+ * These need to be swapped to S.RedactedFromValue for form usage
6
+ * because S.Redacted expects Redacted objects, not plain strings.
7
+ *
8
+ * Detection uses `annotations.representation.id` (beta.107+); the older
9
+ * `typeConstructor._tag === "effect/Redacted"` path is kept as a fallback.
10
+ */
11
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean => {
12
+ if (!S.AST.isDeclaration(ast) || ast.encoding) return false
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Effect Schema AST annotations are loosely typed
14
+ const annotations = ast.annotations as any
15
+ return annotations?.representation?.id === "effect/schema/Redacted"
16
+ || annotations?.typeConstructor?._tag === "effect/Redacted"
17
+ }
18
+
19
+ /*
20
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
21
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
22
+ * Redacted<A> on both encoded and type sides, so form inputs (which
23
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
24
+ * plain values on the encoded side and wraps them in Redacted on decode.
25
+ */
26
+ export const toFormSchema = <From, To>(
27
+ schema: S.Codec<To, From>
28
+ ): S.Codec<To, From> => {
29
+ const ast = schema.ast
30
+ const objAst = S.AST.isObjects(ast)
31
+ ? ast
32
+ : S.AST.isDeclaration(ast)
33
+ ? S.AST.toEncoded(ast)
34
+ : null
35
+
36
+ if (!objAst || !("propertySignatures" in objAst)) return schema
37
+
38
+ let hasRedacted = false
39
+ const props: Record<string, S.Struct.Fields[string]> = {}
40
+
41
+ for (const p of objAst.propertySignatures) {
42
+ if (isRedactedWithoutEncoding(p.type)) {
43
+ hasRedacted = true
44
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0])
45
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
46
+ } else if (S.AST.isUnion(p.type)) {
47
+ const types = p.type.types
48
+ const redactedType = types.find(isRedactedWithoutEncoding)
49
+ if (redactedType) {
50
+ hasRedacted = true
51
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0])
52
+ const hasNull = types.some(S.AST.isNull)
53
+ const hasUndefined = types.some(S.AST.isUndefined)
54
+ const base = S.RedactedFromValue(innerSchema)
55
+ props[p.name as string] = hasNull && hasUndefined
56
+ ? S.NullishOr(base)
57
+ : hasNull
58
+ ? S.NullOr(base)
59
+ : hasUndefined
60
+ ? S.UndefinedOr(base)
61
+ : base
62
+ } else {
63
+ props[p.name as string] = S.make(p.type)
64
+ }
65
+ } else {
66
+ props[p.name as string] = S.make(p.type)
67
+ }
68
+ }
69
+
70
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From> : schema
71
+ }
@@ -0,0 +1,78 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { DeepKeys } from "@tanstack/vue-form"
3
+ import type * as S from "effect-app/Schema"
4
+ import type { Redacted } from "effect/Redacted"
5
+
6
+ // Recursively replace Redacted<A> with its inner type so DeepKeys treats it as a leaf
7
+ type StripRedacted<T> = T extends Redacted<any> ? string
8
+ : T extends ReadonlyArray<infer U> ? ReadonlyArray<StripRedacted<U>>
9
+ : T extends Record<string, any> ? { [K in keyof T]: StripRedacted<T[K]> }
10
+ : T
11
+
12
+ export type NestedKeyOf<T> = DeepKeys<StripRedacted<T>>
13
+
14
+ // Field metadata type definitions
15
+ export type BaseFieldMeta = {
16
+ required: boolean
17
+ nullableOrUndefined?: false | "undefined" | "null"
18
+ /**
19
+ * True when the schema property is `S.optionalKey` (AST
20
+ * `context.isOptional`) — i.e. the key should be ABSENT from the submitted
21
+ * object when empty, not present with `undefined`. Distinct from
22
+ * `required: false`, which may also mean "empty string is valid" for
23
+ * unconstrained `S.String` fields.
24
+ */
25
+ isOptionalKey?: boolean
26
+ }
27
+
28
+ export type StringFieldMeta = BaseFieldMeta & {
29
+ type: "string"
30
+ maxLength?: number
31
+ minLength?: number
32
+ format?: string
33
+ }
34
+
35
+ export type NumberFieldMeta = BaseFieldMeta & {
36
+ type: "number"
37
+ minimum?: number
38
+ maximum?: number
39
+ exclusiveMinimum?: number
40
+ exclusiveMaximum?: number
41
+ refinement?: "int"
42
+ }
43
+
44
+ export type SelectFieldMeta = BaseFieldMeta & {
45
+ type: "select"
46
+ members: any[] // TODO: should be non empty array?
47
+ }
48
+
49
+ export type MultipleFieldMeta = BaseFieldMeta & {
50
+ type: "multiple"
51
+ members: any[] // TODO: should be non empty array?
52
+ rest: readonly S.AST.AST[]
53
+ }
54
+
55
+ export type BooleanFieldMeta = BaseFieldMeta & {
56
+ type: "boolean"
57
+ }
58
+
59
+ export type DateFieldMeta = BaseFieldMeta & {
60
+ type: "date"
61
+ }
62
+
63
+ export type UnknownFieldMeta = BaseFieldMeta & {
64
+ type: "unknown"
65
+ }
66
+
67
+ export type FieldMeta =
68
+ | StringFieldMeta
69
+ | NumberFieldMeta
70
+ | SelectFieldMeta
71
+ | MultipleFieldMeta
72
+ | BooleanFieldMeta
73
+ | DateFieldMeta
74
+ | UnknownFieldMeta
75
+
76
+ export type MetaRecord<T = string> = {
77
+ [K in NestedKeyOf<T>]?: FieldMeta
78
+ }
@@ -0,0 +1,248 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- AST walker interops with Effect Schema generics */
2
+ /* eslint-disable @typescript-eslint/no-use-before-define -- mutual recursion between walk and helpers (handleStruct/handleUnion/etc.) */
3
+ import * as S from "effect-app/Schema"
4
+ import { getFieldMetadataFromAst } from "./checks"
5
+ import { isNullableOrUndefined, unwrapDeclaration } from "./createMeta"
6
+ import type { FieldMeta, MetaRecord, NestedKeyOf, SelectFieldMeta } from "./types"
7
+
8
+ const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
9
+
10
+ // TODO: remove after manual _tag deprecation — S.Struct({ _tag: S.Literal("X") }) wraps as Union([Literal("X")])
11
+ const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
12
+ S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0])
13
+ ? ast.types[0]
14
+ : ast
15
+
16
+ const unwrapNestedUnions = (types: readonly S.AST.AST[]): readonly S.AST.AST[] =>
17
+ types.flatMap((type) => S.AST.isUnion(type) ? unwrapNestedUnions(type.types) : [type])
18
+
19
+ export type WalkerContext<T> = {
20
+ acc: Partial<MetaRecord<T>>
21
+ unionMeta: Record<string, MetaRecord<T>>
22
+ }
23
+
24
+ export type ParentMeta = {
25
+ required: boolean
26
+ nullableOrUndefined: false | "null" | "undefined"
27
+ /** Set when iterating the members of a nullable discriminated union */
28
+ isNullableDiscriminatedUnion?: boolean
29
+ /** Set when this property was declared with S.optionalKey */
30
+ isOptionalKey?: boolean
31
+ }
32
+
33
+ export const leafMetaForAst = (
34
+ ast: S.AST.AST,
35
+ parentMeta: ParentMeta
36
+ ): FieldMeta => {
37
+ const { nullableOrUndefined, required } = parentMeta
38
+
39
+ if (S.AST.isArrays(ast)) {
40
+ return {
41
+ required,
42
+ nullableOrUndefined,
43
+ type: "multiple",
44
+ members: ast.elements,
45
+ rest: ast.rest
46
+ } as FieldMeta
47
+ }
48
+
49
+ if (S.AST.isLiteral(ast)) {
50
+ return {
51
+ required,
52
+ nullableOrUndefined,
53
+ type: "select",
54
+ members: [ast.literal]
55
+ }
56
+ }
57
+
58
+ return {
59
+ ...getFieldMetadataFromAst(ast),
60
+ required,
61
+ nullableOrUndefined
62
+ } as FieldMeta
63
+ }
64
+
65
+ export const walkStruct = <T>(
66
+ propertySignatures: readonly S.AST.PropertySignature[],
67
+ parent: string,
68
+ parentMeta: ParentMeta,
69
+ ctx: WalkerContext<T>
70
+ ): void => {
71
+ for (const p of propertySignatures) {
72
+ const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
73
+ const nullableOrUndefined = isNullableOrUndefined(p.type)
74
+ const isOptionalKey = (p.type as any).context?.isOptional === true
75
+
76
+ let isRequired: boolean
77
+ if (parentMeta.isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
78
+ isRequired = false
79
+ } else if (!parentMeta.required) {
80
+ isRequired = false
81
+ } else if (isOptionalKey) {
82
+ isRequired = false
83
+ } else {
84
+ isRequired = !nullableOrUndefined
85
+ }
86
+
87
+ walk(
88
+ p.type,
89
+ key,
90
+ { required: isRequired, nullableOrUndefined, isOptionalKey },
91
+ ctx
92
+ )
93
+ }
94
+ }
95
+
96
+ export const classifyAndWalkUnion = <T>(
97
+ unionAst: S.AST.Union,
98
+ key: string,
99
+ parentMeta: ParentMeta,
100
+ ctx: WalkerContext<T>
101
+ ): void => {
102
+ const { acc } = ctx
103
+ const unwrappedTypes = unwrapNestedUnions(unionAst.types).map(unwrapDeclaration)
104
+ const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
105
+
106
+ // Boolean literal shortcut (single-value union wrapping a boolean literal)
107
+ if (nonNullTypes.length === 1 && S.AST.isLiteral(nonNullTypes[0]) && typeof nonNullTypes[0].literal === "boolean") {
108
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(nonNullTypes[0], parentMeta)
109
+ return
110
+ }
111
+
112
+ if (nonNullTypes.some(S.AST.isObjects)) {
113
+ const isNullableDiscriminatedUnion = !!parentMeta.nullableOrUndefined && nonNullTypes.length > 1
114
+
115
+ // Mixed union: also create a parent leaf entry from the first non-struct member
116
+ if (!parentMeta.nullableOrUndefined && key) {
117
+ const firstNonStruct = nonNullTypes.find((t) => !S.AST.isObjects(t))
118
+ if (firstNonStruct) {
119
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(firstNonStruct, parentMeta)
120
+ }
121
+ }
122
+
123
+ const discriminatorValues: any[] = []
124
+ const branchParentMeta: ParentMeta = isNullableDiscriminatedUnion
125
+ ? { required: true, nullableOrUndefined: false, isNullableDiscriminatedUnion: true }
126
+ : { required: true, nullableOrUndefined: false }
127
+
128
+ for (const memberType of nonNullTypes) {
129
+ if (!S.AST.isObjects(memberType)) continue
130
+
131
+ const tagProp = memberType.propertySignatures.find((p) => p.name.toString() === "_tag")
132
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
133
+ let tagValue: string | null = null
134
+
135
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
136
+ tagValue = resolvedTagType.literal as string
137
+ if (!discriminatorValues.includes(tagValue)) discriminatorValues.push(tagValue)
138
+ }
139
+
140
+ const branchCtx: WalkerContext<T> = { acc: {}, unionMeta: ctx.unionMeta }
141
+ walkStruct(memberType.propertySignatures, key, branchParentMeta, branchCtx)
142
+
143
+ if (tagValue) {
144
+ const existing = ctx.unionMeta[tagValue]
145
+ if (existing) Object.assign(existing, branchCtx.acc as MetaRecord<T>)
146
+ else ctx.unionMeta[tagValue] = branchCtx.acc
147
+ }
148
+
149
+ for (const [metaKey, metaValue] of Object.entries(branchCtx.acc)) {
150
+ const existing = acc[metaKey as NestedKeyOf<T>]
151
+ if (existing && existing.type === "select" && (metaValue as any)?.type === "select") {
152
+ existing.members = [
153
+ ...existing.members,
154
+ ...(metaValue as SelectFieldMeta).members.filter((m: any) => !existing.members.includes(m))
155
+ ]
156
+ } else {
157
+ acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
158
+ }
159
+ }
160
+ }
161
+
162
+ if (discriminatorValues.length > 0) {
163
+ const tagKey = key ? `${key}._tag` : "_tag"
164
+ const existing = acc[tagKey as NestedKeyOf<T>]
165
+ if (existing && existing.type === "select") {
166
+ for (const v of discriminatorValues) {
167
+ if (!existing.members.includes(v)) existing.members.push(v)
168
+ }
169
+ } else {
170
+ acc[tagKey as NestedKeyOf<T>] = {
171
+ type: "select",
172
+ members: discriminatorValues,
173
+ required: !isNullableDiscriminatedUnion
174
+ }
175
+ }
176
+ }
177
+ return
178
+ }
179
+
180
+ const ast = nonNullTypes.find(S.AST.isArrays)
181
+ if (ast) {
182
+ walk(ast, key, parentMeta, ctx)
183
+ return
184
+ }
185
+
186
+ // Literal / primitive union (e.g. legacy _tag pattern)
187
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
188
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
189
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
190
+ const leaf: FieldMeta = {
191
+ required,
192
+ nullableOrUndefined,
193
+ type: "select",
194
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
195
+ }
196
+ if (isOptionalKey) leaf.isOptionalKey = true
197
+ acc[key as NestedKeyOf<T>] = leaf
198
+ return
199
+ }
200
+
201
+ // Fallback: recurse into first non-null type
202
+ const nonNullType = nonNullTypes[0]
203
+ if (nonNullType) walk(nonNullType, key, parentMeta, ctx)
204
+ }
205
+
206
+ export const walk = <T>(
207
+ ast: S.AST.AST,
208
+ key: string,
209
+ parentMeta: ParentMeta,
210
+ ctx: WalkerContext<T>
211
+ ): void => {
212
+ ast = unwrapDeclaration(ast)
213
+ const { acc } = ctx
214
+
215
+ if (S.AST.isObjects(ast)) {
216
+ walkStruct(ast.propertySignatures, key, parentMeta, ctx)
217
+ return
218
+ }
219
+
220
+ if (S.AST.isUnion(ast)) {
221
+ classifyAndWalkUnion(ast, key, parentMeta, ctx)
222
+ return
223
+ }
224
+
225
+ if (S.AST.isArrays(ast)) {
226
+ const restElement = ast.rest.length > 0 ? unwrapDeclaration(ast.rest[0]) : null
227
+ if (restElement && S.AST.isObjects(restElement)) {
228
+ // Array-of-struct: skip creating a meta entry for the array itself,
229
+ // recurse into the element struct's properties instead
230
+ walkStruct(restElement.propertySignatures, key, { required: true, nullableOrUndefined: false }, ctx)
231
+ return
232
+ }
233
+
234
+ // Primitive or tuple array
235
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(ast, parentMeta)
236
+ return
237
+ }
238
+
239
+ // Leaf primitive / literal / unknown
240
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
241
+ const adjusted: ParentMeta = {
242
+ required: required && (!S.AST.isString(ast) || !!getFieldMetadataFromAst(ast).minLength),
243
+ nullableOrUndefined
244
+ }
245
+ const leaf = leafMetaForAst(ast, adjusted)
246
+ if (isOptionalKey) leaf.isOptionalKey = true
247
+ acc[key as NestedKeyOf<T>] = leaf
248
+ }