package engine import ( "encoding" "reflect" "strconv" "strings" "time" ) type descriptorKind uint8 const ( descriptorInvalid descriptorKind = iota descriptorStruct descriptorMap descriptorSlice descriptorArray descriptorPointer descriptorDynamic descriptorString descriptorBoolean descriptorSigned descriptorUnsigned descriptorFloat descriptorDuration descriptorSecret descriptorText ) type schemaDescriptor struct { root *typeDescriptor types map[reflect.Type]*typeDescriptor secrets SecretBinding } type typeDescriptor struct { typeOf reflect.Type kind descriptorKind element *typeDescriptor length int members []structMember fields []fieldDescriptor fieldByName map[string]int inlineMap *fieldDescriptor } type structMember struct { index int name string descriptor *typeDescriptor inline bool } type fieldDescriptor struct { name string index []int descriptor *typeDescriptor secret bool inlineMap bool } type schemaIssue struct { code Code path Path message string } // SecretState is the private operation state for one exact-type secret value. // It is supplied by the facade and never enters inspection records. type SecretState struct { Name string Value string Specified bool Resolved bool } // SecretBinding adapts the facade-owned exact secret type for one load. // Bindings are operation data: callers must supply the type and conversions // together, and a binding never becomes package-global state. type SecretBinding struct { Type reflect.Type Read func(reflect.Value) SecretState Make func(SecretState) reflect.Value } var ( durationType = reflect.TypeFor[time.Duration]() textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]() textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]() ) func buildSchema(root reflect.Type, secrets SecretBinding) (*schemaDescriptor, *schemaIssue) { secrets.validate() if root == nil || (root.Kind() != reflect.Struct && !isStringMap(root)) { return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a struct or string-keyed map"} } builder := schemaBuilder{ cache: make(map[reflect.Type]*typeDescriptor), building: make(map[reflect.Type]bool), secrets: secrets, } descriptor, issue := builder.describe(root, Path{}) if issue != nil { return nil, issue } if descriptor.kind != descriptorStruct && descriptor.kind != descriptorMap { return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a structural struct or string-keyed map"} } return &schemaDescriptor{root: descriptor, types: builder.cache, secrets: secrets}, nil } type schemaBuilder struct { cache map[reflect.Type]*typeDescriptor building map[reflect.Type]bool journal []reflect.Type secrets SecretBinding } func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescriptor, *schemaIssue) { if descriptor, ok := b.cache[current]; ok { return descriptor, nil } descriptor := &typeDescriptor{typeOf: current} b.cache[current] = descriptor b.journal = append(b.journal, current) b.building[current] = true defer delete(b.building, current) fail := func(code Code, message string) (*typeDescriptor, *schemaIssue) { delete(b.cache, current) return nil, &schemaIssue{code: code, path: path.clone(), message: message} } if current.Kind() == reflect.Pointer { descriptor.kind = descriptorPointer element, issue := b.describe(current.Elem(), path) if issue != nil { return nil, issue } descriptor.element = element return descriptor, nil } if current == durationType { descriptor.kind = descriptorDuration return descriptor, nil } if b.secrets.Type != nil && current == b.secrets.Type { descriptor.kind = descriptorSecret return descriptor, nil } marshals, unmarshals := textCodecSupport(current) if marshals || unmarshals { if !marshals || !unmarshals { return fail(CodeSchemaTextCodec, "custom text scalar must implement both text marshal and unmarshal") } descriptor.kind = descriptorText return descriptor, nil } switch current.Kind() { case reflect.Struct: descriptor.kind = descriptorStruct descriptor.fieldByName = make(map[string]int) if issue := b.describeFields(descriptor, current, path); issue != nil { delete(b.cache, current) return nil, issue } case reflect.Map: if current.Key().Kind() != reflect.String { return fail(CodeSchemaUnsupportedType, "configuration maps must have string keys") } descriptor.kind = descriptorMap element, issue := b.describe(current.Elem(), path) if issue != nil { return nil, issue } descriptor.element = element case reflect.Slice: descriptor.kind = descriptorSlice element, issue := b.describe(current.Elem(), path.Index(0)) if issue != nil { return nil, issue } descriptor.element = element case reflect.Array: descriptor.kind = descriptorArray descriptor.length = current.Len() element, issue := b.describe(current.Elem(), path.Index(0)) if issue != nil { return nil, issue } descriptor.element = element case reflect.Interface: if current.NumMethod() != 0 { return fail(CodeSchemaUnsupportedType, "only the empty interface is supported for dynamic values") } descriptor.kind = descriptorDynamic case reflect.String: descriptor.kind = descriptorString case reflect.Bool: descriptor.kind = descriptorBoolean case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: descriptor.kind = descriptorSigned case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: descriptor.kind = descriptorUnsigned case reflect.Float32, reflect.Float64: descriptor.kind = descriptorFloat default: return fail(CodeSchemaUnsupportedType, "configuration type is not supported") } return descriptor, nil } func (b *schemaBuilder) rollback() { for _, current := range b.journal { delete(b.cache, current) } b.journal = nil } func (binding SecretBinding) validate() { if binding.Type == nil { return } if binding.Read == nil || binding.Make == nil { panic("configkit engine: incomplete secret binding") } } func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current reflect.Type, path Path) *schemaIssue { for i := 0; i < current.NumField(); i++ { field := current.Field(i) if !field.IsExported() { continue } name, inline, ignored, issue := parseYAMLField(field, path) if issue != nil { return issue } if ignored { continue } secret, issue := parseConfigKitField(field, path) if issue != nil { return issue } if inline { if name != "" { return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"} } if issue := b.describeInlineField(descriptor, field, i, secret, path); issue != nil { return issue } continue } if issue := b.describeOrdinaryField(descriptor, field, i, name, secret, path); issue != nil { return issue } } return nil } func addField(owner *typeDescriptor, field fieldDescriptor, path Path) *schemaIssue { if _, exists := owner.fieldByName[field.name]; exists { return &schemaIssue{code: CodeSchemaAmbiguousField, path: path.Key(field.name), message: "configuration field name is ambiguous"} } owner.fieldByName[field.name] = len(owner.fields) owner.fields = append(owner.fields, field) return nil } func parseYAMLField(field reflect.StructField, path Path) (name string, inline, ignored bool, issue *schemaIssue) { name = strings.ToLower(field.Name) tag, present := field.Tag.Lookup("yaml") if !present { return name, false, false, nil } parts := strings.Split(tag, ",") if parts[0] == "-" { if len(parts) != 1 { return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "ignored YAML field cannot declare options"} } return "", false, true, nil } if parts[0] != "" { name = parts[0] } seen := make(map[string]bool, len(parts)-1) for _, option := range parts[1:] { if option == "" || seen[option] { return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.Key(name), message: "YAML field tag is malformed"} } seen[option] = true switch option { case "inline": inline = true case "omitempty": // ConfigKit accepts the spelling but never omits projected values. default: return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.Key(name), message: "YAML field tag option is not supported"} } } if inline && parts[0] == "" { name = "" } return name, inline, false, nil } func parseConfigKitField(field reflect.StructField, path Path) (bool, *schemaIssue) { tag, present := field.Tag.Lookup("configkit") if !present || tag == "" { return false, nil } if tag != "secret" { return false, &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "ConfigKit field tag is not supported"} } return true, nil } func textCodecSupport(current reflect.Type) (marshals, unmarshals bool) { marshals = current.Implements(textMarshalerType) unmarshals = current.Implements(textUnmarshalerType) if current.Kind() != reflect.Pointer { pointer := reflect.PointerTo(current) marshals = marshals || pointer.Implements(textMarshalerType) unmarshals = unmarshals || pointer.Implements(textUnmarshalerType) } return marshals, unmarshals } func isStringMap(current reflect.Type) bool { return current.Kind() == reflect.Map && current.Key().Kind() == reflect.String } func prependIndex(index int, nested []int) []int { result := make([]int, 1, len(nested)+1) result[0] = index return append(result, nested...) } type schemaPath struct { descriptor *typeDescriptor } // terminalDescriptor follows a pointer-only descriptor chain without entering // any value. Recursive pointer aliases can refer back to an earlier descriptor, // so callers must handle a repeated descriptor as an unaddressable chain. func terminalDescriptor(descriptor *typeDescriptor) (*typeDescriptor, bool) { visited := make(map[*typeDescriptor]struct{}) for descriptor != nil && descriptor.kind == descriptorPointer { if _, exists := visited[descriptor]; exists { return nil, false } visited[descriptor] = struct{}{} descriptor = descriptor.element } return descriptor, descriptor != nil } func resolveSchemaPath(schema *schemaDescriptor, path Path) (schemaPath, bool) { if schema == nil || schema.root == nil || !path.valid() { return schemaPath{}, false } current := schema.root for _, segment := range path.segments { var ok bool current, ok = terminalDescriptor(current) if !ok { return schemaPath{}, false } switch current.kind { case descriptorStruct: if segment.kind == pathSegmentIndex { return schemaPath{}, false } position, ok := current.fieldByName[segment.key] if ok { current = current.fields[position].descriptor continue } if current.inlineMap != nil { current = current.inlineMap.descriptor.element continue } return schemaPath{}, false case descriptorMap: if segment.kind == pathSegmentIndex { return schemaPath{}, false } current = current.element case descriptorSlice, descriptorArray: index, ok := schemaIndex(segment) if !ok || (current.kind == descriptorArray && index >= current.length) { return schemaPath{}, false } current = current.element case descriptorDynamic: return schemaPath{descriptor: current}, true default: return schemaPath{}, false } } return schemaPath{descriptor: current}, true } func schemaIndex(segment pathSegment) (int, bool) { if segment.kind == pathSegmentIndex { return segment.index, segment.index >= 0 } if segment.kind != pathSegmentParsed || segment.key == "" || (len(segment.key) > 1 && segment.key[0] == '0') { return 0, false } index, err := strconv.Atoi(segment.key) return index, err == nil && index >= 0 && strconv.Itoa(index) == segment.key }