Separate schema field discovery and assembly

This commit is contained in:
2026-10-25 13:46:51 +00:00
parent ac4ce242a2
commit 0fbce5d141
3 changed files with 113 additions and 70 deletions

View File

@@ -241,80 +241,15 @@ func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current refle
if name != "" {
return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"}
}
inlineType := field.Type
seenPointers := make(map[reflect.Type]bool)
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"}
if issue := b.describeInlineField(descriptor, field, i, secret, path); issue != nil {
return issue
}
continue
}
fieldPath := path.Key(name)
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 {
if issue := b.describeOrdinaryField(descriptor, field, i, name, secret, path); issue != nil {
return issue
}
descriptor.members = append(descriptor.members, structMember{index: i, name: name, descriptor: fieldType})
}
return nil
}

View 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
}