@effect-app/vue-components 4.0.0-beta.158 → 4.0.0-beta.159

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 (124) hide show
  1. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  2. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  3. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
  4. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  5. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  6. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
  7. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
  9. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  10. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  12. package/dist/types/components/OmegaForm/index.d.ts +13 -3
  13. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  14. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  15. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
  16. package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
  17. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  18. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  19. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  20. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  21. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  22. package/dist/types/components/OmegaForm/types.d.ts +281 -0
  23. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +6 -212
  24. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  25. package/dist/vue-components.es.js +24 -16
  26. package/dist/vue-components10.es.js +4 -4
  27. package/dist/vue-components11.es.js +19 -12
  28. package/dist/vue-components12.es.js +22 -444
  29. package/dist/vue-components13.es.js +126 -3
  30. package/dist/vue-components14.es.js +61 -34
  31. package/dist/vue-components15.es.js +57 -24
  32. package/dist/vue-components16.es.js +20 -26
  33. package/dist/vue-components17.es.js +4 -6
  34. package/dist/vue-components18.es.js +78 -16
  35. package/dist/vue-components19.es.js +86 -30
  36. package/dist/vue-components20.es.js +72 -17
  37. package/dist/vue-components21.es.js +10 -19
  38. package/dist/vue-components22.es.js +54 -28
  39. package/dist/vue-components23.es.js +4 -6
  40. package/dist/vue-components24.es.js +43 -8
  41. package/dist/vue-components25.es.js +4 -37
  42. package/dist/vue-components26.es.js +83 -24
  43. package/dist/vue-components28.es.js +6 -22
  44. package/dist/vue-components29.es.js +8 -20
  45. package/dist/vue-components3.es.js +2 -2
  46. package/dist/vue-components30.es.js +267 -7
  47. package/dist/vue-components32.es.js +7 -4
  48. package/dist/vue-components33.es.js +71 -27
  49. package/dist/vue-components34.es.js +4 -4
  50. package/dist/vue-components35.es.js +50 -27
  51. package/dist/vue-components36.es.js +4 -5
  52. package/dist/vue-components37.es.js +23 -17
  53. package/dist/vue-components38.es.js +4 -55
  54. package/dist/vue-components39.es.js +57 -3
  55. package/dist/vue-components40.es.js +4 -43
  56. package/dist/vue-components41.es.js +11 -4
  57. package/dist/vue-components42.es.js +17 -79
  58. package/dist/vue-components44.es.js +8 -7
  59. package/dist/vue-components45.es.js +3 -8
  60. package/dist/vue-components46.es.js +36 -267
  61. package/dist/vue-components47.es.js +27 -0
  62. package/dist/vue-components48.es.js +27 -7
  63. package/dist/vue-components49.es.js +6 -79
  64. package/dist/vue-components50.es.js +17 -4
  65. package/dist/vue-components51.es.js +32 -69
  66. package/dist/vue-components52.es.js +17 -4
  67. package/dist/vue-components53.es.js +19 -22
  68. package/dist/vue-components54.es.js +29 -4
  69. package/dist/vue-components55.es.js +6 -58
  70. package/dist/vue-components56.es.js +8 -4
  71. package/dist/vue-components57.es.js +37 -11
  72. package/dist/vue-components58.es.js +24 -21
  73. package/dist/{vue-components27.es.js → vue-components59.es.js} +2 -2
  74. package/dist/vue-components6.es.js +11 -11
  75. package/dist/vue-components60.es.js +23 -8
  76. package/dist/vue-components61.es.js +18 -232
  77. package/dist/vue-components62.es.js +7 -31
  78. package/dist/vue-components63.es.js +19 -8
  79. package/dist/vue-components64.es.js +4 -35
  80. package/dist/vue-components65.es.js +29 -0
  81. package/dist/vue-components66.es.js +5 -0
  82. package/dist/vue-components67.es.js +29 -0
  83. package/dist/vue-components68.es.js +6 -0
  84. package/dist/vue-components69.es.js +18 -0
  85. package/dist/vue-components7.es.js +11 -26
  86. package/dist/vue-components70.es.js +40 -0
  87. package/dist/vue-components71.es.js +81 -0
  88. package/dist/vue-components72.es.js +33 -0
  89. package/dist/vue-components73.es.js +19 -0
  90. package/dist/vue-components74.es.js +48 -0
  91. package/dist/vue-components8.es.js +33 -45
  92. package/dist/vue-components9.es.js +46 -4
  93. package/package.json +7 -7
  94. package/src/components/CommandButton.vue +3 -1
  95. package/src/components/OmegaForm/OmegaArray.vue +1 -1
  96. package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
  97. package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
  98. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  99. package/src/components/OmegaForm/OmegaInput.vue +6 -68
  100. package/src/components/OmegaForm/OmegaInputVuetify.vue +1 -1
  101. package/src/components/OmegaForm/OmegaInternalInput.vue +5 -11
  102. package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
  103. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  104. package/src/components/OmegaForm/blockDialog.ts +10 -1
  105. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
  106. package/src/components/OmegaForm/errors.ts +136 -0
  107. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  108. package/src/components/OmegaForm/hocs.ts +19 -0
  109. package/src/components/OmegaForm/index.ts +16 -4
  110. package/src/components/OmegaForm/inputs.ts +22 -0
  111. package/src/components/OmegaForm/meta/checks.ts +81 -0
  112. package/src/components/OmegaForm/meta/createMeta.ts +138 -0
  113. package/src/components/OmegaForm/meta/defaults.ts +132 -0
  114. package/src/components/OmegaForm/meta/redacted.ts +66 -0
  115. package/src/components/OmegaForm/meta/types.ts +78 -0
  116. package/src/components/OmegaForm/meta/walker.ts +247 -0
  117. package/src/components/OmegaForm/persistency.ts +247 -0
  118. package/src/components/OmegaForm/submit.ts +128 -0
  119. package/src/components/OmegaForm/types.ts +751 -0
  120. package/src/components/OmegaForm/useOmegaForm.ts +49 -913
  121. package/src/components/OmegaForm/validation/localized.ts +202 -0
  122. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -173
  123. package/dist/vue-components31.es.js +0 -19
  124. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1422
@@ -0,0 +1,132 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Effect, Option, S } from "effect-app"
3
+ import { isNullableOrUndefined, unwrapDeclaration } from "./createMeta"
4
+
5
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
6
+ if (!link?.transformation?.decode?.run) return undefined
7
+ try {
8
+ const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
9
+ return Option.isSome(result) ? result.value : undefined
10
+ } catch {
11
+ return undefined
12
+ }
13
+ }
14
+
15
+ const getDefaultFromAst = (property: S.AST.AST) => {
16
+ // 1. Check withConstructorDefault (stored in context.defaultValue)
17
+ const constructorLink = property.context?.defaultValue?.[0]
18
+ const constructorDefault = extractDefaultFromLink(constructorLink)
19
+ if (constructorDefault !== undefined) return constructorDefault
20
+
21
+ // 2. Check withDecodingDefault (stored in encoding)
22
+ const encodingLink = property.encoding?.[0]
23
+ if (encodingLink && property.context?.isOptional) {
24
+ return extractDefaultFromLink(encodingLink)
25
+ }
26
+
27
+ return undefined
28
+ }
29
+
30
+ type SchemaWithMembers = {
31
+ members: readonly S.Schema<any>[]
32
+ }
33
+
34
+ function hasMembers(schema: any): schema is SchemaWithMembers {
35
+ return schema && "members" in schema && Array.isArray(schema.members)
36
+ }
37
+
38
+ // Internal implementation with WeakSet tracking
39
+ export const defaultsValueFromSchema = (
40
+ schema: S.Schema<any>,
41
+ record: Record<string, any> = {}
42
+ ): any => {
43
+ const ast = schema.ast
44
+ const defaultValue = getDefaultFromAst(ast)
45
+
46
+ if (defaultValue !== undefined) {
47
+ return defaultValue
48
+ }
49
+
50
+ if (isNullableOrUndefined(schema.ast) === "null") {
51
+ return null
52
+ }
53
+ if (isNullableOrUndefined(schema.ast) === "undefined") {
54
+ return undefined
55
+ }
56
+
57
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, Class, etc.)
58
+ const objectsAst = S.AST.isObjects(ast)
59
+ ? ast
60
+ : S.AST.isDeclaration(ast)
61
+ ? unwrapDeclaration(ast)
62
+ : undefined
63
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
64
+ const result: Record<string, any> = {}
65
+
66
+ for (const prop of objectsAst.propertySignatures) {
67
+ const key = prop.name.toString()
68
+ const propType = prop.type
69
+
70
+ const propDefault = getDefaultFromAst(propType)
71
+ if (propDefault !== undefined) {
72
+ result[key] = propDefault
73
+ continue
74
+ }
75
+
76
+ const propSchema = S.make(propType)
77
+ const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
78
+
79
+ if (propValue !== undefined) {
80
+ result[key] = propValue
81
+ } else if (isNullableOrUndefined(propType) === "undefined") {
82
+ result[key] = undefined
83
+ }
84
+ }
85
+
86
+ return { ...result, ...record }
87
+ }
88
+
89
+ // Handle unions via AST or schema-level .members
90
+ const unionTypes = S.AST.isUnion(ast)
91
+ ? ast.types
92
+ : hasMembers(schema)
93
+ ? schema.members.map((m) => m.ast)
94
+ : undefined
95
+ if (unionTypes) {
96
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
97
+
98
+ for (const memberAstRaw of unionTypes) {
99
+ const memberAst = unwrapDeclaration(memberAstRaw)
100
+ if (!S.AST.isObjects(memberAst)) continue
101
+
102
+ for (const prop of memberAst.propertySignatures) {
103
+ const key = prop.name.toString()
104
+ const fieldDefault = getDefaultFromAst(prop.type)
105
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
106
+
107
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
108
+ mergedFields[key] = { ast: prop.type }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (Object.keys(mergedFields).length === 0) {
114
+ return Object.keys(record).length > 0 ? record : undefined
115
+ }
116
+
117
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
118
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
119
+ return acc
120
+ }, record)
121
+ }
122
+
123
+ if (Object.keys(record).length === 0) {
124
+ if (S.AST.isString(ast)) {
125
+ return ""
126
+ }
127
+
128
+ if (S.AST.isBoolean(ast)) {
129
+ return false
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,66 @@
1
+ import { S } from "effect-app"
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
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
9
+ S.AST.isDeclaration(ast)
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Effect Schema AST annotations are loosely typed
11
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
12
+ && !ast.encoding
13
+
14
+ /*
15
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
16
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
17
+ * Redacted<A> on both encoded and type sides, so form inputs (which
18
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
19
+ * plain values on the encoded side and wraps them in Redacted on decode.
20
+ */
21
+ export const toFormSchema = <From, To>(
22
+ schema: S.Codec<To, From, never>
23
+ ): S.Codec<To, From, never> => {
24
+ const ast = schema.ast
25
+ const objAst = S.AST.isObjects(ast)
26
+ ? ast
27
+ : S.AST.isDeclaration(ast)
28
+ ? S.AST.toEncoded(ast)
29
+ : null
30
+
31
+ if (!objAst || !("propertySignatures" in objAst)) return schema
32
+
33
+ let hasRedacted = false
34
+ const props: Record<string, S.Struct.Fields[string]> = {}
35
+
36
+ for (const p of objAst.propertySignatures) {
37
+ if (isRedactedWithoutEncoding(p.type)) {
38
+ hasRedacted = true
39
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
40
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
41
+ } else if (S.AST.isUnion(p.type)) {
42
+ const types = p.type.types
43
+ const redactedType = types.find(isRedactedWithoutEncoding)
44
+ if (redactedType) {
45
+ hasRedacted = true
46
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
47
+ const hasNull = types.some(S.AST.isNull)
48
+ const hasUndefined = types.some(S.AST.isUndefined)
49
+ const base = S.RedactedFromValue(innerSchema)
50
+ props[p.name as string] = hasNull && hasUndefined
51
+ ? S.NullishOr(base)
52
+ : hasNull
53
+ ? S.NullOr(base)
54
+ : hasUndefined
55
+ ? S.UndefinedOr(base)
56
+ : base
57
+ } else {
58
+ props[p.name as string] = S.make(p.type)
59
+ }
60
+ } else {
61
+ props[p.name as string] = S.make(p.type)
62
+ }
63
+ }
64
+
65
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
66
+ }
@@ -0,0 +1,78 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { DeepKeys } from "@tanstack/vue-form"
3
+ import type { S } from "effect-app"
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,247 @@
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 { S } from "effect-app"
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
+ } as FieldMeta
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 === false) {
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 as MetaRecord<T>
147
+ }
148
+
149
+ for (const [metaKey, metaValue] of Object.entries(branchCtx.acc)) {
150
+ const existing = acc[metaKey as NestedKeyOf<T>] as FieldMeta | undefined
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>] as FieldMeta | undefined
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
+ } as FieldMeta
175
+ }
176
+ }
177
+ return
178
+ }
179
+
180
+ if (nonNullTypes.some(S.AST.isArrays)) {
181
+ walk(nonNullTypes.find(S.AST.isArrays)!, key, parentMeta, ctx)
182
+ return
183
+ }
184
+
185
+ // Literal / primitive union (e.g. legacy _tag pattern)
186
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
187
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
188
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
189
+ const leaf: FieldMeta = {
190
+ required,
191
+ nullableOrUndefined,
192
+ type: "select",
193
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
194
+ } as FieldMeta
195
+ if (isOptionalKey) leaf.isOptionalKey = true
196
+ acc[key as NestedKeyOf<T>] = leaf
197
+ return
198
+ }
199
+
200
+ // Fallback: recurse into first non-null type
201
+ const nonNullType = nonNullTypes[0]
202
+ if (nonNullType) walk(nonNullType, key, parentMeta, ctx)
203
+ }
204
+
205
+ export const walk = <T>(
206
+ ast: S.AST.AST,
207
+ key: string,
208
+ parentMeta: ParentMeta,
209
+ ctx: WalkerContext<T>
210
+ ): void => {
211
+ ast = unwrapDeclaration(ast)
212
+ const { acc } = ctx
213
+
214
+ if (S.AST.isObjects(ast)) {
215
+ walkStruct(ast.propertySignatures, key, parentMeta, ctx)
216
+ return
217
+ }
218
+
219
+ if (S.AST.isUnion(ast)) {
220
+ classifyAndWalkUnion(ast, key, parentMeta, ctx)
221
+ return
222
+ }
223
+
224
+ if (S.AST.isArrays(ast)) {
225
+ const restElement = ast.rest.length > 0 ? unwrapDeclaration(ast.rest[0]!) : null
226
+ if (restElement && S.AST.isObjects(restElement)) {
227
+ // Array-of-struct: skip creating a meta entry for the array itself,
228
+ // recurse into the element struct's properties instead
229
+ walkStruct(restElement.propertySignatures, key, { required: true, nullableOrUndefined: false }, ctx)
230
+ return
231
+ }
232
+
233
+ // Primitive or tuple array
234
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(ast, parentMeta)
235
+ return
236
+ }
237
+
238
+ // Leaf primitive / literal / unknown
239
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
240
+ const adjusted: ParentMeta = {
241
+ required: required && (!S.AST.isString(ast) || !!getFieldMetadataFromAst(ast).minLength),
242
+ nullableOrUndefined
243
+ }
244
+ const leaf = leafMetaForAst(ast, adjusted)
245
+ if (isOptionalKey) leaf.isOptionalKey = true
246
+ acc[key as NestedKeyOf<T>] = leaf
247
+ }