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 = o.schema.secrets.Read(value).Name } 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 }