Add internal engine relocation candidate
This commit is contained in:
248
internal/engine/projection.go
Normal file
248
internal/engine/projection.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func projectConfiguration(schema *schemaDescriptor, value reflect.Value, limits projectionLimits) (*document.Node, *projectionIssue) {
|
||||
if schema == nil || schema.root == nil || !value.IsValid() || value.Type() != schema.root.typeOf {
|
||||
return nil, &projectionIssue{code: CodeSchemaUnsupportedType, message: "configuration value does not match its schema"}
|
||||
}
|
||||
operation := valueOperation{schema: schema, limits: limits, active: make(map[valueVisit]bool)}
|
||||
return operation.project(value, schema.root, Path{}, 0)
|
||||
}
|
||||
|
||||
func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor, path Path, depth int) (*document.Node, *projectionIssue) {
|
||||
if issue := o.checkDepth(path, depth); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
if descriptor.kind == descriptorPointer {
|
||||
if value.IsNil() {
|
||||
return o.node(document.Null, path, depth)
|
||||
}
|
||||
leave, issue := o.enter(value, path)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
defer leave()
|
||||
return o.project(value.Elem(), descriptor.element, path, depth)
|
||||
}
|
||||
|
||||
switch descriptor.kind {
|
||||
case descriptorStruct:
|
||||
result, issue := o.node(document.Mapping, path, depth)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Mapping = make([]document.Entry, 0, len(descriptor.fields))
|
||||
seen := make(map[string]bool, len(descriptor.fields))
|
||||
for _, member := range descriptor.members {
|
||||
memberValue := value.Field(member.index)
|
||||
if member.inline {
|
||||
if isNilValue(memberValue) {
|
||||
continue
|
||||
}
|
||||
inline, issue := o.project(memberValue, member.descriptor, path, depth)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
if inline.Kind != document.Mapping {
|
||||
return nil, &projectionIssue{code: CodeSchemaUnsupportedType, path: path.clone(), message: "inline value did not project to a mapping"}
|
||||
}
|
||||
o.nodes-- // The flattened container is not retained in the output tree.
|
||||
for _, entry := range inline.Mapping {
|
||||
if seen[entry.Key] {
|
||||
return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: path.Key(entry.Key), message: "inline value conflicts with a declared field"}
|
||||
}
|
||||
seen[entry.Key] = true
|
||||
result.Mapping = append(result.Mapping, entry)
|
||||
}
|
||||
continue
|
||||
}
|
||||
childPath := path.Key(member.name)
|
||||
if seen[member.name] {
|
||||
return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: childPath, message: "inline value conflicts with a declared field"}
|
||||
}
|
||||
if issue := o.charge(childPath); issue != nil { // mapping key
|
||||
return nil, issue
|
||||
}
|
||||
child, issue := o.project(memberValue, member.descriptor, childPath, depth+1)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
seen[member.name] = true
|
||||
result.Mapping = append(result.Mapping, document.Entry{Key: member.name, Value: child})
|
||||
}
|
||||
for _, entry := range result.Mapping {
|
||||
field := descriptor.inlineMap
|
||||
if index, ok := descriptor.fieldByName[entry.Key]; ok {
|
||||
field = &descriptor.fields[index]
|
||||
}
|
||||
if field != nil && field.secret {
|
||||
entry.Value.Sensitive = true
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
case descriptorMap:
|
||||
if value.IsNil() {
|
||||
return o.node(document.Null, path, depth)
|
||||
}
|
||||
leave, issue := o.enter(value, path)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
defer leave()
|
||||
result, issue := o.node(document.Mapping, path, depth)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Mapping = make([]document.Entry, 0, value.Len())
|
||||
keys := value.MapKeys()
|
||||
slices.SortFunc(keys, func(left, right reflect.Value) int {
|
||||
return cmp.Compare(left.String(), right.String())
|
||||
})
|
||||
for _, key := range keys {
|
||||
childPath := path.Key(key.String())
|
||||
if issue := o.charge(childPath); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
child, issue := o.project(value.MapIndex(key), descriptor.element, childPath, depth+1)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Mapping = append(result.Mapping, document.Entry{Key: key.String(), Value: child})
|
||||
}
|
||||
return result, nil
|
||||
case descriptorSlice:
|
||||
if value.IsNil() {
|
||||
return o.node(document.Null, path, depth)
|
||||
}
|
||||
leave, issue := o.enter(value, path)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
defer leave()
|
||||
fallthrough
|
||||
case descriptorArray:
|
||||
result, issue := o.node(document.Sequence, path, depth)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Sequence = make([]*document.Node, 0, value.Len())
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
childPath := path.Index(i)
|
||||
child, issue := o.project(value.Index(i), descriptor.element, childPath, depth+1)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Sequence = append(result.Sequence, child)
|
||||
}
|
||||
return result, nil
|
||||
case descriptorDynamic:
|
||||
if value.IsNil() {
|
||||
return o.node(document.Null, path, depth)
|
||||
}
|
||||
concrete := value.Elem()
|
||||
dynamic, issue := o.dynamicDescriptor(concrete.Type(), path)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
return o.project(concrete, dynamic, path, depth)
|
||||
case descriptorString:
|
||||
node, issue := o.node(document.String, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = value.String()
|
||||
}
|
||||
return node, issue
|
||||
case descriptorBoolean:
|
||||
node, issue := o.node(document.Boolean, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = strconv.FormatBool(value.Bool())
|
||||
}
|
||||
return node, issue
|
||||
case descriptorSigned:
|
||||
node, issue := o.node(document.Number, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = strconv.FormatInt(value.Int(), 10)
|
||||
}
|
||||
return node, issue
|
||||
case descriptorUnsigned:
|
||||
node, issue := o.node(document.Number, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = strconv.FormatUint(value.Uint(), 10)
|
||||
}
|
||||
return node, issue
|
||||
case descriptorFloat:
|
||||
if math.IsNaN(value.Float()) || math.IsInf(value.Float(), 0) {
|
||||
return nil, &projectionIssue{code: CodeSchemaNonFinite, path: path.clone(), message: "configuration float must be finite"}
|
||||
}
|
||||
node, issue := o.node(document.Number, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = strconv.FormatFloat(value.Float(), 'g', -1, value.Type().Bits())
|
||||
}
|
||||
return node, issue
|
||||
case descriptorDuration:
|
||||
node, issue := o.node(document.String, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = time.Duration(value.Int()).String()
|
||||
}
|
||||
return node, issue
|
||||
case descriptorSecret:
|
||||
node, issue := o.node(document.String, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = value.Interface().(Secret).EnvName()
|
||||
}
|
||||
return node, issue
|
||||
case descriptorText:
|
||||
text, issue := marshalCanonicalText(value, path)
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
node, nodeIssue := o.node(document.String, path, depth)
|
||||
if nodeIssue == nil {
|
||||
node.Text = text
|
||||
}
|
||||
return node, nodeIssue
|
||||
default:
|
||||
return nil, &projectionIssue{code: CodeSchemaUnsupportedType, path: path.clone(), message: "configuration type is not supported"}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *valueOperation) node(kind document.Kind, path Path, depth int) (*document.Node, *projectionIssue) {
|
||||
if issue := o.checkDepth(path, depth); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
if issue := o.charge(path); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
return &document.Node{Kind: kind}, nil
|
||||
}
|
||||
|
||||
func marshalCanonicalText(value reflect.Value, path Path) (string, *projectionIssue) {
|
||||
var marshaler encoding.TextMarshaler
|
||||
if value.CanInterface() {
|
||||
marshaler, _ = value.Interface().(encoding.TextMarshaler)
|
||||
}
|
||||
if marshaler == nil {
|
||||
addressable := reflect.New(value.Type())
|
||||
addressable.Elem().Set(value)
|
||||
marshaler, _ = addressable.Interface().(encoding.TextMarshaler)
|
||||
}
|
||||
if marshaler == nil {
|
||||
return "", &projectionIssue{code: CodeSchemaCodec, path: path.clone(), message: "custom text scalar has no usable text marshaler"}
|
||||
}
|
||||
text, err := marshaler.MarshalText()
|
||||
if err != nil || !utf8.Valid(text) {
|
||||
return "", &projectionIssue{code: CodeSchemaCodec, path: path.clone(), message: "custom text scalar could not be projected"}
|
||||
}
|
||||
return string(text), nil
|
||||
}
|
||||
Reference in New Issue
Block a user