Skip to content

Conversation

@Akkadius
Copy link
Member

@Akkadius Akkadius commented May 31, 2025

This PR enhances godump to fully support introspection of private struct fields, including:

  • Support for unexported fmt.Stringer values via forceExported() and asStringer() logic
  • Automatic pointer wrapping in writeDump() to ensure fields are addressable when passed by value
  • Cleaner handling of zero and nil values, with <invalid> safely rendered for uninitialized fields
  • Redundant time.Time special-case removed, now universally handled by generalized Stringer logic
  • No more panic fallbacks — all access is guarded, safe, and cleanly formatted

Example:

package main

import (
	"errors"
	"fmt"
	"test/internal/godump"
	"time"
)

type FriendlyDuration time.Duration

const maxVal = int64(time.Hour * 24)

var ErrOutOfBoundsDuration error = errors.New("FriendlyDuration cannot hold more than 23:59:59")

func NewFriendlyDurationFromString(v string) (_ FriendlyDuration, err error) {
	var td time.Duration
	td, err = parseValue(v)
	if err != nil {
		return 0, err
	}

	if int64(td) < maxVal {
		return FriendlyDuration(td), nil
	}

	return 0, ErrOutOfBoundsDuration
}

func (fd FriendlyDuration) MarshalText() (_ []byte, err error) {
	return []byte(fd.String()), nil
}

func (fd *FriendlyDuration) UnmarshalText(text []byte) (err error) {
	if len(text) < 1 {
		return fmt.Errorf("invalid text")
	}

	var td time.Duration
	td, err = parseValue(string(text))
	if err == nil {
		*fd = FriendlyDuration(td)
	}
	return err
}

func (fd FriendlyDuration) String() string {
	var td time.Duration = time.Duration(fd)
	return fmt.Sprintf("%02d:%02d:%02d", int(td.Hours()), int(td.Minutes())%60, int(td.Seconds())%60)
}

func (fd FriendlyDuration) Duration() time.Duration {
	return time.Duration(fd)
}

func parseValue(v string) (td time.Duration, err error) {
	var (
		h int64
		m int64
		s int64
		i int
	)
	i, err = fmt.Sscanf(v, "%d:%d:%d", &h, &m, &s)
	if i == 3 && err == nil {
		td = time.Hour*time.Duration(h) + time.Minute*time.Duration(m) + time.Second*time.Duration(s)
		if int64(td) > maxVal {
			err = ErrOutOfBoundsDuration
		}
	}

	return td, err
}

func main() {
	type SomeStruct struct {
		Name string
		Dur  time.Duration
		fd   FriendlyDuration
		Fd   FriendlyDuration
	}
	fd, err := NewFriendlyDurationFromString("0:0:0")
	if err != nil {
		fmt.Println(err)
	}

	ss := SomeStruct{
		Name: "Test",
		Dur:  time.Duration(time.Hour*23 + time.Minute*59 + time.Second*59),
		fd:   fd,
		Fd:   FriendlyDuration(time.Hour*23 + time.Minute*59 + time.Second*59),
	}

	// Pretty-print to stdout
	godump.Dump(ss)
	godump.Dump(fd)
	godump.Dump(NewFriendlyDurationFromString("20:0:0"))
}

Now outputs

image

Another test

package main

import (
	"fmt"
	"test/internal/godump"
	"time"
)

type FriendlyDuration time.Duration

func (fd FriendlyDuration) String() string {
	td := time.Duration(fd)
	return fmt.Sprintf("%02d:%02d:%02d", int(td.Hours()), int(td.Minutes())%60, int(td.Seconds())%60)
}

type Inner struct {
	ID    int
	Notes []string
}

type Ref struct {
	Self *Ref
}

type Everything struct {
	String        string
	Bool          bool
	Int           int
	Float         float64
	Time          time.Time
	Duration      time.Duration
	Friendly      FriendlyDuration
	PtrString     *string
	PtrDuration   *time.Duration
	SliceInts     []int
	ArrayStrings  [2]string
	MapValues     map[string]int
	Nested        Inner
	NestedPtr     *Inner
	Interface     any
	Recursive     *Ref
	privateField  string
	privateStruct Inner
}

func main() {
	now := time.Now()
	ptrStr := "Hello"
	dur := time.Minute * 20

	val := Everything{
		String:        "test",
		Bool:          true,
		Int:           42,
		Float:         3.1415,
		Time:          now,
		Duration:      dur,
		Friendly:      FriendlyDuration(dur),
		PtrString:     &ptrStr,
		PtrDuration:   &dur,
		SliceInts:     []int{1, 2, 3},
		ArrayStrings:  [2]string{"foo", "bar"},
		MapValues:     map[string]int{"a": 1, "b": 2},
		Nested:        Inner{ID: 10, Notes: []string{"alpha", "beta"}},
		NestedPtr:     &Inner{ID: 99, Notes: []string{"x", "y"}},
		Interface:     map[string]bool{"ok": true},
		Recursive:     &Ref{},
		privateField:  "should show",
		privateStruct: Inner{ID: 5, Notes: []string{"private"}},
	}
	val.Recursive.Self = val.Recursive // cycle

	godump.Dump(val)
}

demo-terminal-2

@Akkadius Akkadius merged commit c23242c into main May 31, 2025
1 check passed
@Akkadius Akkadius deleted the akkadius/better-type-handling branch May 31, 2025 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants