feat: add OffsetDateTime, nesting limits and uniform Marshaler dispatch
Test / test (push) Successful in 2m18s
Test / test (push) Successful in 2m18s
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -20,12 +20,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
localDateTimeType = reflect.TypeFor[LocalDateTime]()
|
||||
localDateType = reflect.TypeFor[LocalDate]()
|
||||
localTimeType = reflect.TypeFor[LocalTime]()
|
||||
timeGoType = reflect.TypeFor[time.Time]()
|
||||
durationType = reflect.TypeFor[time.Duration]()
|
||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||
localDateTimeType = reflect.TypeFor[LocalDateTime]()
|
||||
localDateType = reflect.TypeFor[LocalDate]()
|
||||
localTimeType = reflect.TypeFor[LocalTime]()
|
||||
offsetDateTimeType = reflect.TypeFor[OffsetDateTime]()
|
||||
timeGoType = reflect.TypeFor[time.Time]()
|
||||
durationType = reflect.TypeFor[time.Duration]()
|
||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||
)
|
||||
|
||||
// inlineLimit is the column past which an inline table is written across
|
||||
@@ -342,17 +343,15 @@ func buildMapDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
||||
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
|
||||
|
||||
func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
|
||||
if v.CanInterface() {
|
||||
if m, ok := v.Interface().(Marshaler); ok {
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return &EncodeError{Path: joinKey(ctx, name), Err: err}
|
||||
}
|
||||
if mv == nil {
|
||||
return &EncodeError{Path: joinKey(ctx, name), Err: errNilMarshalTOML}
|
||||
}
|
||||
v = reflect.ValueOf(mv)
|
||||
if m, ok := marshalerOf(v); ok {
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return &EncodeError{Path: joinKey(ctx, name), Err: err}
|
||||
}
|
||||
if mv == nil {
|
||||
return &EncodeError{Path: joinKey(ctx, name), Err: errNilMarshalTOML}
|
||||
}
|
||||
v = reflect.ValueOf(mv)
|
||||
}
|
||||
// A type that renders itself as text becomes a TOML string, whether it is
|
||||
// a scalar kind or a struct.
|
||||
@@ -431,12 +430,29 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
||||
return doc.appendScalar(name, []any{}, ctx)
|
||||
}
|
||||
|
||||
// Every element is resolved through MarshalTOML first, so an element that
|
||||
// renders itself as a scalar, a table or a value array is classified by
|
||||
// what it produces rather than by its Go kind, and its method runs once.
|
||||
elems := make([]reflect.Value, n)
|
||||
for i := range n {
|
||||
if i%ctxCheckInterval == 0 {
|
||||
if err := doc.checkCtx(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ev, err := resolveElement(v.Index(i), fmt.Sprintf("%s[%d]", joinKey(ctx, name), i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
elems[i] = ev
|
||||
}
|
||||
|
||||
// An array keeps the [[header]] form only when every element is a table.
|
||||
// TOML lets one array mix tables with scalars, and that mix renders as a
|
||||
// value array with the table elements written inline.
|
||||
allTables := true
|
||||
for i := range n {
|
||||
if !isTableElementValue(v.Index(i)) {
|
||||
for _, ev := range elems {
|
||||
if !ev.IsValid() || !isTableElementValue(ev) {
|
||||
allTables = false
|
||||
break
|
||||
}
|
||||
@@ -450,16 +466,12 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
||||
}
|
||||
if allTables {
|
||||
subs := make([]*tomlDoc, n)
|
||||
for i := range n {
|
||||
for i, ev := range elems {
|
||||
if i%ctxCheckInterval == 0 {
|
||||
if err := doc.checkCtx(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ev := followPtr(v.Index(i))
|
||||
if !ev.IsValid() {
|
||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errors.New("nil element")}
|
||||
}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
switch ev.Kind() {
|
||||
case reflect.Struct:
|
||||
@@ -485,29 +497,12 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
||||
// Value array. Table elements normalise to map[string]any and the emitter
|
||||
// writes them as inline tables.
|
||||
items := make([]any, n)
|
||||
for i := range n {
|
||||
for i, ev := range elems {
|
||||
if i%ctxCheckInterval == 0 {
|
||||
if err := doc.checkCtx(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ev := followPtr(v.Index(i))
|
||||
if !ev.IsValid() {
|
||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errors.New("nil element")}
|
||||
}
|
||||
if ev.CanInterface() {
|
||||
if m, ok := ev.Interface().(Marshaler); ok {
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: err}
|
||||
}
|
||||
if mv == nil {
|
||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errNilMarshalTOML}
|
||||
}
|
||||
ev = reflect.ValueOf(mv)
|
||||
ev = followPtr(ev)
|
||||
}
|
||||
}
|
||||
val, err := normaliseValue(ev)
|
||||
if err != nil {
|
||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: err}
|
||||
@@ -517,6 +512,50 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
||||
return doc.appendScalar(name, items, ctx)
|
||||
}
|
||||
|
||||
// marshalerOf finds the Marshaler a value carries: on the value itself, or on
|
||||
// its address, so a pointer-receiver MarshalTOML is found on an addressable
|
||||
// struct field or slice element, exactly as textMarshalerOf finds MarshalText.
|
||||
func marshalerOf(v reflect.Value) (Marshaler, bool) {
|
||||
if !v.CanInterface() {
|
||||
return nil, false
|
||||
}
|
||||
if m, ok := v.Interface().(Marshaler); ok {
|
||||
return m, true
|
||||
}
|
||||
if v.CanAddr() {
|
||||
if m, ok := v.Addr().Interface().(Marshaler); ok {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// resolveElement looks through pointers and runs MarshalTOML, so an array
|
||||
// element is classified by what its method produces. path names the element,
|
||||
// for the errors the method can raise.
|
||||
func resolveElement(v reflect.Value, path string) (reflect.Value, error) {
|
||||
ev := followPtr(v)
|
||||
if !ev.IsValid() {
|
||||
return ev, &EncodeError{Path: path, Err: errors.New("nil element")}
|
||||
}
|
||||
m, ok := marshalerOf(ev)
|
||||
if !ok {
|
||||
return ev, nil
|
||||
}
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return reflect.Value{}, &EncodeError{Path: path, Err: err}
|
||||
}
|
||||
if mv == nil {
|
||||
return reflect.Value{}, &EncodeError{Path: path, Err: errNilMarshalTOML}
|
||||
}
|
||||
ev = followPtr(reflect.ValueOf(mv))
|
||||
if !ev.IsValid() {
|
||||
return ev, &EncodeError{Path: path, Err: errors.New("nil element")}
|
||||
}
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// normaliseValue converts a reflect.Value into one of the canonical scalar or
|
||||
// nested-array representations the emitter understands. Slices and arrays are
|
||||
// recursively normalised so that nested arrays (e.g. [][]int) work.
|
||||
@@ -528,20 +567,25 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
if v.Kind() == reflect.Interface {
|
||||
return nil, fmt.Errorf("cannot encode nil value")
|
||||
}
|
||||
if v.CanInterface() {
|
||||
if m, ok := v.Interface().(Marshaler); ok {
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mv == nil {
|
||||
return nil, errNilMarshalTOML
|
||||
}
|
||||
return mv, nil
|
||||
if m, ok := marshalerOf(v); ok {
|
||||
mv, err := m.MarshalTOML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mv == nil {
|
||||
return nil, errNilMarshalTOML
|
||||
}
|
||||
// The result is normalised like any other value, so a method may return
|
||||
// a duration, a defined type or another Marshaler. A result of the
|
||||
// receiver's own type is written as it is, because recursing into it
|
||||
// would never end.
|
||||
if rv := reflect.ValueOf(mv); rv.Type() != v.Type() {
|
||||
return normaliseValue(rv)
|
||||
}
|
||||
return mv, nil
|
||||
}
|
||||
// The datetime structs are TOML scalars; the emitter renders each of them.
|
||||
if t := v.Type(); t == timeGoType || isLocalDateType(t) {
|
||||
if isScalarStruct(v.Type()) {
|
||||
return v.Interface(), nil
|
||||
}
|
||||
// TOML has no duration type, so a duration goes out in its canonical Go
|
||||
@@ -622,24 +666,24 @@ func followPtr(v reflect.Value) reflect.Value {
|
||||
}
|
||||
|
||||
// isScalarStruct reports whether t is a struct type that the encoder treats
|
||||
// as a TOML scalar (time.Time, LocalDateTime, LocalDate, LocalTime).
|
||||
// as a TOML scalar: time.Time and the four date-time wrappers.
|
||||
func isScalarStruct(t reflect.Type) bool {
|
||||
return t == timeGoType || isLocalDateType(t)
|
||||
return t == timeGoType || t == offsetDateTimeType || isLocalDateType(t)
|
||||
}
|
||||
|
||||
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.
|
||||
// isDateTimeType reports whether t is one of the 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)
|
||||
return isScalarStruct(t)
|
||||
}
|
||||
|
||||
// isTextMarshalerType reports whether t or *t implements
|
||||
@@ -945,6 +989,9 @@ func (e *encoder) writeValue(val any) error {
|
||||
case time.Time:
|
||||
e.buf.WriteString(offsetString(v))
|
||||
return nil
|
||||
case OffsetDateTime:
|
||||
e.buf.WriteString(v.String())
|
||||
return nil
|
||||
case LocalDateTime:
|
||||
e.buf.WriteString(v.String())
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user