gofret

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: 0BSD Imports: 10 Imported by: 0

README

gofret

License Coverage CI Go Reference

Convert Go values between shapes: maps into structs, structs into maps, structs into other structs. No dependencies.

go get github.com/rakunlabs/gofret

Requires Go 1.27.

One entry point

There is a single conversion function. The destination type decides what happens, so there is no separate encoder and decoder to keep in step:

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

Both directions run through the same engine and the same analysis of each struct type, so they cannot disagree about what a tag means. That is also what makes the round trip hold:

m,    _ := gofret.To[map[string]any](cfg)
back, _ := gofret.To[Config](m)
// reflect.DeepEqual(cfg, back) == true

Options

The defaults suit configuration data: keys match loosely and scalars convert leniently, so the "8080" an environment variable hands you reaches an int field and max_retry, max-retry and MaxRetry all find the same one. WithStrictKeys() and WithStrictTypes() turn those off.

Build a Codec when you want options. It 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.WithHooks(gofret.DurationHook),
    gofret.WithErrorUnused(),
)

err := c.ToInto(data, &cfg)
option effect
WithTag(s) struct tag to read, default cfg
WithTagFallback(s...) tags consulted when the primary one is absent
WithHooks(h...) conversion hooks
WithWeakTypes() lenient scalar conversions: "42" to 42, 1 to true (default)
WithStrictTypes() only conversions that cannot lose information
WithLooseKeys() match keys ignoring case, -, _ and (default)
WithStrictKeys() match keys exactly
WithKeyNormalizer(fn) replace the fallback key matcher; FoldKey for case-only, nil for exact
WithKeyFunc(fn) derive keys from field names: CamelCase, SnakeCase, KebabCase, PascalCase
WithZeroFields() zero a destination before writing instead of merging into it
WithInlineEmbedded() treat every embedded struct as inline
WithTaggedOnly() ignore fields with no struct tag
WithDerefPointers() treat every pointer field as deref
WithOmitNil() drop nil fields when writing to a map
WithShallow() treat every field as keep
WithErrorUnused() fail when the input holds keys no field claims
WithFailFast() stop at the first error
WithMaxErrors(n) cap how many errors are collected

Struct tags

type Config struct {
    Name    string         `cfg:"name"`
    Secret  string         `cfg:"-"`
    Debug   bool           `cfg:"debug,omitempty"`
    Auth    Auth           `cfg:",inline"`
    Rest    map[string]any `cfg:",remain"`
}
tag to a map to a struct
- skip skip
omitempty drop the zero value
inline merge into the parent read from the parent
remain write the captured keys back collect the unclaimed keys
string render as text parse from text
deref write the pointed-to value
keep pass through unconverted

An unknown option is an error rather than something quietly ignored, so a typo such as omitemty surfaces the first time the type is used. The check runs once per type, because the analysis is cached.

Hooks

A hook replaces a value before the built-in conversion runs. Return ErrSkip to decline; any other error aborts the conversion and reaches the caller, so a hook can report a real problem instead of quietly falling through.

c := gofret.New(gofret.WithHooks(
    gofret.HookBetween(func(s string) (time.Time, error) {
        return time.Parse("2006-01-02", s)
    }),
))
builder fires when
HookTo[T](fn) the destination type is exactly T
HookFrom[T](fn) the source is assignable to T, whatever the destination
HookBetween[In, Out](fn) both

Use HookFrom when writing into a map, where the destination is the empty interface and carries no type information.

For full control, write the Hook yourself:

func(ctx gofret.HookCtx) (any, error)

HookCtx gives you the source and destination types (From, To), the Value, and the parsed Tag of the field being converted. ctx.Data() boxes the value into an any and ctx.Path() builds the dotted location; both are methods so a hook that only inspects types and declines allocates nothing.

Built-in hooks: DurationHook, TimeHook(layouts...), TimeFormatHook(layout), NilHook.

Types that speak for themselves
type ValueEncoder interface{ EncodeValue() (any, error) }
type ValueDecoder interface{ DecodeValue(any) error }

encoding.TextMarshaler, encoding.TextUnmarshaler and fmt.Stringer are honoured out of the box, so a time.Time field reads and writes RFC 3339 and a time.Duration comes out as "1h30m0s" with no configuration at all.

Errors

Every failure is a *gofret.Error carrying the dotted path of the offending value. Failures are collected and joined, so errors.Is and errors.AsType reach all of them.

if ce, ok := errors.AsType[*gofret.Error](err); ok {
    log.Printf("bad value at %s", ce.Path) // "servers[1].port"
}

A conversion reports every failure at once, so the tree usually holds more than one. errors.AsType gives you the first; gofret.Errors gives you all of them:

for _, ce := range gofret.Errors(err) {
    log.Printf("%s: %v", ce.Path, ce.Err)
}

Failures come back in destination field order, not in whatever order Go happened to walk the input map, so the same input always reports the same thing. The same holds for Metadata.

Sentinels: ErrSkip, ErrNotPointer, ErrUnconvertible, ErrUnsupportedType, ErrOverflow, ErrUnusedKeys, ErrInvalidTag.

Nothing panics. A bad input, a bad destination or a bad tag all arrive as an error.

Metadata

var md gofret.Metadata

err := c.ToIntoMeta(data, &cfg, &md)

md.Keys    // paths that were written
md.Unused  // input keys no field claimed, the usual way to warn about typos
md.Unset   // fields no input supplied

Nothing is tracked when you do not ask for it, so the default path stays cheap.

Performance

Analysis of each struct type is cached per codec, tag options are a bitmask, and key matching is precomputed, so no work is repeated per call.

Converting an eight-field config with two nested structs, on a Ryzen 7 5800X:

map to struct

sec/op B/op allocs/op
gofret 4.30µ 2.03Ki 59
struct2 v1.4.0 10.12µ 8.63Ki 142
mapstructure v2.2.1 10.51µ 7.77Ki 137

struct to map

sec/op B/op allocs/op
gofret 5.21µ 2.92Ki 72
struct2 v1.4.0 6.75µ 5.85Ki 66

A configured hook that declines costs about 10% and no allocations, because HookCtx builds the expensive parts only when asked.

Reuse a Codec; New starts with a cold cache.

License

BSD Zero Clause

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.WithHooks(gofret.DurationHook),
    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

An unknown option is an error rather than something quietly ignored, so a typo such as "omitemty" surfaces the first time the type is used. The cost is paid once per type because the analysis is cached.

Hooks

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:

c := gofret.New(gofret.WithHooks(
    gofret.HookBetween(func(s string) (time.Time, error) {
        return time.Parse("2006-01-02", s)
    }),
))

HookTo, HookFrom and HookBetween build hooks from ordinary typed functions. 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)
package main

import (
	"fmt"
	"time"

	"github.com/rakunlabs/gofret"
)

func main() {
	type Config struct {
		Timeout time.Duration `cfg:"timeout"`
	}

	c := gofret.New(gofret.WithHooks(gofret.DurationHook))

	cfg, err := c.To[Config](map[string]any{"timeout": "1h30m"})
	if err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println(cfg.Timeout)
}
Output:
1h30m0s
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

Examples

Constants

View Source
const DefaultTag = "cfg"

DefaultTag is the struct tag gofret reads when WithTag is not given.

Variables

View Source
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

func CamelCase(s string) string

CamelCase renders the name in lowerCamelCase: "MaxRetry" becomes "maxRetry", "ID" becomes "id" and "HTTPServer" becomes "httpServer".

func FoldKey

func FoldKey(s string) string

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 KebabCase

func KebabCase(s string) string

KebabCase renders the name in kebab-case: "MaxRetry" becomes "max-retry".

func LooseKey

func LooseKey(s string) string

LooseKey lowercases the key and removes '-', '_' and ' ', so "MaxRetry", "max_retry", "max-retry" and "max retry" all match. It is the default matcher.

func LowerCase

func LowerCase(s string) string

LowerCase lowercases the whole name without inserting separators.

func PascalCase

func PascalCase(s string) string

PascalCase renders the name in UpperCamelCase: "max_retry" becomes "MaxRetry".

func SnakeCase

func SnakeCase(s string) string

SnakeCase renders the name in snake_case: "MaxRetry" becomes "max_retry" and "HTTPServer" becomes "http_server".

func To

func To[T any](in any) (T, error)

To converts in to a value of type T using the default configuration.

Build a Codec with New when you need options or want the type cache to be reused across calls.

func ToInto

func ToInto(in, out any) error

ToInto converts in and writes the result through out, which must be a non-nil pointer, using the default configuration.

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 New

func New(opts ...Option) *Codec

New builds a Codec from the given options.

func (*Codec) To

func (c *Codec) To[T any](in any) (T, error)

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.

func (*Codec) ToInto

func (c *Codec) ToInto(in, out any) error

ToInto converts in and writes the result through out, which must be a non-nil pointer.

func (*Codec) ToIntoMeta

func (c *Codec) ToIntoMeta(in, out any, md *Metadata) error

ToIntoMeta is ToInto and additionally records what happened into md, which may be 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

func Errors(err error) []*Error

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

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Hook

type Hook func(HookCtx) (any, error)

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.

var DurationHook Hook = HookBetween(func(s string) (time.Duration, error) {
	if s == "" {
		return 0, nil
	}

	d, err := time.ParseDuration(s)
	if err != nil {
		return 0, fmt.Errorf("%w: %w", ErrUnconvertible, err)
	}

	return d, nil
})

DurationHook parses a string into a time.Duration, so "1h30m" reaches a time.Duration field.

Numbers are left to the ordinary integer conversion, where they are taken as nanoseconds, matching what time.Duration itself means.

The reverse needs no hook: time.Duration is a fmt.Stringer, so writing one into a string destination already yields "1h30m0s".

var NilHook Hook = func(ctx HookCtx) (any, error) {
	if ctx.From == nil || !isNilLike(ctx.Value) {
		return nil, ErrSkip
	}

	if ctx.From.Kind() != reflect.Pointer {
		return nil, ErrSkip
	}

	return reflect.Zero(ctx.From.Elem()).Interface(), nil
}

NilHook replaces a nil pointer, map or slice with the zero value of the type it points at, so a destination never sees an untyped nil.

func HookBetween

func HookBetween[In, Out any](fn func(In) (Out, error)) Hook

HookBetween builds a Hook that fires when the source is assignable to In and the destination type is exactly Out.

gofret.HookBetween(func(s string) (time.Time, error) {
    return time.Parse(time.RFC3339, s)
})

func HookFrom

func HookFrom[T any](fn func(T) (any, error)) Hook

HookFrom builds a Hook that fires when the source value is assignable to T, whatever the destination is.

This is the form to use when writing into a map, where the destination type is the empty interface and so carries no information.

gofret.HookFrom(func(t time.Time) (any, error) {
    return t.Format(time.RFC3339), nil
})

func HookTo

func HookTo[T any](fn func(any) (T, error)) Hook

HookTo builds a Hook that fires when the destination type is exactly T.

gofret.HookTo(func(in any) (time.Duration, error) {
    s, ok := in.(string)
    if !ok {
        return 0, gofret.ErrSkip
    }
    return time.ParseDuration(s)
})

func TimeFormatHook

func TimeFormatHook(layout string) Hook

TimeFormatHook renders a time.Time with the given layout on the way out. With no layout it uses RFC 3339.

func TimeHook

func TimeHook(layouts ...string) Hook

TimeHook parses a string into a time.Time, trying each layout in turn.

With no layouts it accepts RFC 3339. A time.Time field already understands RFC 3339 on its own through encoding.TextUnmarshaler, so reach for this only when the input uses some other layout.

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.

func (HookCtx) Data

func (c HookCtx) Data() any

Data returns the source value as an any, so a simple hook needs no reflection. It boxes the value, so prefer Value when you are only going to inspect it.

func (HookCtx) Path

func (c HookCtx) Path() string

Path returns the dotted location of the value, for example "database.hosts[2].port". It is empty at the root.

The path is built on demand, so asking for it only in an error branch costs nothing on the happy path.

type KeyFunc

type KeyFunc func(string) string

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

type KeyNormalizer func(string) string

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.

func (*Metadata) Reset

func (m *Metadata) Reset()

Reset clears the metadata while keeping the allocated slices, so a single Metadata can be reused across conversions.

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
)

func (Opt) String

func (o Opt) String() string

String renders the option names, comma separated, in declaration order.

type Option

type Option func(*config)

Option configures a Codec. Options are applied in order by New.

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

func WithHooks(hooks ...Hook) Option

WithHooks appends conversion hooks. They run in order before the built-in conversion; the first one that does not return ErrSkip wins.

func WithInlineEmbedded

func WithInlineEmbedded() Option

WithInlineEmbedded treats every embedded struct as if it carried the `inline` tag option.

func WithKeyFunc

func WithKeyFunc(fn KeyFunc) Option

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

func WithMaxErrors(n int) Option

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 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 WithTag

func WithTag(tag string) Option

WithTag sets the struct tag to read. The default is DefaultTag.

func WithTagFallback

func WithTagFallback(tags ...string) Option

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.

func (Tag) Has

func (t Tag) Has(opt Opt) bool

Has reports whether opt is set. Multiple bits may be given, in which case it reports whether all of them are set.

func (Tag) HasAny

func (t Tag) HasAny(opt Opt) bool

HasAny reports whether at least one of the given bits is set.

type ValueDecoder

type ValueDecoder interface {
	DecodeValue(any) error
}

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

type ValueEncoder interface {
	EncodeValue() (any, error)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL