Separate schema field discovery and assembly
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Consolidation Implementation Plan
|
# Consolidation Implementation Plan
|
||||||
|
|
||||||
Status: in progress. Stages 1 through 17 are complete; follow Stage 18 through Stage 20 in order.
|
Status: in progress. Stages 1 through 18 are complete; follow Stage 19 through Stage 20 in order.
|
||||||
The target executor is a Luna coding agent working through one end-to-end sprint.
|
The target executor is a Luna coding agent working through one end-to-end sprint.
|
||||||
|
|
||||||
## Authority, Baseline, And Execution
|
## Authority, Baseline, And Execution
|
||||||
@@ -684,7 +684,7 @@ pointer/dynamic handling, and scalar conversion; struct order, map sorting,
|
|||||||
flattened-node charging, nil slices, and cycle guards remain in their original
|
flattened-node charging, nil slices, and cycle guards remain in their original
|
||||||
paths. Validation group R passed.
|
paths. Validation group R passed.
|
||||||
|
|
||||||
## Stage 18 — Separate Schema Field Discovery And Assembly
|
## Stage 18 — Separate Schema Field Discovery And Assembly (Complete)
|
||||||
|
|
||||||
**Owner:** engine `schema.go`, optionally new `schema_fields.go`.
|
**Owner:** engine `schema.go`, optionally new `schema_fields.go`.
|
||||||
|
|
||||||
@@ -701,6 +701,12 @@ paths. Validation group R passed.
|
|||||||
Do not unify these views or add alternate field/tag policy. Existing schema,
|
Do not unify these views or add alternate field/tag policy. Existing schema,
|
||||||
projection, decoding, and cloning expectations remain the behavior baseline.
|
projection, decoding, and cloning expectations remain the behavior baseline.
|
||||||
|
|
||||||
|
Progress notes (2026-10-25): kept declaration-order enumeration, exported and
|
||||||
|
ignored-field decisions, and tag parsing in `describeFields`; moved ordinary
|
||||||
|
registration, inline pointer inspection, and inline struct/map assembly into
|
||||||
|
focused helpers in `schema_fields.go`. Physical members remain distinct from
|
||||||
|
flattened fields and inline-map metadata. Validation group R passed.
|
||||||
|
|
||||||
**Finish:** field discovery and descriptor assembly have explicit responsibilities
|
**Finish:** field discovery and descriptor assembly have explicit responsibilities
|
||||||
without a new registration framework. Run R for both Stages 17–18.
|
without a new registration framework. Run R for both Stages 17–18.
|
||||||
|
|
||||||
|
|||||||
@@ -241,80 +241,15 @@ func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current refle
|
|||||||
if name != "" {
|
if name != "" {
|
||||||
return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"}
|
return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"}
|
||||||
}
|
}
|
||||||
inlineType := field.Type
|
if issue := b.describeInlineField(descriptor, field, i, secret, path); issue != nil {
|
||||||
seenPointers := make(map[reflect.Type]bool)
|
return issue
|
||||||
for inlineType.Kind() == reflect.Pointer {
|
|
||||||
if seenPointers[inlineType] {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"}
|
|
||||||
}
|
|
||||||
seenPointers[inlineType] = true
|
|
||||||
inlineType = inlineType.Elem()
|
|
||||||
}
|
|
||||||
if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"}
|
|
||||||
}
|
|
||||||
switch inlineType.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
if b.building[inlineType] {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "recursive inline struct is not supported"}
|
|
||||||
}
|
|
||||||
child, childIssue := b.describe(inlineType, path)
|
|
||||||
if childIssue != nil {
|
|
||||||
return childIssue
|
|
||||||
}
|
|
||||||
if child.kind != descriptorStruct {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a structural struct or string-keyed map"}
|
|
||||||
}
|
|
||||||
for _, nested := range child.fields {
|
|
||||||
nested.index = prependIndex(i, nested.index)
|
|
||||||
nested.secret = secret || nested.secret
|
|
||||||
if issue := addField(descriptor, nested, path); issue != nil {
|
|
||||||
return issue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if child.inlineMap != nil {
|
|
||||||
nested := *child.inlineMap
|
|
||||||
nested.index = prependIndex(i, nested.index)
|
|
||||||
nested.secret = secret || nested.secret
|
|
||||||
if descriptor.inlineMap != nil {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
|
||||||
}
|
|
||||||
descriptor.inlineMap = &nested
|
|
||||||
}
|
|
||||||
memberType, memberIssue := b.describe(field.Type, path)
|
|
||||||
if memberIssue != nil {
|
|
||||||
return memberIssue
|
|
||||||
}
|
|
||||||
descriptor.members = append(descriptor.members, structMember{index: i, descriptor: memberType, inline: true})
|
|
||||||
case reflect.Map:
|
|
||||||
if inlineType.Key().Kind() != reflect.String {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline map must have string keys"}
|
|
||||||
}
|
|
||||||
if descriptor.inlineMap != nil {
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
|
||||||
}
|
|
||||||
fieldPath := path.Key("*")
|
|
||||||
fieldType, childIssue := b.describe(inlineType, fieldPath)
|
|
||||||
if childIssue != nil {
|
|
||||||
return childIssue
|
|
||||||
}
|
|
||||||
descriptor.inlineMap = &fieldDescriptor{index: []int{i}, descriptor: fieldType, secret: secret, inlineMap: true}
|
|
||||||
descriptor.members = append(descriptor.members, structMember{index: i, descriptor: fieldType, inline: true})
|
|
||||||
default:
|
|
||||||
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a struct, pointer to struct, or string-keyed map"}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldPath := path.Key(name)
|
if issue := b.describeOrdinaryField(descriptor, field, i, name, secret, path); issue != nil {
|
||||||
fieldType, childIssue := b.describe(field.Type, fieldPath)
|
|
||||||
if childIssue != nil {
|
|
||||||
return childIssue
|
|
||||||
}
|
|
||||||
if issue := addField(descriptor, fieldDescriptor{name: name, index: []int{i}, descriptor: fieldType, secret: secret}, path); issue != nil {
|
|
||||||
return issue
|
return issue
|
||||||
}
|
}
|
||||||
descriptor.members = append(descriptor.members, structMember{index: i, name: name, descriptor: fieldType})
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
102
internal/engine/schema_fields.go
Normal file
102
internal/engine/schema_fields.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
func (b *schemaBuilder) describeOrdinaryField(owner *typeDescriptor, field reflect.StructField, index int, name string, secret bool, path Path) *schemaIssue {
|
||||||
|
fieldPath := path.Key(name)
|
||||||
|
fieldType, issue := b.describe(field.Type, fieldPath)
|
||||||
|
if issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
if issue := addField(owner, fieldDescriptor{name: name, index: []int{index}, descriptor: fieldType, secret: secret}, path); issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
owner.members = append(owner.members, structMember{index: index, name: name, descriptor: fieldType})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *schemaBuilder) describeInlineField(owner *typeDescriptor, field reflect.StructField, index int, secret bool, path Path) *schemaIssue {
|
||||||
|
inlineType, issue := inlineFieldType(field.Type, path)
|
||||||
|
if issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch inlineType.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return b.describeInlineStruct(owner, field, inlineType, index, secret, path)
|
||||||
|
case reflect.Map:
|
||||||
|
return b.describeInlineMap(owner, inlineType, index, secret, path)
|
||||||
|
default:
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a struct, pointer to struct, or string-keyed map"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inlineFieldType(fieldType reflect.Type, path Path) (reflect.Type, *schemaIssue) {
|
||||||
|
current := fieldType
|
||||||
|
seenPointers := make(map[reflect.Type]bool)
|
||||||
|
for current.Kind() == reflect.Pointer {
|
||||||
|
if seenPointers[current] {
|
||||||
|
return nil, &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"}
|
||||||
|
}
|
||||||
|
seenPointers[current] = true
|
||||||
|
current = current.Elem()
|
||||||
|
}
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *schemaBuilder) describeInlineStruct(owner *typeDescriptor, field reflect.StructField, inlineType reflect.Type, index int, secret bool, path Path) *schemaIssue {
|
||||||
|
if b.building[inlineType] {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "recursive inline struct is not supported"}
|
||||||
|
}
|
||||||
|
child, issue := b.describe(inlineType, path)
|
||||||
|
if issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
if child.kind != descriptorStruct {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a structural struct or string-keyed map"}
|
||||||
|
}
|
||||||
|
for _, nested := range child.fields {
|
||||||
|
nested.index = prependIndex(index, nested.index)
|
||||||
|
nested.secret = secret || nested.secret
|
||||||
|
if issue := addField(owner, nested, path); issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if child.inlineMap != nil {
|
||||||
|
nested := *child.inlineMap
|
||||||
|
nested.index = prependIndex(index, nested.index)
|
||||||
|
nested.secret = secret || nested.secret
|
||||||
|
if owner.inlineMap != nil {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
||||||
|
}
|
||||||
|
owner.inlineMap = &nested
|
||||||
|
}
|
||||||
|
|
||||||
|
memberType, issue := b.describe(field.Type, path)
|
||||||
|
if issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
owner.members = append(owner.members, structMember{index: index, descriptor: memberType, inline: true})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *schemaBuilder) describeInlineMap(owner *typeDescriptor, inlineType reflect.Type, index int, secret bool, path Path) *schemaIssue {
|
||||||
|
if inlineType.Key().Kind() != reflect.String {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline map must have string keys"}
|
||||||
|
}
|
||||||
|
if owner.inlineMap != nil {
|
||||||
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldPath := path.Key("*")
|
||||||
|
fieldType, issue := b.describe(inlineType, fieldPath)
|
||||||
|
if issue != nil {
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
owner.inlineMap = &fieldDescriptor{index: []int{index}, descriptor: fieldType, secret: secret, inlineMap: true}
|
||||||
|
owner.members = append(owner.members, structMember{index: index, descriptor: fieldType, inline: true})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user