Repository navigation
Releases: wrfly/ecp
Release list
v0.3.1
Bug fix release. Three fixes, no new features, no API change.
A separator you chose was silently replaced
Regression from v0.3.0. Making the default separator collapse repeats was implemented as "route the value through strings.Fields whenever the separator looks like whitespace", which threw away a separator you deliberately set:
e := ecp.New()
e.Advance.SplitChar = "\t"
// value "a b\tc d"
// v0.2.5: ["a b" "c d"] correct
// v0.3.0: ["a" "b" "c" "d"] wrong
// v0.3.1: ["a b" "c d"] correct againOnly the default separator collapses repeats now, and it does so by dropping empty elements instead of re-splitting — so a tab inside an element is no longer treated as a separator either.
An optional section explicitly set to zero stayed nil
Pointer sections, new in v0.3.0, were allocated when the filled-in value came out non-zero. That cannot tell "nothing was said about this section" from "every value in it is zero":
P_PORT=0 -> cfg.P == nil // explicitly asked for, silently dropped
It also left List and Get disagreeing: List advertised P_PORT, and Get answered key not found for it.
The walk now tracks whether a field was actually assigned, from the environment or from a default tag, and allocates on that. This matches what a scalar pointer has always done — *int with default:"0" gives you &0, not nil.
A section nothing was said about still stays nil.
Errors blamed the wrong part of the value
Present in every earlier version. A value that merely contained an e was reported as a failure to parse the fragment after it:
| value | before | after |
|---|---|---|
not-a-number |
strconv.Atoi: parsing "r" |
strconv.ParseInt: parsing "not-a-number" |
hello |
strconv.Atoi: parsing "llo" |
strconv.ParseInt: parsing "hello" |
A non-numeric exponent means the value is not scientific notation at all, so it is reported whole. 1e, 1e1e1e and 1e-3 still error, and errors.As still unwraps to *strconv.NumError.
Upgrading
Drop-in from v0.3.0. The only behaviour that changes is the three bugs above.
v0.3.0
Panics
Values were built with a hard-coded concrete type and assigned to the field, which panics as soon as the field is not exactly that type — both reachable from an ordinary environment value:
type Level int
type Conf struct{ L []Level } // reflect.Set: []int is not assignable to []Level
type Conf struct{ D *time.Duration } // reflect.Set: *int64 is not assignable to *time.DurationValues are now built through the field's own reflect.Type, so named types behave like their underlying kind. *time.Duration also gains the "10s" syntax that time.Duration already had.
Fixes
| before | after | |
|---|---|---|
PORT=1e1000000 |
~100s building a 1MB string | rejected immediately |
1.5e3 |
unparsable "1.5000" |
1500 |
yaml:"-" / json:"-" / env:"-" |
honoured by List, ignored by Parse |
skipped by both |
Parse(config) without & |
nil error, nothing filled |
error |
*SubStruct section |
never walked into, bogus key in List |
optional section, allocated only when set, cycle-guarded |
default:"false" on a bool |
overwrote a caller-set true |
kept, like every other kind |
map / array fields |
silently ignored | reported, and no longer listed |
"a b" (default separator) |
3 elements, numeric slices fail | 2 elements |
List quoting |
A="a "b" c" |
strconv.Quote |
Get |
no prefix, pointer-only config, missed named types | prefix param, works read-only, kind-based |
| parse errors | %s |
%w, so errors.Is/As work |
| parser type | unexported *ecp |
exported *ECP |
Upgrading
Behaviour changes on purpose: Parse without a pointer now errors, --tagged fields are no longer filled, a caller-set bool survives default:"false", and unsupported kinds error instead of being ignored. The exported type and the new Get prefix argument are source-compatible.
Internals
One setValue helper replaces the fifteen near-identical branches of parseSlice and all of parsePointer (parse.go: 291 → 78 lines). Coverage 91.5% → 94.2%; CI now runs vet, -race and gofmt over Go 1.19/1.21/1.23/stable.
v0.2.5
Bug-fix release (PR #23): edge-case values that previously produced silent wrong values, errors, or panics.
Fixes
intfields no longer accept duration syntax (e.g.10sused to silently set the value to 1e10 nanoseconds); onlytime.Durationfields are parsed as durationsint/uint/floatfields and pointers are parsed with their bit size, so out-of-range values error out instead of being silently truncated (e.g.int8field with1000used to become-24)[]int64with duration-like values and[]time.Durationwith plain numbers no longer panic inreflect.Value.Set*[]Tpointer-to-slice fields now parse correctly (previously failed with "field is not addressable")- environment values now overwrite non-nil pointer fields, matching the documented behavior
parseScientificrejects negative exponents (e.g.1e-3used to silently become1) and handles comma+exponent combos like1,000e3Parse(&c)wherecis already a pointer no longer panics; nil or non-struct configs return an error instead of panicking,Listreturns an empty list
Tests
- All existing tests pass; 13 regression tests added in
bugfix_test.go