Documentation
¶
Overview ¶
Package gofret converts Go values between shapes: maps into structs, structs into maps, and structs into other structs.
One entry point ¶
There is a single conversion function. What happens is decided by the destination type, not by which function you reach for:
cfg, err := gofret.To[Config](data) // map -> struct m, err := gofret.To[map[string]any](cfg) // struct -> map err := gofret.ToInto(src, &dst) // into an existing value
Because both directions run through the same engine and the same analysis of each struct type, they cannot disagree about what a tag means. That is also what makes the round trip property hold: converting a value to a map and back returns the value unchanged.
Configuration ¶
The defaults suit configuration data, which is what gofret is mostly pointed at: keys match loosely, ignoring case, '-', '_' and ' ', and scalars convert leniently, so the "8080" that an environment variable hands you reaches an int field. WithStrictKeys and WithStrictTypes turn those off.
Build a Codec when you want options. A Codec is immutable, safe for concurrent use, and caches its analysis of every struct type it sees, so make one and keep it:
c := gofret.New(
gofret.WithTagFallback("json"),
gofret.Between(netip.ParseAddr),
gofret.WithErrorUnused(),
)
Struct tags ¶
Fields are read from the `cfg` tag by default; see WithTag. The first element is the key name and the rest are options:
Field string `cfg:"name,omitempty"` - skip the field, in both directions omitempty drop the field when it holds the zero value inline merge the field's keys into the parent instead of nesting remain collect the keys that match no field, in both directions string carry the value as text deref write the pointed-to value; nil becomes the zero value keep pass the value through instead of converting it
Unknown options are ignored by default so tags can be shared with other packages. WithStrictTags rejects them instead, so a typo such as "omitemty" surfaces the first time the type is used. "squash" is accepted as an alias for "inline". The cost is paid once per type because the analysis is cached.
Conversions ¶
Between teaches a codec a type it does not already know, written as an ordinary Go function. Functions from other packages need no wrapper, and the registration needs no type arguments:
c := gofret.New(
gofret.Between(netip.ParseAddr), // string -> netip.Addr
gofret.Between(uuid.Parse), // string -> uuid.UUID
)
A registration is looked up by type rather than offered every value, so registering many of them does not make every conversion slower, and the function is never asked to decline: it is only called with the type it names.
Which of Between and BetweenOK you want is decided by the function you already have: the first takes one that can fail, the second one that cannot. Passing the wrong one does not compile, and the error names the other.
Neither has a direction of its own. Both register a pair of types, and which pair is whatever the function's argument and result say, so BetweenOK reads a value in as readily as it writes one out. Parsing is where the errors are and rendering is where they are not, so a pair of registrations often does come out as Between in and BetweenOK out, but that is a fact about parsing rather than a rule.
Each call registers one pair, so a type that has to go out and come back gets two.
Alias lends a named type such as `type Timeout time.Duration` the conversions registered for the type it is defined from, and TimeLayout registers both directions of a time.Time written in some layout of its own. Those two exist because neither states something a plain function could.
An empty interface, as found inside a map[string]any, names no type, so there is nothing to convert towards. A value with a registered text form is written in that form and anything else is carried across as it stands, which is what makes such a map writable by an encoder and readable back.
time.Duration is understood without being asked, in both directions, because its text form is one the language defines rather than one gofret invented. The same holds for encoding.TextMarshaler, encoding.TextUnmarshaler and fmt.Stringer. A number carries no unit, so it keeps the plain integer meaning time.Duration gives it.
Hooks ¶
A hook is the escape hatch for a rule a pair of types cannot express, such as "every type implementing this interface" or "every field carrying this tag option". It is offered every value, so reach for Between first.
A Hook replaces a value before the built-in conversion runs. Return ErrSkip to decline and let the next hook, or the built-in conversion, take over. Any other error aborts the conversion and reaches the caller, so a hook can report a real failure instead of silently declining:
redact := func(ctx gofret.HookCtx) (any, error) {
if !ctx.Tag.Has(gofret.OptOmitEmpty) {
return nil, gofret.ErrSkip
}
return "****", nil
}
c := gofret.New(gofret.WithHooks(redact))
The tag is the part a registration cannot reach, because the rule is about where the value sits rather than what type it is. Matching on an interface is the other, since no list of concrete pairs could name one. A rule of the form "this type becomes that type" is neither; use Between.
A type can also speak for itself by implementing ValueEncoder or ValueDecoder, and encoding.TextMarshaler, encoding.TextUnmarshaler and fmt.Stringer are honoured out of the box.
Errors ¶
Every failure is an Error carrying the dotted path of the value that caused it. Failures are collected and joined, so errors.Is and errors.AsType reach all of them.
errors.AsType picks out the first one, and Errors lists them all:
if ce, ok := errors.AsType[*gofret.Error](err); ok {
log.Printf("bad value at %s", ce.Path)
}
for _, ce := range gofret.Errors(err) {
log.Printf("%s: %v", ce.Path, ce.Err)
}
Failures are reported in destination field order rather than in the order Go happened to walk the input map, so the same input always reports the same thing. The same holds for Metadata.
See WithFailFast and WithMaxErrors to stop earlier, and Codec.ToIntoMeta to find out which keys went unused.
Example ¶
package main
import (
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Name string `cfg:"name"`
Retries int `cfg:"retries"`
}
cfg, err := gofret.To[Config](map[string]any{
"name": "service",
"retries": 3,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s %d\n", cfg.Name, cfg.Retries)
}
Output: service 3
Example (Error) ¶
package main
import (
"errors"
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Server struct {
Port int `cfg:"port"`
}
type Config struct {
Servers []Server `cfg:"servers"`
}
_, err := gofret.To[Config](map[string]any{
"servers": []any{
map[string]any{"port": 80},
map[string]any{"port": "nope"},
},
})
if ce, ok := errors.AsType[*gofret.Error](err); ok {
fmt.Println("path:", ce.Path)
}
fmt.Println("unconvertible:", errors.Is(err, gofret.ErrUnconvertible))
}
Output: path: servers[1].port unconvertible: true
Example (Hook) ¶
A hook is offered every value, so it can match on things a pair of types cannot express. Here the rule is about the struct tag rather than the type.
package main
import (
"fmt"
"reflect"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
User string `cfg:"user"`
Password string `cfg:"password,omitempty"`
}
redact := gofret.Hook(func(ctx gofret.HookCtx) (any, error) {
if !ctx.Tag.Has(gofret.OptOmitEmpty) || ctx.From.Kind() != reflect.String {
return nil, gofret.ErrSkip
}
return "****", nil
})
c := gofret.New(gofret.WithHooks(redact))
m, err := c.To[map[string]string](Config{User: "ray", Password: "hunter2"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(m["user"], m["password"])
}
Output: ray ****
Example (HookError) ¶
A hook declines with ErrSkip and fails with anything else, so a genuine problem is reported instead of quietly falling through.
package main
import (
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Port int `cfg:"port"`
}
c := gofret.New(gofret.WithHooks(func(ctx gofret.HookCtx) (any, error) {
n, ok := ctx.Data().(int)
if !ok {
return nil, gofret.ErrSkip
}
if n < 1024 {
return nil, fmt.Errorf("port %d is reserved", n)
}
return n, nil
}))
_, err := c.To[Config](map[string]any{"port": 80})
fmt.Println(err)
}
Output: gofret: port: cannot convert int to int: port 80 is reserved
Example (Inline) ¶
The `inline` option flattens a nested struct into its parent, in both directions.
package main
import (
"fmt"
"sort"
"github.com/rakunlabs/gofret"
)
func main() {
type Auth struct {
User string `cfg:"user"`
Pass string `cfg:"pass"`
}
type Config struct {
Host string `cfg:"host"`
Auth Auth `cfg:",inline"`
}
m, err := gofret.To[map[string]any](Config{
Host: "db",
Auth: Auth{User: "u", Pass: "p"},
})
if err != nil {
fmt.Println(err)
return
}
printMap(m)
}
// printMap renders a map with its keys in order, so the example output is
// stable.
func printMap(m map[string]any) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %v\n", k, m[k])
}
}
Output: host: db pass: p user: u
Example (Metadata) ¶
package main
import (
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Name string `cfg:"name"`
Missing string `cfg:"missing"`
}
var (
md gofret.Metadata
cfg Config
)
err := gofret.New().ToIntoMeta(map[string]any{
"name": "service",
"typo": true,
}, &cfg, &md)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("used: ", md.Keys)
fmt.Println("unused:", md.Unused)
fmt.Println("unset: ", md.Unset)
}
Output: used: [name] unused: [typo] unset: [missing]
Example (Options) ¶
package main
import (
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
MaxRetry int `cfg:"maxRetry"`
Name string `json:"name"`
}
// The `cfg` tag is read by default; a fallback picks up the `json` tag on
// fields that carry no `cfg` one.
c := gofret.New(gofret.WithTagFallback("json"))
// Weak typing accepts the string, and loose keys match "max_retry"
// against "maxRetry". Both are on by default.
cfg, err := c.To[Config](map[string]any{
"max_retry": "5",
"NAME": "service",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%d %s\n", cfg.MaxRetry, cfg.Name)
}
Output: 5 service
Example (Remain) ¶
The `remain` option collects the keys no field claimed, and writes them back out again, which is what keeps a round trip lossless.
package main
import (
"fmt"
"sort"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Name string `cfg:"name"`
Rest map[string]any `cfg:",remain"`
}
cfg, err := gofret.To[Config](map[string]any{
"name": "service",
"unknown": "kept",
})
if err != nil {
fmt.Println(err)
return
}
back, err := gofret.To[map[string]any](cfg)
if err != nil {
fmt.Println(err)
return
}
printMap(back)
}
// printMap renders a map with its keys in order, so the example output is
// stable.
func printMap(m map[string]any) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %v\n", k, m[k])
}
}
Output: name: service unknown: kept
Example (ToMap) ¶
The destination type decides the direction, so writing a struct out is the same call with a different type argument.
package main
import (
"fmt"
"sort"
"github.com/rakunlabs/gofret"
)
func main() {
type Server struct {
Host string `cfg:"host"`
Port int `cfg:"port"`
}
type Config struct {
Name string `cfg:"name"`
Primary Server `cfg:"primary"`
}
m, err := gofret.To[map[string]any](Config{
Name: "service",
Primary: Server{Host: "localhost", Port: 8080},
})
if err != nil {
fmt.Println(err)
return
}
printMap(m)
}
// printMap renders a map with its keys in order, so the example output is
// stable.
func printMap(m map[string]any) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %v\n", k, m[k])
}
}
Output: name: service primary: map[host:localhost port:8080]
Example (ValueEncoder) ¶
A type can decide for itself how it is written and read.
package main
import (
"fmt"
"sort"
"strings"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Name csv `cfg:"name"`
}
m, err := gofret.To[map[string]any](Config{Name: csv{"a", "b"}})
if err != nil {
fmt.Println(err)
return
}
printMap(m)
back, err := gofret.To[Config](m)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(back.Name)
}
type csv []string
func (c csv) EncodeValue() (any, error) { return strings.Join(c, ","), nil }
func (c *csv) DecodeValue(v any) error {
s, ok := v.(string)
if !ok {
return fmt.Errorf("want a string, got %T", v)
}
*c = strings.Split(s, ",")
return nil
}
// printMap renders a map with its keys in order, so the example output is
// stable.
func printMap(m map[string]any) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %v\n", k, m[k])
}
}
Output: name: a,b [a b]
Index ¶
- Constants
- Variables
- func CamelCase(s string) string
- func FoldKey(s string) string
- func KebabCase(s string) string
- func LooseKey(s string) string
- func LowerCase(s string) string
- func PascalCase(s string) string
- func SnakeCase(s string) string
- func To[T any](in any) (T, error)
- func ToInto(in, out any) error
- type Codec
- type Error
- type Hook
- type HookCtx
- type KeyFunc
- type KeyNormalizer
- type Metadata
- type Opt
- type Option
- func Alias[Named, Base any]() Option
- func Between[In, Out any](fn func(In) (Out, error)) Option
- func BetweenOK[In, Out any](fn func(In) Out) Option
- func TimeLayout(layouts ...string) Option
- func WithDerefPointers() Option
- func WithErrorUnused() Option
- func WithFailFast() Option
- func WithHooks(hooks ...Hook) Option
- func WithInlineEmbedded() Option
- func WithKeyFunc(fn KeyFunc) Option
- func WithKeyNormalizer(fn KeyNormalizer) Option
- func WithLooseKeys() Option
- func WithMaxErrors(n int) Option
- func WithOmitNil() Option
- func WithShallow() Option
- func WithStrictKeys() Option
- func WithStrictTags() Option
- func WithStrictTypes() Option
- func WithTag(tag string) Option
- func WithTagFallback(tags ...string) Option
- func WithTaggedOnly() Option
- func WithWeakTypes() Option
- func WithZeroFields() Option
- type Tag
- type ValueDecoder
- type ValueEncoder
Examples ¶
Constants ¶
const DefaultTag = "cfg"
DefaultTag is the struct tag gofret reads when WithTag is not given.
Variables ¶
var ( // ErrSkip is returned by a Hook that does not handle the value. It means // "not mine, keep going" and is never reported to the caller. Any other // error from a hook aborts the conversion. ErrSkip = errors.New("gofret: skip hook") // ErrNotPointer is returned by ToInto when out is not a non-nil pointer. ErrNotPointer = errors.New("gofret: output must be a non-nil pointer") // ErrUnconvertible means the source value cannot be represented in the // destination type. ErrUnconvertible = errors.New("gofret: unconvertible value") // ErrUnsupportedType means the destination type is one gofret cannot // write to, such as a channel. ErrUnsupportedType = errors.New("gofret: unsupported type") // ErrOverflow means the source value does not fit in the destination. ErrOverflow = errors.New("gofret: value overflows destination type") // ErrUnusedKeys is reported when WithErrorUnused is set and the input // contains keys that match no destination field. ErrUnusedKeys = errors.New("gofret: unused keys in input") // ErrInvalidTag means a struct tag carries an unknown option. ErrInvalidTag = errors.New("gofret: invalid struct tag") )
Sentinel errors. Every error produced by gofret wraps one of these, so callers can branch with errors.Is instead of matching on strings.
Functions ¶
func CamelCase ¶
CamelCase renders the name in lowerCamelCase: "MaxRetry" becomes "maxRetry", "ID" becomes "id" and "HTTPServer" becomes "httpServer".
func FoldKey ¶
FoldKey lowercases the key, which makes lookups case-insensitive while still telling separators apart. Pass it to WithKeyNormalizer to soften the default LooseKey matching without turning it off entirely.
func LooseKey ¶
LooseKey lowercases the key and removes '-', '_' and ' ', so "MaxRetry", "max_retry", "max-retry" and "max retry" all match. It is the default matcher.
func PascalCase ¶
PascalCase renders the name in UpperCamelCase: "max_retry" becomes "MaxRetry".
func SnakeCase ¶
SnakeCase renders the name in snake_case: "MaxRetry" becomes "max_retry" and "HTTPServer" becomes "http_server".
Types ¶
type Codec ¶
type Codec struct {
// contains filtered or unexported fields
}
Codec converts values between shapes according to a fixed configuration.
A Codec is immutable once built and safe for concurrent use. It caches the analysis of every struct type it sees, so reuse one instead of calling New on every conversion.
func (*Codec) To ¶
To converts in to a value of type T.
T decides what happens: converting to a struct reads a map, converting to map[string]any writes one, and converting between two structs copies field by field.
cfg, err := c.To[Config](data) // map -> struct m, err := c.To[map[string]any](cfg) // struct -> map
On error the returned value holds whatever was converted before the failure; treat it as unusable unless err is nil.
type Error ¶
type Error struct {
// Path is the dotted location of the value, for example
// "database.hosts[2].port". It is empty at the root.
Path string
// From is the type of the source value. It may be nil for a nil source.
From reflect.Type
// To is the type of the destination.
To reflect.Type
// Err is the underlying cause.
Err error
}
Error describes a single conversion failure and where it happened.
Errors are collected rather than aborting on the first failure (unless WithFailFast is set) and joined with errors.Join, so errors.Is and errors.As reach every one of them.
func Errors ¶
Errors returns every *Error in err's tree, in traversal order.
errors.AsType finds the first one. Because a conversion reports all of its failures at once, this is the way to list every bad value:
for _, ce := range gofret.Errors(err) {
log.Printf("%s: %v", ce.Path, ce.Err)
}
Example ¶
A conversion reports every failure at once, so the joined error usually holds more than one. Errors lists them all.
package main
import (
"fmt"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Port int `cfg:"port"`
Retries int `cfg:"retries"`
}
_, err := gofret.To[Config](map[string]any{
"port": "nope",
"retries": "many",
})
for _, ce := range gofret.Errors(err) {
fmt.Printf("%s: %s -> %s\n", ce.Path, ce.From, ce.To)
}
}
Output: port: string -> int retries: string -> int
type Hook ¶
Hook replaces a value before the built-in conversion runs.
Return ErrSkip to decline, which passes the value to the next hook and finally to the built-in conversion. Any other error aborts the conversion and is reported to the caller, so a hook can report a genuine failure instead of silently declining.
The returned value is converted to the destination as usual, so a hook may hand back an intermediate representation rather than the final type.
type HookCtx ¶
type HookCtx struct {
// From is the type of the source value. It is nil when the source is
// an untyped nil.
From reflect.Type
// To is the type of the destination. When writing into a map[string]any
// or an any field this is the empty interface, not the concrete type of
// the source.
To reflect.Type
// Value is the source value.
Value reflect.Value
// Tag is the parsed struct tag of the field being converted. It is the
// zero Tag when the value is not a struct field.
Tag Tag
// contains filtered or unexported fields
}
HookCtx describes the value a hook is being offered.
The cheap facts are fields; anything that costs an allocation is a method, so a hook that only inspects types and declines pays nothing.
type KeyFunc ¶
KeyFunc derives a map key from a Go field name. It is only consulted for fields whose struct tag does not spell out a name.
See WithKeyFunc.
type KeyNormalizer ¶
KeyNormalizer folds a key into a canonical form used for matching.
Both the destination field names and the incoming keys are passed through the normalizer; two keys match when their normalized forms are equal. That makes matching precomputable, so lookups stay O(1) no matter how expensive the normalizer is.
See WithKeyNormalizer.
type Metadata ¶
type Metadata struct {
// Keys holds the dotted path of every destination field that was
// written, for example "database.host".
Keys []string
// Unused holds the dotted path of every input key that matched no
// destination field. It is the usual way to warn about typos in
// configuration files.
Unused []string
// Unset holds the dotted path of every destination field that no input
// key supplied.
Unset []string
}
Metadata records what a conversion did. Pass one to Codec.ToIntoMeta to collect it; nothing is tracked otherwise, so the default path stays cheap.
type Opt ¶
type Opt uint16
Opt is a bitmask of struct tag options.
const ( // OptSkip is `-`: the field is ignored in both directions. OptSkip Opt = 1 << iota // OptOmitEmpty drops the field when its value is the zero value. // Applies when the destination is a map. OptOmitEmpty // OptInline merges the field's keys into the parent instead of nesting // them under the field name. Applies in both directions. OptInline // OptRemain collects keys that match no field. Applies in both // directions, which keeps round-trips lossless. OptRemain // OptString converts the value to its string form. OptString // OptDeref writes the pointed-to value instead of the pointer. A nil // pointer yields the zero value. Applies when the destination is a map. OptDeref // OptKeep passes the value through untouched instead of converting it // recursively. Applies when the destination is a map. OptKeep )
type Option ¶
type Option func(*config)
Option configures a Codec. Options are applied in order by New.
func Alias ¶ added in v0.2.0
Alias declares that the named type Named is converted exactly like Base, so that a conversion registered for Base also serves Named:
type Timeout time.Duration
c := gofret.New(
gofret.Between(time.ParseDuration),
gofret.Alias[Timeout, time.Duration](),
)
It is deliberately explicit rather than inferred from the underlying type, because sharing an underlying type does not mean sharing a format: `type Celsius int64` has the same underlying type as time.Duration and must not be parsed as a duration.
Named and Base must be convertible to one another, which two types sharing an underlying type always are.
Example ¶
Alias lends a named type the conversions of the type it is defined from. It is explicit because sharing an underlying type does not mean sharing a format.
package main
import (
"fmt"
"time"
"github.com/rakunlabs/gofret"
)
func main() {
type Timeout time.Duration
type Config struct {
Read Timeout `cfg:"read"`
}
c := gofret.New(gofret.Alias[Timeout, time.Duration]())
cfg, err := c.To[Config](map[string]any{"read": "1h30m"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(time.Duration(cfg.Read))
}
Output: 1h30m0s
func Between ¶ added in v0.2.0
Between registers a conversion from In to Out, written as an ordinary Go function.
This is the form to reach for first. Unlike a Hook it is looked up by type rather than offered every value, so registering more of them does not make every conversion slower, and the function itself never has to decline: it is only ever called with an In.
Because the function is an ordinary one, functions from other packages need no wrapper and the registration needs no type arguments:
c := gofret.New(
gofret.Between(time.ParseDuration), // string -> time.Duration
gofret.Between(netip.ParseAddr), // string -> netip.Addr
)
The pair being registered is whatever the function's argument and result say, so this reads a value in and writes one out equally well:
gofret.Between(netip.ParseAddr) // string -> netip.Addr
gofret.Between(func(s Secret) (string, error) {…}) // Secret -> string
Registering the same pair twice keeps the last registration, so a conversion gofret provides by default can be replaced by registering it again.
The match is on the exact types. A named type such as `type Timeout time.Duration` is a different type and does not match; see Alias.
Example ¶
Between teaches a codec a type it does not already know. The rule is an ordinary function, so it needs no wrapper and no type arguments.
package main
import (
"fmt"
"net/netip"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Addr netip.Addr `cfg:"addr"`
}
c := gofret.New(gofret.Between(netip.ParseAddr))
cfg, err := c.To[Config](map[string]any{"addr": "10.0.0.1"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(cfg.Addr)
}
Output: 10.0.0.1
func BetweenOK ¶ added in v0.2.0
BetweenOK is Between for a function that cannot fail:
gofret.BetweenOK(time.Duration.String) // time.Duration -> string
gofret.BetweenOK(func(s string) Tags {…}) // string -> Tags
Choosing between the two is not a judgement call: pass the function you have and the compiler names the one that takes it.
It has no more of a direction than Between does. A String method is the commonest function that cannot fail, so this is often the way out of a type while Between is the way in, but that follows from parsing being the part that fails rather than from anything here.
Example ¶
Registering the way out settles what a value becomes inside a map[string]any, where the destination is an empty interface and names no type of its own, so the map can be handed to an encoder and read back.
package main
import (
"encoding/json"
"fmt"
"net/netip"
"time"
"github.com/rakunlabs/gofret"
)
func main() {
type Config struct {
Addr netip.Addr `cfg:"addr"`
Timeout time.Duration `cfg:"timeout"`
}
c := gofret.New(
gofret.BetweenOK(netip.Addr.String),
gofret.Between(netip.ParseAddr),
)
m, err := c.To[map[string]any](Config{
Addr: netip.MustParseAddr("10.0.0.1"),
Timeout: 90 * time.Minute,
})
if err != nil {
fmt.Println(err)
return
}
raw, err := json.Marshal(m)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(raw))
}
Output: {"addr":"10.0.0.1","timeout":"1h30m0s"}
func TimeLayout ¶ added in v0.2.0
TimeLayout registers both directions of a time.Time written as text, trying each layout in turn when reading and using the first when writing:
gofret.New(gofret.TimeLayout("2006-01-02"))
A time.Time already understands RFC 3339 on its own through encoding.TextUnmarshaler, so reach for this only when the input uses some other layout. With no layouts it is RFC 3339, which is useful for pinning the way times are written rather than for reading them.
func WithDerefPointers ¶
func WithDerefPointers() Option
WithDerefPointers treats every pointer field as if it carried the `deref` tag option when writing to a map.
func WithErrorUnused ¶
func WithErrorUnused() Option
WithErrorUnused reports an error when the input holds keys that match no destination field and no `remain` field is present.
func WithFailFast ¶
func WithFailFast() Option
WithFailFast stops at the first error. By default every error is collected and returned joined together.
func WithHooks ¶
WithHooks appends conversion hooks. They run in order before the built-in conversion; the first one that does not return ErrSkip wins.
A hook is offered every value, so it can match on things a type pair cannot express, such as "any type implementing this interface". When the rule is simply "this type becomes that type", prefer Between, which is looked up rather than offered and so does not grow the cost of every conversion.
func WithInlineEmbedded ¶
func WithInlineEmbedded() Option
WithInlineEmbedded treats every embedded struct as if it carried the `inline` tag option.
func WithKeyFunc ¶
WithKeyFunc sets how a map key is derived from a field name that carries no name in its struct tag. See CamelCase, SnakeCase, KebabCase and PascalCase.
func WithKeyNormalizer ¶
func WithKeyNormalizer(fn KeyNormalizer) Option
WithKeyNormalizer replaces the fallback key matcher. Keys match when their normalized forms are equal. Passing nil disables fallback matching, making lookups exact.
The default is LooseKey. Pass FoldKey for case-insensitive matching that still respects separators.
func WithLooseKeys ¶
func WithLooseKeys() Option
WithLooseKeys matches keys ignoring case, '-', '_' and ' ', so "MaxRetry", "max_retry" and "max-retry" all refer to the same field.
It is shorthand for WithKeyNormalizer(LooseKey) and is the default; pass it explicitly only to undo an earlier WithStrictKeys or WithKeyNormalizer.
func WithMaxErrors ¶
WithMaxErrors caps how many errors are collected before conversion stops. A value of zero or less means no cap.
func WithOmitNil ¶
func WithOmitNil() Option
WithOmitNil drops nil pointer, map, slice and interface fields when writing to a map.
func WithShallow ¶
func WithShallow() Option
WithShallow treats every field as if it carried the `keep` tag option when writing to a map, so nested structs are passed through as-is instead of being converted recursively.
func WithStrictKeys ¶
func WithStrictKeys() Option
WithStrictKeys matches keys exactly, byte for byte. It is shorthand for WithKeyNormalizer(nil) and turns off the default loose matching.
func WithStrictTags ¶ added in v0.2.1
func WithStrictTags() Option
WithStrictTags reports an error when a struct tag contains an unknown option. By default unknown options are ignored so tags can be shared with packages that have their own option vocabulary.
func WithStrictTypes ¶
func WithStrictTypes() Option
WithStrictTypes turns off the lenient conversions described by WithWeakTypes, so only conversions that cannot lose information are performed: "42" no longer reaches an int field, 3.7 no longer truncates into one, and a negative value no longer wraps into an unsigned one.
func WithTagFallback ¶
WithTagFallback adds tags consulted, in order, when the primary tag is absent on a field. It is handy for reusing existing `json` tags.
func WithTaggedOnly ¶
func WithTaggedOnly() Option
WithTaggedOnly ignores fields that carry no struct tag, as if each of them were tagged "-".
func WithWeakTypes ¶
func WithWeakTypes() Option
WithWeakTypes enables lenient scalar conversions:
- bool to and from string ("1"/"0", "true"/"false")
- number to and from string
- bool to and from number (true == 1)
- []byte and []rune to and from string
- a single value lifted into a one-element slice
- an empty map to an empty slice, and the reverse
It is the default; pass it explicitly only to undo an earlier WithStrictTypes.
func WithZeroFields ¶
func WithZeroFields() Option
WithZeroFields zeroes a destination before writing to it. Without it, maps and slices are merged into rather than replaced.
type Tag ¶
type Tag struct {
// Name is the key name. Empty means "derive from the field name".
Name string
// Opts is the bitmask of the recognised options.
Opts Opt
}
Tag holds the parsed struct tag of a single field.
A tag looks like `cfg:"name,opt1,opt2"`. The first comma-separated element is the key name, the rest are options.
type ValueDecoder ¶
ValueDecoder lets a type choose how it is read in. It is consulted before the hooks and before the built-in conversion.
It is called on an addressable value, so implement it on the pointer receiver.
type ValueEncoder ¶
ValueEncoder lets a type choose how it is written out. It is consulted before the hooks and before the built-in conversion.
The returned value is converted to the destination as usual.