v2.1.4: Internal package refactoring#17
Conversation
There was a problem hiding this comment.
Pull request overview
This PR (v2.1.4) refactors the cast package by relocating the conversion implementation behind an internal/cast/ boundary while keeping To / ToE and the public type aliases in the root package. It removes the now-redundant ToStruct / ToStructE public APIs (callers use To / ToE for struct targets), fixes the time.Time ↔ *big.Int round-trip to use UnixNano(), corrects several documentation conflicts (Options table, footnotes for time/bigInt, string→bool parse semantics, string→map JSON-decode path), and substantially expands tests and runnable godoc examples (24 new examples, 33 new sub-tests covering previously-untested error cells of the conversion table).
Changes:
- Move all converter sources and tests into
internal/cast/; rootto.go/to.type.gore-export non-generic types via Go aliases and dispatch tointernal.*(withmakeChan/makeFuncrewritten on top ofCastToTypeto break the import cycle). - Drop
ToStruct[T]/ToStructE[T]from the public API; rename happy-path examples toExampleTo_*and reserveExampleToE_*for matching error examples. - Fix
time.Time → *big.Intto useUnixNano(), expand flag/option docs (FORMAT row, propagation section, multi-line constant comments), and document the wrong-typedDEFAULTearly-error behavior plus the empty-stringparseTimeStringedge case.
Reviewed changes
Copilot reviewed 42 out of 64 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| to.go | Dispatch switch routes to internal.* converters; pointer/func/chan paths use CastToType and final assertion in main. |
| to.type.go | Reduced to type aliases and constant re-exports of the new internal/cast definitions. |
| to.func.go | Removed; replaced by internal/cast/to.func.go reflection-based implementation. |
| internal/cast/to.type.go | New canonical home for Flag, Op, flag constants (with multi-line docs), Ops, ParseOps, error sentinels. |
| internal/cast/to.func.go, to.chan.go | Rewritten to use CastToType / reflect.MakeFunc / reflect.MakeChan, eliminating the per-element generic switch. |
| internal/cast/to.big.go | time.Time → *big.Int now uses UnixNano() for lossless round-trip. |
| internal/cast/util.reflect.go, util.decode.go | Exported NamedConverters, IsScalarKind, CastToType, decode helpers. |
| internal/cast/to.{bool,int,float,complex,string,slice,map,struct,time,duration,net,url,regexp}.go | Moved/renamed (lowercase → exported) converters; behavior preserved. |
| internal/cast/*_test.go, to.types_test.go, test.options_test.go, test.panic_test.go | Tests relocated to internal package; new error/edge-case sub-tests added. |
| internal/cast/my._test.go | New TestMyTest probe-style test with non-conventional filename. |
| test.examples_test.go | Stays in root; renamed happy-path examples to ExampleTo_*, added 24 new examples and matching ExampleToE_* error variants. |
| README.md | New Options table columns, FORMAT row, global-propagation subsection, footnote/IMPORTANT-callout corrections, struct-hydration example uses cast.To. |
| CHANGELOG.md | Adds v2.1.4 entry summarizing the refactor and fixes. |
Comments suppressed due to low confidence (1)
internal/cast/my._test.go:33
- This new test file appears to be developer scratch/probe code that was accidentally committed. The filename
my._test.goand the test nameTestMyTestare not descriptive and do not follow the naming conventions used by every other test file in this directory (to.<type>_test.gowithTest<Behavior>names). Consider either deleting this file or, if the anonymous-struct hydration coverage is valuable, moving the assertion intoto.struct_test.gounder a properly named test.
package cast_test
import (
"testing"
"github.com/bdlm/cast/v2"
)
func TestMyTest(t *testing.T) {
t.Run("Casting to anonymous struct", func(t *testing.T) {
result, err := cast.ToE[struct {
Foo string
Bar int
}](map[string]any{
"Foo": "hello",
"Bar": 42,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != struct {
Foo string
Bar int
}{
Foo: "hello",
Bar: 42,
} {
t.Errorf("unexpected result: %v", result)
}
t.Logf("successfully cast to anonymous struct: %+v", result)
})
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Internal package refactoring,
ToStruct/ToStructEremoval, documentation corrections, flag-behavior fixes, and substantially expanded test and example coverage.Removed
ToStruct[T]— public API removed. Usecast.To[T]instead; struct hydration is dispatched automatically whenTis a struct type.ToStructE[T]— public API removed. Usecast.ToE[T]instead. The implementation moved tointernal/castasToStructE.cast.ToStruct[T]/cast.ToStructE[T]replaced withcast.To[T]/cast.ToE[T].Changed
internal/cast/packageImplementation moved out of the public package surface:
to.goandto.type.goremain inpackage castand exposeTo,ToE, and the public type API.to.bool.go,to.int.go,to.float.go,to.complex.go,to.string.go,to.slice.go,to.map.go,to.chan.go,to.func.go,to.struct.go,to.time.go,to.duration.go,to.big.go,to.net.go,to.url.go,to.regexp.go,util.decode.go,util.reflect.go) moved tointernal/cast/.*_test.gofiles moved tointernal/cast/excepttest.examples_test.go, which stays in main to validate the public API surface via godoc examples.Flag,Op, flag constants, error vars) are defined in the internal package; main package re-exports them via Go type aliases. Generic types (Func,Types,Tbase,Tslice,Tchan,Tmap) stay in main because Go 1.21 does not support generic type aliases.opsstruct renamed toOpswith exported fields so the main package can construct it viainternal.ParseOps.makeChan/makeFuncrewritten to use reflection-based casting (CastToType) instead of recursively calling the publicToE, breaking the otherwise-circular import path between main and internal.ToChanandToFuncare non-generic in the internal package; main'sToEperforms the final type assertion (withreflect.Convertfallback) to produce the user's namedchan TorFunc[T].Options documentation
FORMATrow to the README Options table (previously absent).ABS,JSON,LENGTH,UNIQUE_VALUES, andFORMATpropagating into nested casts.internal/cast/to.type.gonow carry full multi-line doc comments stating their value type, scope, applicability, and propagation behavior.Example function naming convention
Happy-path
ExampleToE_*functions that never showed an error have been renamed toExampleTo_*and switched tocast.To; the freed-upExampleToE_*names now hold corresponding error examples. EveryExampleTo_*has a matchingExampleToE_*error example. Total example functions: 73.Added
Test coverage for the conversion table
33 new sub-tests cover previously-untested
✗cells of the conversion table: scalar → map errors, scalar → struct errors, slice/map/struct → bool|complex errors, map → int/uint/float errors, struct → uint error, and the documented[]byte/[]rune→map[K]Vsuccess path. New ABS coverage added for negativefloat64sources and negative-float strings touinttargets.Godoc example functions
24 new example functions covering previously-undocumented type categories:
time.Time(string, int, float,FORMAT),time.Duration(string, int),net.IP(string, packeduint32),*url.URL,*regexp.Regexp,*big.Int(decimal, hex),*big.Float,bool,complex64/complex128,errorandfmt.Stringerinterface targets,DECODE(scalar and slice),[]byte→string, JSON-string → map, nestedchan []intandFunc[chan int].Fixed
time.Time↔*big.Intround-triptime.Time → *big.Intnow usesval.UnixNano()(wasval.Unix()).*big.Int → time.Timealready used nanoseconds, so the round-trip is now lossless for times withinint64range. Three test cases inTestTimeToBigConversionswere updated accordingly.Documentation conflicts
*big.Int → time.Timewas Unix seconds; corrected to nanoseconds.*big.Float → time.Timeremains documented as Unix seconds with fractional precision.string → map[K]Vwas✗, but JSON-object/array strings auto-decode; corrected to~ⁿwith a new footnote.string → boolsaid onlystrconv.ParseBoolvariants are accepted; the integer-parse fallback path was undocumented and is now described ("-1"→ true,"0.1"→ false becausefloor(0.1) = 0, etc.).int*→time.Timeclaimed Unix seconds; corrected to nanoseconds. ThetoTimedocstring previously said "Unix seconds" for integer sources; fixed to "Unix nanoseconds".Flag documentation
FORMATconstant comment said "time/duration parsing"; onlytime.Timereadsops.FormatVal. Corrected.PRIVATEconstant comment said "include unexported struct fields in map output" — too narrow. Corrected to describe both directions (struct→map source-field reading and map/struct→struct target hydration).ABSrow said "negative signed inputs";ABSalso applies to negativefloat32/float64sources and numeric strings that parse to negative floats. Description, example block, and IMPORTANT callout updated.parseTimeStringundocumented edge caseWhen
FORMATis not set,parseTimeStringfalls through totime.Parse("", str)after the 19-format loop. The empty string""is a valid source that returnstime.Time{}becausetime.Parse("", "")succeeds. Now documented in both the function godoc and the README footnote ᶠ.Wrong-typed
DEFAULTerrors immediatelyEvery converter type-asserts the
DEFAULTvalue at the top, before inspecting the input. Passing a wrong type returns an error immediately even for inputs that would otherwise convert successfully. Now documented on theOpstruct, theDEFAULTconstant, and the README Options table.Documentation (README.md)
IMPORTANTcallout updated to describe the actualstring → booltwo-step parse behavior and to mention float-source applicability forABS.int64(1_713_787_200_000_000_000)for 2024-04-22) instead of a value that only made sense as Unix seconds.#### Structsrewritten to usecast.To/cast.ToEafter the removal ofToStruct/ToStructE.