feat: honour TextMarshaler and TextUnmarshaler by default
Test / test (push) Successful in 1m35s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-19 02:41:09 +02:00
parent 9023784da3
commit 815141440e
8 changed files with 632 additions and 3 deletions
+92 -1
View File
@@ -6,6 +6,7 @@ package interpres
import (
"bytes"
"context"
"encoding"
"errors"
"fmt"
"maps"
@@ -23,6 +24,8 @@ var (
localDateType = reflect.TypeFor[LocalDate]()
localTimeType = reflect.TypeFor[LocalTime]()
timeGoType = reflect.TypeFor[time.Time]()
durationType = reflect.TypeFor[time.Duration]()
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
)
// encoder produces a TOML document from a Go value via a small intermediate
@@ -319,6 +322,15 @@ func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
v = reflect.ValueOf(mv)
}
}
// A type that renders itself as text becomes a TOML string, whether it is
// a scalar kind or a struct.
s, isText, err := textValue(v)
if err != nil {
return &EncodeError{Path: joinKey(ctx, name), Err: err}
}
if isText {
return doc.appendScalar(name, s, ctx)
}
v = followPtr(v)
if !v.IsValid() {
return nil
@@ -500,6 +512,20 @@ func normaliseValue(v reflect.Value) (any, error) {
if t := v.Type(); t == timeGoType || isLocalDateType(t) {
return v.Interface(), nil
}
// TOML has no duration type, so a duration goes out in its canonical Go
// form, the shape it comes back in.
if v.Type() == durationType {
return time.Duration(v.Int()).String(), nil
}
// A type that renders itself as text becomes a TOML string, scalar kinds
// and structs alike.
s, isText, err := textValue(v)
if err != nil {
return nil, err
}
if isText {
return s, nil
}
switch v.Kind() {
case reflect.String:
return v.String(), nil
@@ -573,10 +599,75 @@ func isLocalDateType(t reflect.Type) bool {
return t == localDateTimeType || t == localDateType || t == localTimeType
}
// isDateTimeType reports whether t is one of the four TOML date-time types,
// which the encoder emits as bare atoms. Pointers are looked through. The types
// carry time.Time's text methods through an embedded field, and the atom form
// takes precedence over them.
func isDateTimeType(t reflect.Type) bool {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t == timeGoType || isLocalDateType(t)
}
// isTextMarshalerType reports whether t or *t implements
// encoding.TextMarshaler. An array of such values stays a value array, because
// each element's TOML form is a string.
func isTextMarshalerType(t reflect.Type) bool {
if isDateTimeType(t) {
return false
}
return t.Implements(textMarshalerType) || reflect.PointerTo(t).Implements(textMarshalerType)
}
// textValue returns the string a value renders itself as through
// encoding.TextMarshaler. The date-time types are excluded, because their
// embedded time.Time would answer with an RFC 3339 string where the TOML form
// is a bare timestamp. A nil pointer offers no text and is left to the ordinary
// nil handling, which omits the field.
func textValue(v reflect.Value) (string, bool, error) {
for v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
if !v.IsValid() || isDateTimeType(v.Type()) {
return "", false, nil
}
if v.Kind() == reflect.Pointer && v.IsNil() {
return "", false, nil
}
m, ok := textMarshalerOf(v)
if !ok {
return "", false, nil
}
b, err := m.MarshalText()
if err != nil {
return "", true, err
}
return string(b), true, nil
}
// textMarshalerOf finds the encoding.TextMarshaler for v: on the value itself,
// or on its address, so a pointer-receiver MarshalText is found on an
// addressable struct field.
func textMarshalerOf(v reflect.Value) (encoding.TextMarshaler, bool) {
if !v.CanInterface() {
return nil, false
}
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
return m, true
}
if v.CanAddr() {
if m, ok := v.Addr().Interface().(encoding.TextMarshaler); ok {
return m, true
}
}
return nil, false
}
func isTableElementType(t reflect.Type) bool {
switch t.Kind() {
case reflect.Struct:
return !isScalarStruct(t)
return !isScalarStruct(t) && !isTextMarshalerType(t)
case reflect.Map:
return t.Key().Kind() == reflect.String
}