capsule AI-native Unix-like composition layer

src/server/internal/config/yamlfile.go

4,328 bytes · 176 lines · capsule://quake0day/[email protected] raw on github

package config

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"

	"gopkg.in/yaml.v3"
)

var yamlMu sync.Mutex

// ReadYAMLNode reads a YAML file into a yaml.Node tree without expanding env vars.
func ReadYAMLNode(path string) (*yaml.Node, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", path, err)
	}
	var doc yaml.Node
	if err := yaml.Unmarshal(data, &doc); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	// yaml.Unmarshal wraps in a DocumentNode; return the inner mapping.
	if doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 {
		return &doc, nil
	}
	return &doc, nil
}

// mappingRoot returns the top-level mapping node from a document node.
func mappingRoot(doc *yaml.Node) *yaml.Node {
	if doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 {
		return doc.Content[0]
	}
	return doc
}

// GetNodeAtPath traverses a yaml.Node tree by dot-separated path.
// An empty dotPath returns the root mapping node.
func GetNodeAtPath(doc *yaml.Node, dotPath string) (*yaml.Node, error) {
	node := mappingRoot(doc)
	if dotPath == "" {
		return node, nil
	}
	parts := strings.Split(dotPath, ".")

	for _, key := range parts {
		if node.Kind != yaml.MappingNode {
			return nil, fmt.Errorf("expected mapping at %q, got kind %d", key, node.Kind)
		}
		found := false
		for i := 0; i < len(node.Content)-1; i += 2 {
			if node.Content[i].Value == key {
				node = node.Content[i+1]
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("key %q not found in path %q", key, dotPath)
		}
	}
	return node, nil
}

// GetMappingKeys returns all keys of a mapping node at the given dot-path.
func GetMappingKeys(doc *yaml.Node, dotPath string) ([]string, error) {
	node, err := GetNodeAtPath(doc, dotPath)
	if err != nil {
		return nil, err
	}
	if node.Kind != yaml.MappingNode {
		return nil, fmt.Errorf("node at %q is not a mapping", dotPath)
	}
	var keys []string
	for i := 0; i < len(node.Content)-1; i += 2 {
		keys = append(keys, node.Content[i].Value)
	}
	return keys, nil
}

// SetNodeAtPath sets a scalar value at the given dot-path.
// It auto-detects numeric types so the YAML tag is correct.
func SetNodeAtPath(doc *yaml.Node, dotPath string, value string) error {
	node, err := GetNodeAtPath(doc, dotPath)
	if err != nil {
		return err
	}
	if node.Kind != yaml.ScalarNode {
		return fmt.Errorf("node at %q is not a scalar (kind %d)", dotPath, node.Kind)
	}

	node.Value = value
	node.Tag = inferYAMLTag(value)
	node.Style = 0 // reset style so yaml.v3 picks the natural representation
	return nil
}

// WriteYAMLNode atomically writes a yaml.Node tree back to disk.
func WriteYAMLNode(path string, doc *yaml.Node) error {
	yamlMu.Lock()
	defer yamlMu.Unlock()

	out, err := yaml.Marshal(doc)
	if err != nil {
		return fmt.Errorf("marshal yaml: %w", err)
	}

	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, out, 0644); err != nil {
		return fmt.Errorf("write temp file: %w", err)
	}
	if err := os.Rename(tmp, path); err != nil {
		os.Remove(tmp)
		return fmt.Errorf("rename: %w", err)
	}
	return nil
}

// NodeScalarValue returns the string value of a scalar node,
// optionally expanding ${ENV_VAR} references for display.
func NodeScalarValue(node *yaml.Node, expandEnv bool) string {
	if node.Kind != yaml.ScalarNode {
		return ""
	}
	v := node.Value
	if expandEnv && strings.Contains(v, "${") {
		v = os.ExpandEnv(v)
	}
	return v
}

// NodeValue returns the value as an appropriate Go type (string, int, float64, bool).
func NodeValue(node *yaml.Node, expandEnv bool) any {
	if node.Kind != yaml.ScalarNode {
		return node.Value
	}
	raw := node.Value
	display := raw
	if expandEnv && strings.Contains(raw, "${") {
		display = os.ExpandEnv(raw)
	}

	// Try int
	if i, err := strconv.ParseInt(display, 10, 64); err == nil {
		return i
	}
	// Try float
	if f, err := strconv.ParseFloat(display, 64); err == nil {
		return f
	}
	// Try bool
	if display == "true" {
		return true
	}
	if display == "false" {
		return false
	}
	return display
}

func inferYAMLTag(value string) string {
	if _, err := strconv.ParseInt(value, 10, 64); err == nil {
		return "!!int"
	}
	if _, err := strconv.ParseFloat(value, 64); err == nil {
		return "!!float"
	}
	if value == "true" || value == "false" {
		return "!!bool"
	}
	return "!!str"
}