Files
2026-07-13 02:45:49 -04:00

462 lines
11 KiB
Go

package toml
import (
"bytes"
"fmt"
"math"
"reflect"
"sort"
"strconv"
"strings"
)
// Marshal returns the TOML encoding of v
//
// Marshal supports struct and map types as the root object
// It follows standard TOML formatting:
// - Comments and whitespace are not preserved from original decoding
// - Dates are not supported (as per requirements).
// - Nil pointers are skipped
// - Unexported fields are skipped
// - Fields with `omitempty` are skipped if zero
// - Struct fields/Map keys are sorted alphabetically for determinism
// - Fully numerical keys are rejected
func Marshal(v any) ([]byte, error) {
val := reflect.ValueOf(v)
// Dereference pointer if necessary
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, fmt.Errorf("marshal: cannot marshal nil pointer")
}
val = val.Elem()
}
// Root must be a Table (Struct or Map)
if val.Kind() != reflect.Struct && val.Kind() != reflect.Map {
return nil, fmt.Errorf("marshal: root must be struct or map, got %v", val.Kind())
}
buf := new(bytes.Buffer)
enc := &encoder{w: buf}
if err := enc.encodeTable(val, ""); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type encoder struct {
w *bytes.Buffer
}
// encodeTable writes the fields of a struct or map.
// It uses a two-pass approach:
// 1. Write all scalar values (primitives, inline arrays).
// 2. Recurse and write nested tables (structs, maps, arrays of tables).
// This ensures valid TOML where keys are defined before sub-tables.
func (e *encoder) encodeTable(rv reflect.Value, prefix string) error {
// 1. Gather keys
keys, err := e.getSortedKeys(rv)
if err != nil {
return err
}
// 2. Separation: Identify which keys are scalars (printed now) vs tables (printed later)
var scalars []string
var tables []string
for _, k := range keys {
fieldVal := e.resolveValue(rv, k)
if !fieldVal.IsValid() {
continue // Skip invalid/nil
}
// Check if we should skip (omitempty, unexported handled in getSortedKeys)
if e.shouldSkip(rv, k, fieldVal) {
continue
}
// isTable validates slice homogeneity
isTab, err := e.isTable(fieldVal)
if err != nil {
return fmt.Errorf("key %q: %w", e.getKeyName(rv, k), err)
}
if isTab {
tables = append(tables, k)
} else {
scalars = append(scalars, k)
}
}
// 3. Pass 1: Write Scalars
for _, k := range scalars {
val := e.resolveValue(rv, k)
keyName := e.getKeyName(rv, k)
if err := e.writeKey(keyName); err != nil {
return err
}
if _, err := e.w.WriteString(" = "); err != nil {
return err
}
if err := e.encodeValue(val); err != nil {
return fmt.Errorf("key %q: %w", keyName, err)
}
e.w.WriteString("\n")
}
// 4. Pass 2: Write Tables
for _, k := range tables {
val := e.resolveValue(rv, k)
keyName := e.getKeyName(rv, k)
// Determine full path for header
fullKey := keyName
if prefix != "" {
fullKey = prefix + "." + keyName
}
// Handle specific table types
switch val.Kind() {
case reflect.Struct, reflect.Map:
// [header]
e.w.WriteString("\n")
e.w.WriteString("[" + fullKey + "]\n")
if err := e.encodeTable(val, fullKey); err != nil {
return err
}
case reflect.Slice, reflect.Array:
// [[header]]
for i := 0; i < val.Len(); i++ {
elem := val.Index(i)
// Dereference pointer elements in slice
if elem.Kind() == reflect.Ptr {
if elem.IsNil() {
continue
}
elem = elem.Elem()
}
e.w.WriteString("\n")
e.w.WriteString("[[" + fullKey + "]]\n")
if err := e.encodeTable(elem, fullKey); err != nil {
return err
}
}
}
}
return nil
}
// encodeValue writes a single primitive value or inline array
func (e *encoder) encodeValue(v reflect.Value) error {
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
e.w.WriteString("true")
} else {
e.w.WriteString("false")
}
case reflect.String:
e.encodeString(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
e.w.WriteString(strconv.FormatInt(v.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
e.w.WriteString(strconv.FormatUint(v.Uint(), 10))
case reflect.Float32, reflect.Float64:
f := v.Float()
// NaN/Inf have no TOML representation; emitting them produces
// output the parser rejects
if math.IsNaN(f) || math.IsInf(f, 0) {
return fmt.Errorf("cannot encode %v as TOML float", f)
}
// bitSize matches kind (float32 emitted noise digits);
// 'g' avoids ~100-digit 'f' expansions for large exponents
bits := 64
if v.Kind() == reflect.Float32 {
bits = 32
}
str := strconv.FormatFloat(f, 'g', -1, bits)
if !strings.ContainsAny(str, ".eE") {
str += ".0"
}
e.w.WriteString(str)
case reflect.Slice, reflect.Array:
// Inline array: [1, 2, "3"]
e.w.WriteString("[")
for i := 0; i < v.Len(); i++ {
if i > 0 {
e.w.WriteString(", ")
}
if err := e.encodeValue(v.Index(i)); err != nil {
return err
}
}
e.w.WriteString("]")
case reflect.Interface:
if v.IsNil() {
return nil // Should be handled by caller usually
}
return e.encodeValue(v.Elem())
default:
return fmt.Errorf("unsupported type: %v", v.Kind())
}
return nil
}
// --- Helpers ---
// getSortedKeys returns all field names (struct) or keys (map) sorted
func (e *encoder) getSortedKeys(rv reflect.Value) ([]string, error) {
var keys []string
if rv.Kind() == reflect.Map {
for _, key := range rv.MapKeys() {
if key.Kind() != reflect.String {
return nil, fmt.Errorf("map key must be string, got %v", key.Kind())
}
keys = append(keys, key.String())
}
} else if rv.Kind() == reflect.Struct {
typ := rv.Type()
for i := 0; i < rv.NumField(); i++ {
field := typ.Field(i)
// Skip unexported
if field.PkgPath != "" {
continue
}
// Skip if tag is "-"
tag := field.Tag.Get("toml")
if tag == "-" {
continue
}
keys = append(keys, field.Name)
}
}
// Sort by emitted key (tag-resolved), not Go field name.
// Identity for maps (getKeyName returns the key itself).
sort.Slice(keys, func(i, j int) bool {
return e.getKeyName(rv, keys[i]) < e.getKeyName(rv, keys[j])
})
return keys, nil
}
// resolveValue extracts the value from struct field or map key
// It also handles interface unwrapping for map[string]any.
func (e *encoder) resolveValue(container reflect.Value, key string) reflect.Value {
var val reflect.Value
if container.Kind() == reflect.Map {
val = container.MapIndex(reflect.ValueOf(key))
} else {
val = container.FieldByName(key)
}
// Unwrap interface if needed
if val.Kind() == reflect.Interface && !val.IsNil() {
val = val.Elem()
}
// Dereference pointer if needed (but keep nil ptrs for checking)
if val.Kind() == reflect.Ptr && !val.IsNil() {
val = val.Elem()
}
return val
}
// getKeyName resolves the TOML key name (handles struct tags)
// For maps, the key name is the key itself
func (e *encoder) getKeyName(container reflect.Value, realName string) string {
if container.Kind() == reflect.Map {
return realName
}
// Struct: lookup tag
field, _ := container.Type().FieldByName(realName)
tag := field.Tag.Get("toml")
if tag != "" {
parts := strings.Split(tag, ",")
if parts[0] != "" {
return parts[0]
}
}
return realName
}
// shouldSkip returns true if the field should be omitted (nil ptr, omitempty)
func (e *encoder) shouldSkip(container reflect.Value, realName string, val reflect.Value) bool {
// Skip nil pointers / interfaces
if (val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface) && val.IsNil() {
return true
}
// Maps don't have tags, so we only skip nil values
if container.Kind() == reflect.Map {
return false
}
// Structs: check omitempty
field, _ := container.Type().FieldByName(realName)
tag := field.Tag.Get("toml")
if strings.Contains(tag, "omitempty") && isEmptyValue(val) {
return true
}
return false
}
// isTable determines if a value renders as [Table] / [[Array of Tables]].
// Slices must be homogeneous: mixing table and scalar elements is an error
// (parser accepts such arrays; encoder cannot represent them — documented
// limitation). Nil pointer elements are allowed in table slices and skipped
// at emission; nil interface elements are an error.
func (e *encoder) isTable(v reflect.Value) (bool, error) {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Struct, reflect.Map:
return true, nil
case reflect.Slice, reflect.Array:
tables, scalars := 0, 0
for i := 0; i < v.Len(); i++ {
elem := v.Index(i)
if elem.Kind() == reflect.Interface {
if elem.IsNil() {
return false, fmt.Errorf("nil element at index %d", i)
}
elem = elem.Elem()
}
if elem.Kind() == reflect.Ptr {
if elem.IsNil() {
tables++ // nil pointers are only legal in [[table]] context (skipped)
continue
}
elem = elem.Elem()
}
switch elem.Kind() {
case reflect.Struct, reflect.Map:
tables++
default:
scalars++
}
}
if tables > 0 && scalars > 0 {
return false, fmt.Errorf("mixed table/scalar elements in array")
}
return tables > 0, nil
}
return false, nil
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func (e *encoder) writeKey(s string) error {
if isBareKey(s) {
_, err := e.w.WriteString(s)
return err
}
e.encodeString(s)
return nil
}
func (e *encoder) encodeString(s string) {
e.w.WriteString("\"")
for _, r := range s {
switch r {
case '"':
e.w.WriteString(`\"`)
case '\\':
e.w.WriteString(`\\`)
case '\n':
e.w.WriteString(`\n`)
case '\r':
e.w.WriteString(`\r`)
case '\t':
e.w.WriteString(`\t`)
case '\b':
e.w.WriteString(`\b`)
case '\f':
e.w.WriteString(`\f`)
default:
if r < 0x20 || r == 0x7F {
e.w.WriteString(fmt.Sprintf(`\u%04X`, r))
} else {
e.w.WriteRune(r)
}
}
}
e.w.WriteString("\"")
}
// isBareKey determines if a string can be written as a bare key
// It follows the TOML spec (A-Za-z0-9_-) but also respects the provided Lexer's behavior
// If the Lexer would interpret the string as a Number or Boolean, it must be quoted because the Parser expects TokenIdent (or TokenString) for keys
func isBareKey(s string) bool {
if s == "" {
return false
}
// 1. Valid bare key characters: A-Za-z0-9_-
for _, r := range s {
if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-') {
return false
}
}
// 2. Avoid collision with Booleans
// Lexer emits TokenBool for these, Parser expects TokenIdent.
if s == "true" || s == "false" {
return false
}
// 3. Avoid collision with Numbers
// The provided Lexer triggers number parsing if the token starts with a digit,
// or a '-' followed by a digit.
// Since the Lexer never produces TokenIdent for these cases (it produces TokenInteger/Float),
// and the Parser rejects numeric tokens as keys, we must quote them.
c0 := s[0]
if c0 >= '0' && c0 <= '9' {
return false
}
if c0 == '-' && len(s) > 1 {
c1 := s[1]
if c1 >= '0' && c1 <= '9' {
return false
}
}
return true
}