mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 08:30:29 +00:00
213 lines
4.8 KiB
Go
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
|
|
}
|