@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.
- package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
- package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
- package/dist/types/components/OmegaForm/errors.d.ts +33 -0
- package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
- package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
- package/dist/types/components/OmegaForm/index.d.ts +13 -3
- package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
- package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
- package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
- package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
- package/dist/types/components/OmegaForm/submit.d.ts +60 -0
- package/dist/types/components/OmegaForm/types.d.ts +281 -0
- package/dist/types/components/OmegaForm/useOmegaForm.d.ts +6 -212
- package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
- package/dist/vue-components.es.js +24 -16
- package/dist/vue-components10.es.js +4 -4
- package/dist/vue-components11.es.js +19 -12
- package/dist/vue-components12.es.js +22 -444
- package/dist/vue-components13.es.js +126 -3
- package/dist/vue-components14.es.js +61 -34
- package/dist/vue-components15.es.js +57 -24
- package/dist/vue-components16.es.js +20 -26
- package/dist/vue-components17.es.js +4 -6
- package/dist/vue-components18.es.js +78 -16
- package/dist/vue-components19.es.js +86 -30
- package/dist/vue-components20.es.js +72 -17
- package/dist/vue-components21.es.js +10 -19
- package/dist/vue-components22.es.js +54 -28
- package/dist/vue-components23.es.js +4 -6
- package/dist/vue-components24.es.js +43 -8
- package/dist/vue-components25.es.js +4 -37
- package/dist/vue-components26.es.js +83 -24
- package/dist/vue-components28.es.js +6 -22
- package/dist/vue-components29.es.js +8 -20
- package/dist/vue-components3.es.js +2 -2
- package/dist/vue-components30.es.js +267 -7
- package/dist/vue-components32.es.js +7 -4
- package/dist/vue-components33.es.js +71 -27
- package/dist/vue-components34.es.js +4 -4
- package/dist/vue-components35.es.js +50 -27
- package/dist/vue-components36.es.js +4 -5
- package/dist/vue-components37.es.js +23 -17
- package/dist/vue-components38.es.js +4 -55
- package/dist/vue-components39.es.js +57 -3
- package/dist/vue-components40.es.js +4 -43
- package/dist/vue-components41.es.js +11 -4
- package/dist/vue-components42.es.js +17 -79
- package/dist/vue-components44.es.js +8 -7
- package/dist/vue-components45.es.js +3 -8
- package/dist/vue-components46.es.js +36 -267
- package/dist/vue-components47.es.js +27 -0
- package/dist/vue-components48.es.js +27 -7
- package/dist/vue-components49.es.js +6 -79
- package/dist/vue-components50.es.js +17 -4
- package/dist/vue-components51.es.js +32 -69
- package/dist/vue-components52.es.js +17 -4
- package/dist/vue-components53.es.js +19 -22
- package/dist/vue-components54.es.js +29 -4
- package/dist/vue-components55.es.js +6 -58
- package/dist/vue-components56.es.js +8 -4
- package/dist/vue-components57.es.js +37 -11
- package/dist/vue-components58.es.js +24 -21
- package/dist/{vue-components27.es.js → vue-components59.es.js} +2 -2
- package/dist/vue-components6.es.js +11 -11
- package/dist/vue-components60.es.js +23 -8
- package/dist/vue-components61.es.js +18 -232
- package/dist/vue-components62.es.js +7 -31
- package/dist/vue-components63.es.js +19 -8
- package/dist/vue-components64.es.js +4 -35
- package/dist/vue-components65.es.js +29 -0
- package/dist/vue-components66.es.js +5 -0
- package/dist/vue-components67.es.js +29 -0
- package/dist/vue-components68.es.js +6 -0
- package/dist/vue-components69.es.js +18 -0
- package/dist/vue-components7.es.js +11 -26
- package/dist/vue-components70.es.js +40 -0
- package/dist/vue-components71.es.js +81 -0
- package/dist/vue-components72.es.js +33 -0
- package/dist/vue-components73.es.js +19 -0
- package/dist/vue-components74.es.js +48 -0
- package/dist/vue-components8.es.js +33 -45
- package/dist/vue-components9.es.js +46 -4
- package/package.json +7 -7
- package/src/components/CommandButton.vue +3 -1
- package/src/components/OmegaForm/OmegaArray.vue +1 -1
- package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
- package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
- package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
- package/src/components/OmegaForm/OmegaInput.vue +6 -68
- package/src/components/OmegaForm/OmegaInputVuetify.vue +1 -1
- package/src/components/OmegaForm/OmegaInternalInput.vue +5 -11
- package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
- package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
- package/src/components/OmegaForm/blockDialog.ts +10 -1
- package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
- package/src/components/OmegaForm/errors.ts +136 -0
- package/src/components/OmegaForm/getOmegaStore.ts +1 -1
- package/src/components/OmegaForm/hocs.ts +19 -0
- package/src/components/OmegaForm/index.ts +16 -4
- package/src/components/OmegaForm/inputs.ts +22 -0
- package/src/components/OmegaForm/meta/checks.ts +81 -0
- package/src/components/OmegaForm/meta/createMeta.ts +138 -0
- package/src/components/OmegaForm/meta/defaults.ts +132 -0
- package/src/components/OmegaForm/meta/redacted.ts +66 -0
- package/src/components/OmegaForm/meta/types.ts +78 -0
- package/src/components/OmegaForm/meta/walker.ts +247 -0
- package/src/components/OmegaForm/persistency.ts +247 -0
- package/src/components/OmegaForm/submit.ts +128 -0
- package/src/components/OmegaForm/types.ts +751 -0
- package/src/components/OmegaForm/useOmegaForm.ts +49 -913
- package/src/components/OmegaForm/validation/localized.ts +202 -0
- package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -173
- package/dist/vue-components31.es.js +0 -19
- 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
|
+
}
|