Files
at-container-registry/pkg/config/marshal.go
2026-02-07 22:45:10 -06:00

213 lines
4.8 KiB
Go

package config
import (
"fmt"
"reflect"
"sort"
"strings"
"time"
"go.yaml.in/yaml/v4"
)
// MarshalCommentedYAML serializes cfg into a YAML document with comments
// derived from `comment` struct tags. The title becomes the document head comment.
// Fields with `yaml:"-"` are excluded. Nested structs become YAML mapping nodes.
// time.Duration values render as their string representation (e.g. "15m0s").
func MarshalCommentedYAML(title string, cfg any) ([]byte, error) {
doc := &yaml.Node{
Kind: yaml.DocumentNode,
}
if title != "" {
doc.HeadComment = title + "\nGenerated with defaults — edit as needed."
}
root, err := structToNode(reflect.ValueOf(cfg))
if err != nil {
return nil, err
}
doc.Content = append(doc.Content, root)
return yaml.Marshal(doc)
}
// structToNode converts a struct value into a yaml.MappingNode.
func structToNode(v reflect.Value) (*yaml.Node, error) {
// Dereference pointer
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, nil
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct, got %s", v.Kind())
}
mapping := &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fv := v.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Read yaml tag
yamlTag := field.Tag.Get("yaml")
if yamlTag == "-" {
continue
}
yamlName := yamlTag
if idx := strings.Index(yamlName, ","); idx != -1 {
yamlName = yamlName[:idx]
}
if yamlName == "" {
yamlName = strings.ToLower(field.Name)
}
comment := field.Tag.Get("comment")
// Build key node
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: yamlName,
}
if comment != "" {
keyNode.HeadComment = comment
}
// Build value node
valNode, err := valueToNode(fv)
if err != nil {
return nil, fmt.Errorf("field %s: %w", field.Name, err)
}
mapping.Content = append(mapping.Content, keyNode, valNode)
}
return mapping, nil
}
// valueToNode converts a reflect.Value into the appropriate yaml.Node.
func valueToNode(v reflect.Value) (*yaml.Node, error) {
// Dereference pointer
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: ""}, nil
}
v = v.Elem()
}
// Handle time.Duration specially: render as string, not nanoseconds
if v.Type() == reflect.TypeOf(time.Duration(0)) {
d := v.Interface().(time.Duration)
s := d.String()
if d == 0 {
s = "0s"
}
return &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: s,
}, nil
}
// Nested struct → recurse
if v.Kind() == reflect.Struct {
return structToNode(v)
}
// Map → yaml mapping with sorted keys
if v.Kind() == reflect.Map {
return mapToNode(v)
}
// Slice → yaml sequence
if v.Kind() == reflect.Slice {
seq := &yaml.Node{
Kind: yaml.SequenceNode,
Tag: "!!seq",
}
for i := 0; i < v.Len(); i++ {
elemNode, err := valueToNode(v.Index(i))
if err != nil {
return nil, fmt.Errorf("slice index %d: %w", i, err)
}
seq.Content = append(seq.Content, elemNode)
}
return seq, nil
}
// Scalar types
node := &yaml.Node{Kind: yaml.ScalarNode}
switch v.Kind() {
case reflect.String:
node.Tag = "!!str"
node.Value = v.String()
case reflect.Bool:
node.Tag = "!!bool"
node.Value = fmt.Sprintf("%t", v.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
node.Tag = "!!int"
node.Value = fmt.Sprintf("%d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
node.Tag = "!!int"
node.Value = fmt.Sprintf("%d", v.Uint())
case reflect.Float32, reflect.Float64:
node.Tag = "!!float"
node.Value = fmt.Sprintf("%g", v.Float())
default:
// Fall back to letting yaml.Marshal handle it
node.Tag = "!!str"
node.Value = fmt.Sprintf("%v", v.Interface())
}
return node, nil
}
// mapToNode converts a map value into a yaml.MappingNode with sorted keys.
func mapToNode(v reflect.Value) (*yaml.Node, error) {
mapping := &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
}
if v.IsNil() || v.Len() == 0 {
return mapping, nil
}
// Sort keys for deterministic output
keys := make([]string, 0, v.Len())
for _, k := range v.MapKeys() {
keys = append(keys, fmt.Sprintf("%v", k.Interface()))
}
sort.Strings(keys)
for _, keyStr := range keys {
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: keyStr,
}
val := v.MapIndex(reflect.ValueOf(keyStr))
valNode, err := valueToNode(val)
if err != nil {
return nil, fmt.Errorf("map key %q: %w", keyStr, err)
}
mapping.Content = append(mapping.Content, keyNode, valNode)
}
return mapping, nil
}