Skip to content

feat: support encoding.TextUnmarshaler fields - #18

Open
JeremyLoy wants to merge 5 commits into
feat/typed-errorsfrom
feat/text-unmarshaler
Open

JeremyLoy wants to merge 5 commits into
feat/typed-errorsfrom
feat/text-unmarshaler

Conversation

@JeremyLoy

Copy link
Copy Markdown
Owner

Stack 5/7. Base: #17.

Any field whose pointer implements encoding.TextUnmarshaler is now parsed with UnmarshalText as a single value. That covers time.Time, netip.Addr/Prefix, net.IP, slog.Level, big.Int and users' own types, plus slices of any of them, with no special cases.

var c struct {
	Listen   netip.Addr
	LogLevel slog.Level `config:"LOG_LEVEL"`
	Peers    []netip.Addr
}

Details

  • These struct types (time.Time, netip.Addr) are now treated as single values instead of being descended into as sub-configs.
  • These slice types (net.IP is a []byte) are no longer split on spaces.
  • UnmarshalText errors are reported as ErrInvalidValue, because they often include the input.

Edge case: a nested config struct that embeds a TextUnmarshaler (e.g. struct{ time.Time; Port int }) picks up UnmarshalText through embedding and is now treated as a single value. I expect this to be rare. Say if you want an opt-out.

Verification

Test_textUnmarshaler covers netip.Addr, []netip.Addr, net.IP, slog.Level, nested and top-level time.Time, and a custom type that returns an error. It fails without this change. Added Example_textUnmarshaler. The full suite plus vet and staticcheck pass on go1.26 and go1.27.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP


Generated by Claude Code

JeremyLoy and others added 2 commits October 12, 2026 01:27
fix: crashes on pointer/unexported fields, slice appends, error accumulation
Any field whose pointer implements encoding.TextUnmarshaler is parsed with
UnmarshalText, as a single value. This adds time.Time, netip.Addr,
netip.Prefix, net.IP, slog.Level, big.Int and user-defined types without
special cases, including slices of them.

- Such struct types (time.Time, netip.Addr) are no longer descended into
  as sub-configs.
- Such slice types (net.IP is a []byte) are no longer split on spaces.
- UnmarshalText errors are reported as ErrInvalidValue because they may
  contain the configured value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP
@JeremyLoy
JeremyLoy force-pushed the feat/text-unmarshaler branch from 76f4e10 to be168e6 Compare October 12, 2026 05:39
JeremyLoy and others added 3 commits October 12, 2026 01:41
* chore: require Go 1.26 and modernize

Go supports the two most recent releases (1.26 and 1.27), so raise the
go directive from 1.16 to 1.26.

- go fix: interface{} -> any, reflect.Ptr -> reflect.Pointer, maps.Copy,
  range-over-int, drop per-iteration `tt := tt` copies (1.22 semantics)
- strings.Cut in stringsToMap (resolves the 1.18 TODO)
- slices.Sort instead of sort.Strings
- reflect.TypeFor for the time.Duration and *url.URL checks instead of
  comparing PkgPath/Name strings
- reflect.Value.Fields iterator and StructField.IsExported in mapKeysToFields
- new(expr) replaces the func() interface{} { v := x; return &v }() test helpers
- t.Setenv / t.TempDir in Test_Api, which no longer clears the whole
  environment for later tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP

* chore: require Go 1.27

Raise the go directive to 1.27.0 so generic methods (Go 1.27) can be used.

CI: replace the oldstable/stable matrix with "minimum" (the version in
go.mod, via go-version-file) and "stable". With GOTOOLCHAIN=auto an
oldstable 1.26 job would silently download and test 1.27.0 instead, so CI
now sets GOTOOLCHAIN=local to always test the Go it installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat: return a typed *config.Error with per-field causes

To and Sub now return *config.Error, which lists a *FieldError per failed
field or source, sorted by key. Error() is unchanged, so existing output and
string comparisons keep working.

- Error.Unwrap() []error and FieldError.Unwrap() expose causes to errors.Is
  and errors.AsType: fs.ErrNotExist / *fs.PathError for files,
  strconv.ErrSyntax / strconv.ErrRange for numbers and bools,
  ErrUnsupportedType for fields that cannot be configured, and
  ErrInvalidValue otherwise.
- Causes are sanitized so they never contain the configured value, keeping
  the existing guarantee that errors are safe to log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP

* chore: require Go 1.27 and modernize (#16)

* chore: require Go 1.26 and modernize

Go supports the two most recent releases (1.26 and 1.27), so raise the
go directive from 1.16 to 1.26.

- go fix: interface{} -> any, reflect.Ptr -> reflect.Pointer, maps.Copy,
  range-over-int, drop per-iteration `tt := tt` copies (1.22 semantics)
- strings.Cut in stringsToMap (resolves the 1.18 TODO)
- slices.Sort instead of sort.Strings
- reflect.TypeFor for the time.Duration and *url.URL checks instead of
  comparing PkgPath/Name strings
- reflect.Value.Fields iterator and StructField.IsExported in mapKeysToFields
- new(expr) replaces the func() interface{} { v := x; return &v }() test helpers
- t.Setenv / t.TempDir in Test_Api, which no longer clears the whole
  environment for later tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP

* chore: require Go 1.27

Raise the go directive to 1.27.0 so generic methods (Go 1.27) can be used.

CI: replace the oldstable/stable matrix with "minimum" (the version in
go.mod, via go-version-file) and "stable". With GOTOOLCHAIN=auto an
oldstable 1.26 job would silently download and test 1.27.0 instead, so CI
now sets GOTOOLCHAIN=local to always test the Go it installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUxivLjodSESTkzRtdQ4CP

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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