package configkit import ( "encoding" "math" "reflect" "slices" "strconv" "time" "unicode/utf8" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type projectionLimits struct { depth int nodes int } func defaultProjectionLimits() projectionLimits { return projectionLimits{depth: maxDocumentDepth, nodes: maxDocumentNodes} } type projectionIssue struct { code Code path Path message string } type valueVisit struct { typeOf reflect.Type kind reflect.Kind ptr uintptr } type valueOperation struct { schema *schemaDescriptor limits projectionLimits active map[valueVisit]bool nodes int } 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 cloneConfiguration(schema *schemaDescriptor, value reflect.Value, limits projectionLimits) (reflect.Value, *projectionIssue) { if schema == nil || schema.root == nil || !value.IsValid() || value.Type() != schema.root.typeOf { return reflect.Value{}, &projectionIssue{code: CodeSchemaUnsupportedType, message: "configuration value does not match its schema"} } operation := valueOperation{schema: schema, limits: limits, active: make(map[valueVisit]bool)} return operation.clone(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}) } 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 cmpString(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) clone(value reflect.Value, descriptor *typeDescriptor, path Path, depth int) (reflect.Value, *projectionIssue) { if issue := o.checkDepth(path, depth); issue != nil { return reflect.Value{}, issue } if descriptor.kind == descriptorPointer { if value.IsNil() { return reflect.Zero(value.Type()), nil } leave, issue := o.enter(value, path) if issue != nil { return reflect.Value{}, issue } defer leave() child, issue := o.clone(value.Elem(), descriptor.element, path, depth) if issue != nil { return reflect.Value{}, issue } result := reflect.New(value.Type().Elem()) result.Elem().Set(child) return result, nil } switch descriptor.kind { case descriptorStruct: result := reflect.New(value.Type()).Elem() for _, member := range descriptor.members { memberPath, memberDepth := path, depth if !member.inline { memberPath, memberDepth = path.Key(member.name), depth+1 } cloned, issue := o.clone(value.Field(member.index), member.descriptor, memberPath, memberDepth) if issue != nil { return reflect.Value{}, issue } result.Field(member.index).Set(cloned) } return result, nil case descriptorMap: if value.IsNil() { return reflect.Zero(value.Type()), nil } leave, issue := o.enter(value, path) if issue != nil { return reflect.Value{}, issue } defer leave() result := reflect.MakeMapWithSize(value.Type(), value.Len()) for _, key := range value.MapKeys() { cloned, issue := o.clone(value.MapIndex(key), descriptor.element, path.Key(key.String()), depth+1) if issue != nil { return reflect.Value{}, issue } result.SetMapIndex(key, cloned) } return result, nil case descriptorSlice: if value.IsNil() { return reflect.Zero(value.Type()), nil } leave, issue := o.enter(value, path) if issue != nil { return reflect.Value{}, issue } defer leave() result := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) for i := 0; i < value.Len(); i++ { cloned, issue := o.clone(value.Index(i), descriptor.element, path.Index(i), depth+1) if issue != nil { return reflect.Value{}, issue } result.Index(i).Set(cloned) } return result, nil case descriptorArray: result := reflect.New(value.Type()).Elem() for i := 0; i < value.Len(); i++ { cloned, issue := o.clone(value.Index(i), descriptor.element, path.Index(i), depth+1) if issue != nil { return reflect.Value{}, issue } result.Index(i).Set(cloned) } return result, nil case descriptorDynamic: if value.IsNil() { return reflect.Zero(value.Type()), nil } concrete := value.Elem() dynamic, issue := o.dynamicDescriptor(concrete.Type(), path) if issue != nil { return reflect.Value{}, issue } cloned, issue := o.clone(concrete, dynamic, path, depth) if issue != nil { return reflect.Value{}, issue } result := reflect.New(value.Type()).Elem() result.Set(cloned) return result, nil case descriptorSecret: result := reflect.New(value.Type()).Elem() reference := value.Interface().(Secret) if reference.specified { result.Set(reflect.ValueOf(SecretEnv(reference.EnvName()))) } return result, nil case descriptorText: text, issue := marshalCanonicalText(value, path) if issue != nil { return reflect.Value{}, issue } result := reflect.New(value.Type()).Elem() unmarshaler := result.Addr().Interface().(encoding.TextUnmarshaler) if err := unmarshaler.UnmarshalText([]byte(text)); err != nil { return reflect.Value{}, &projectionIssue{code: CodeSchemaCodec, path: path.clone(), message: "custom text scalar could not be reconstructed"} } return result, nil case descriptorFloat: if math.IsNaN(value.Float()) || math.IsInf(value.Float(), 0) { return reflect.Value{}, &projectionIssue{code: CodeSchemaNonFinite, path: path.clone(), message: "configuration float must be finite"} } fallthrough default: result := reflect.New(value.Type()).Elem() result.Set(value) return result, nil } } func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*typeDescriptor, *projectionIssue) { if current == secretType { return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"} } builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool)} descriptor, issue := builder.describe(current, path) if issue != nil { return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration value has an unsupported type"} } if descriptorContainsSecret(descriptor, make(map[*typeDescriptor]bool)) { return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"} } return descriptor, nil } func descriptorContainsSecret(descriptor *typeDescriptor, visited map[*typeDescriptor]bool) bool { if descriptor == nil || visited[descriptor] { return false } if descriptor.kind == descriptorSecret { return true } visited[descriptor] = true if descriptorContainsSecret(descriptor.element, visited) { return true } for _, member := range descriptor.members { if descriptorContainsSecret(member.descriptor, visited) { return true } } return false } 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 (o *valueOperation) charge(path Path) *projectionIssue { o.nodes++ if o.limits.nodes >= 0 && o.nodes > o.limits.nodes { return &projectionIssue{code: CodeSchemaProjectionLimit, path: path.clone(), message: "configuration projection exceeds the node limit"} } return nil } func (o *valueOperation) checkDepth(path Path, depth int) *projectionIssue { if depth > o.limits.depth { return &projectionIssue{code: CodeSchemaProjectionLimit, path: path.clone(), message: "configuration projection exceeds the depth limit"} } return nil } func (o *valueOperation) enter(value reflect.Value, path Path) (func(), *projectionIssue) { visit, ok := visitFor(value) if !ok { return func() {}, nil } if o.active[visit] { return nil, &projectionIssue{code: CodeSchemaCycle, path: path.clone(), message: "configuration values must not contain cycles"} } o.active[visit] = true return func() { delete(o.active, visit) }, nil } func visitFor(value reflect.Value) (valueVisit, bool) { if isNilValue(value) { return valueVisit{}, false } visit := valueVisit{typeOf: value.Type(), kind: value.Kind()} switch value.Kind() { case reflect.Pointer, reflect.Slice: if value.Kind() == reflect.Slice && value.Len() == 0 { return valueVisit{}, false } visit.ptr = value.Pointer() case reflect.Map: if value.Len() == 0 { return valueVisit{}, false } visit.ptr = uintptr(value.UnsafePointer()) default: return valueVisit{}, false } return visit, visit.ptr != 0 } func isNilValue(value reflect.Value) bool { switch value.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return value.IsNil() default: return false } } 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 } func cmpString(left, right string) int { switch { case left < right: return -1 case left > right: return 1 default: return 0 } } type documentDeltaKind uint8 const ( documentAdded documentDeltaKind = iota + 1 documentRemoved documentReplaced ) type documentDelta struct { path Path kind documentDeltaKind } func compareDocumentTrees(before, after *document.Node) []documentDelta { var deltas []documentDelta compareDocumentNode(before, after, Path{}, &deltas) slices.SortFunc(deltas, func(left, right documentDelta) int { if order := cmpString(left.path.String(), right.path.String()); order != 0 { return order } return int(left.kind) - int(right.kind) }) return deltas } func compareDocumentNode(before, after *document.Node, path Path, deltas *[]documentDelta) { if before == nil && after == nil { return } if before == nil { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentAdded}) return } if after == nil { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentRemoved}) return } if before.Kind != after.Kind { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) return } switch before.Kind { case document.Mapping: beforeEntries := make(map[string]*document.Node, len(before.Mapping)) afterEntries := make(map[string]*document.Node, len(after.Mapping)) keys := make(map[string]bool, len(before.Mapping)+len(after.Mapping)) for _, entry := range before.Mapping { beforeEntries[entry.Key] = entry.Value keys[entry.Key] = true } for _, entry := range after.Mapping { afterEntries[entry.Key] = entry.Value keys[entry.Key] = true } ordered := make([]string, 0, len(keys)) for key := range keys { ordered = append(ordered, key) } slices.Sort(ordered) for _, key := range ordered { compareDocumentNode(beforeEntries[key], afterEntries[key], path.Key(key), deltas) } case document.Sequence: if !documentNodesEqual(before, after) { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) } default: if before.Text != after.Text { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) } } } func documentNodesEqual(left, right *document.Node) bool { if left == nil || right == nil { return left == right } if left.Kind != right.Kind || left.Text != right.Text || len(left.Mapping) != len(right.Mapping) || len(left.Sequence) != len(right.Sequence) { return false } if left.Kind == document.Mapping { leftMap := make(map[string]*document.Node, len(left.Mapping)) for _, entry := range left.Mapping { leftMap[entry.Key] = entry.Value } for _, entry := range right.Mapping { if !documentNodesEqual(leftMap[entry.Key], entry.Value) { return false } } } for i := range left.Sequence { if !documentNodesEqual(left.Sequence[i], right.Sequence[i]) { return false } } return true }