Support Unmarshaler interface for tables and array tables (#1027)
Fixes #873 Extend the unstable.Unmarshaler interface support to work with tables and array tables, not just single values. When a type implementing unstable.Unmarshaler is the target of a table (e.g., [table] or [[array]]), the UnmarshalTOML method receives a synthetic InlineTable node containing all the key-value pairs belonging to that table. Key changes: - Add handleKeyValuesUnmarshaler to collect and process table content - Add copyExpressionNodes to deep-copy AST nodes for synthetic tables - Add helper functions in unstable/ast.go for node manipulation - Update documentation for EnableUnmarshalerInterface - Add comprehensive tests for table and array table unmarshaling * Implement bytes-based Unmarshaler interface for tables and arrays (#873) This change brings back support for the unstable.Unmarshaler interface for tables and array tables, addressing issue #873. Key changes: - Changed UnmarshalTOML signature from (*Node) to ([]byte) to provide raw TOML bytes instead of AST nodes - Added RawMessage type (similar to json.RawMessage) for capturing raw TOML bytes for later processing - Updated handleKeyValuesUnmarshaler to reconstruct key-value lines from the parsed keys and raw value bytes - Added support for slice types implementing Unmarshaler (e.g., RawMessage) - Removed unused AST helper functions from unstable/ast.go The bytes-based interface allows users to: - Get raw TOML bytes for custom parsing - Delay TOML decoding using RawMessage - Implement custom unmarshaling logic for complex types Tests added for: - Table unmarshaler with various scenarios - Array table unmarshaler - Split tables (same parent defined in multiple places) - RawMessage usage - Nested tables and mixed regular fields * Fix lint issues and improve test coverage for Unmarshaler interface - Apply De Morgan's law in keyNeedsQuoting to satisfy staticcheck QF1001 - Remove unused splitTableUnmarshaler type from test - Fix unused parameter lint warning in errorUnmarshaler873 - Add test for quoted keys that need special handling - Add test for error propagation from UnmarshalTOML - Update customTable873 parser to handle quoted keys properly Coverage improved: - handleKeyValuesUnmarshaler: 80.0% -> 93.3% - keyNeedsQuoting: 66.7% -> 83.3% - Overall main package: 97.2% -> 97.5% * Add test for dotted keys to improve coverage Add TestIssue873_DottedKeys to test dotted key handling (e.g., sub.key = value) in the Unmarshaler interface. This improves coverage for handleKeyValuesUnmarshaler from 93.3% to 96.7%. * Add double pointer test to achieve 100% coverage for handleKeyValues Add TestIssue873_DoublePointerUnmarshaler to test pointer-to-pointer to Unmarshaler types. This covers the pointer dereferencing loop in handleKeyValues, bringing its coverage from 88% to 100%. Total coverage: 97.4% * Add Example tests and fix raw value extraction for boolean types Add two godoc Example tests: - ExampleDecoder_EnableUnmarshalerInterface_dynamicConfig: shows dynamic unmarshaling based on a type field - ExampleDecoder_EnableUnmarshalerInterface_rawMessage: demonstrates RawMessage usage for deferred parsing Fix handleKeyValuesUnmarshaler to handle values where Raw.Length == 0 (like boolean types) by using value.Data as fallback. * Preserve original formatting in Unmarshaler by using raw byte ranges Instead of reconstructing key-value lines from parsed components, now uses the original raw bytes from the document. This preserves: - Whitespace around '=' (e.g., "key = value") - String quoting style (basic vs literal) - Number formats (hex, octal, binary) - Inline table formatting Changes: - Add Raw range tracking to KeyValue expressions in parseKeyval - Update handleKeyValuesUnmarshaler to use expr.Raw directly - Remove keyNeedsQuoting helper (no longer needed) - Add TestIssue873_FormattingPreservation test - Update expected output in ExampleParser_comments * Prevent test matrix from canceling on first failure Add fail-fast: false to the test workflow strategy so that all OS/Go version combinations continue running even if one fails. This provides better visibility into which specific combinations have issues. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+85
-15
@@ -56,13 +56,18 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
||||
|
||||
// EnableUnmarshalerInterface allows to enable unmarshaler interface.
|
||||
//
|
||||
// With this feature enabled, types implementing the unstable/Unmarshaler
|
||||
// With this feature enabled, types implementing the unstable.Unmarshaler
|
||||
// interface can be decoded from any structure of the document. It allows types
|
||||
// that don't have a straightforward TOML representation to provide their own
|
||||
// decoding logic.
|
||||
//
|
||||
// Currently, types can only decode from a single value. Tables and array tables
|
||||
// are not supported.
|
||||
// The UnmarshalTOML method receives raw TOML bytes:
|
||||
// - For single values: the raw value bytes (e.g., `"hello"` for a string)
|
||||
// - For tables: all key-value lines belonging to that table
|
||||
// - For inline tables/arrays: the raw bytes of the inline structure
|
||||
//
|
||||
// The unstable.RawMessage type can be used to capture raw TOML bytes for
|
||||
// later processing, similar to json.RawMessage.
|
||||
//
|
||||
// *Unstable:* This method does not follow the compatibility guarantees of
|
||||
// semver. It can be changed or removed without a new major version being
|
||||
@@ -599,18 +604,28 @@ func (d *decoder) handleArrayTablePart(key unstable.Iterator, v reflect.Value) (
|
||||
// cannot handle it.
|
||||
func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.Value, error) {
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.Len() == 0 {
|
||||
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
|
||||
// For non-empty slices, work with the last element
|
||||
if v.Len() > 0 {
|
||||
elem := v.Index(v.Len() - 1)
|
||||
x, err := d.handleTable(key, elem)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if x.IsValid() {
|
||||
elem.Set(x)
|
||||
}
|
||||
return reflect.Value{}, nil
|
||||
}
|
||||
elem := v.Index(v.Len() - 1)
|
||||
x, err := d.handleTable(key, elem)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
// Empty slice - check if it implements Unmarshaler (e.g., RawMessage)
|
||||
// and we're at the end of the key path
|
||||
if d.unmarshalerInterface && !key.Next() {
|
||||
if v.CanAddr() && v.Addr().CanInterface() {
|
||||
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
|
||||
return d.handleKeyValuesUnmarshaler(outi)
|
||||
}
|
||||
}
|
||||
}
|
||||
if x.IsValid() {
|
||||
elem.Set(x)
|
||||
}
|
||||
return reflect.Value{}, nil
|
||||
return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice")
|
||||
}
|
||||
if key.Next() {
|
||||
// Still scoping the key
|
||||
@@ -624,6 +639,24 @@ func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.V
|
||||
// Handle root expressions until the end of the document or the next
|
||||
// non-key-value.
|
||||
func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
|
||||
// Check if target implements Unmarshaler before processing key-values.
|
||||
// This allows types to handle entire tables themselves.
|
||||
if d.unmarshalerInterface {
|
||||
vv := v
|
||||
for vv.Kind() == reflect.Ptr {
|
||||
if vv.IsNil() {
|
||||
vv.Set(reflect.New(vv.Type().Elem()))
|
||||
}
|
||||
vv = vv.Elem()
|
||||
}
|
||||
if vv.CanAddr() && vv.Addr().CanInterface() {
|
||||
if outi, ok := vv.Addr().Interface().(unstable.Unmarshaler); ok {
|
||||
// Collect all key-value expressions for this table
|
||||
return d.handleKeyValuesUnmarshaler(outi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rv reflect.Value
|
||||
for d.nextExpr() {
|
||||
expr := d.expr()
|
||||
@@ -653,6 +686,41 @@ func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// handleKeyValuesUnmarshaler collects all key-value expressions for a table
|
||||
// and passes them to the Unmarshaler as raw TOML bytes.
|
||||
func (d *decoder) handleKeyValuesUnmarshaler(u unstable.Unmarshaler) (reflect.Value, error) {
|
||||
// Collect raw bytes from all key-value expressions for this table.
|
||||
// We use the Raw field on each KeyValue expression to preserve the
|
||||
// original formatting (whitespace, quoting style, etc.) from the document.
|
||||
var buf []byte
|
||||
|
||||
for d.nextExpr() {
|
||||
expr := d.expr()
|
||||
if expr.Kind != unstable.KeyValue {
|
||||
d.stashExpr()
|
||||
break
|
||||
}
|
||||
|
||||
_, err := d.seen.CheckExpression(expr)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
|
||||
// Use the raw bytes from the original document to preserve formatting
|
||||
if expr.Raw.Length > 0 {
|
||||
raw := d.p.Raw(expr.Raw)
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
buf = append(buf, '\n')
|
||||
}
|
||||
|
||||
if err := u.UnmarshalTOML(buf); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
|
||||
return reflect.Value{}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
handlerFn func(key unstable.Iterator, v reflect.Value) (reflect.Value, error)
|
||||
valueMakerFn func() reflect.Value
|
||||
@@ -697,7 +765,8 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error {
|
||||
if d.unmarshalerInterface {
|
||||
if v.CanAddr() && v.Addr().CanInterface() {
|
||||
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
|
||||
return outi.UnmarshalTOML(value)
|
||||
// Pass raw bytes from the original document
|
||||
return outi.UnmarshalTOML(d.p.Raw(value.Raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1201,7 +1270,8 @@ func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node
|
||||
if d.unmarshalerInterface {
|
||||
if v.CanAddr() && v.Addr().CanInterface() {
|
||||
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
|
||||
return reflect.Value{}, outi.UnmarshalTOML(value)
|
||||
// Pass raw bytes from the original document
|
||||
return reflect.Value{}, outi.UnmarshalTOML(d.p.Raw(value.Raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user