A flexible binding library for Go that maps external values (YAML, JSON, CLI args, environment, HTTP request paths, etc.) into struct fields via struct tags.
bind provides a simple way to populate Go structs from various external sources using struct tags. It supports multiple suppliers that can be combined to fill in struct fields from different sources.
go get github.com/ZackarySantana/bindtype Config struct {
Port int `json:"port"`
Host string `yaml:"host"`
DB string `env:"DB_URL"`
}
yaml := []byte(`host: localhost`)
yamlSup, _ := bind.NewYAMLSupplier(bytes.NewReader(yaml))
json := []byte(`{"port":8080}`)
jsonSup, _ := bind.NewJSONSupplier(bytes.NewReader(json))
os.Setenv("DB_URL", "postgres://user:pass@localhost/db")
var cfg Config
bind.Bind(ctx, &cfg, []bind.Supplier{yamlSup, jsonSup, bind.NewEnvSupplier()})If the target struct already has values, they will not be overwritten. This means calls to bind.Bind will only fill in missing values.
Bind outputs debug information using the provided slog.Logger. If not provided, no logging is done.
Example:
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
bind.Bind(ctx, &var, suppliers, bind.WithLogger(logger))Bind only sets fields with a level less than or equal to the provided level. Default is 1. For more information, see Options Struct Tag. This is to support multiple levels of configuration (e.g. the first level is non-auth fields, the second level is auth-required fields).
Example:
var test struct {
Retries int `json:"retries"`
Name string `json:"name" options:"level=1"`
DBURL string `json:"db_url" options:"level=2"`
}
// Only the Retries and Name fields will be set.
bind.Bind(ctx, &var, suppliers)
// DBURL will also be set.
bind.Bind(ctx, &var, suppliers, bind.WithLevel(2))The options struct tag allows you to specify additional options for each field.
Example:
var test struct {
Name string `json:"name" options:"required"`
Age int `json:"age"`
}
jsonSup, _ := bind.NewJSONSupplier(strings.NewReader(`{"age":30}`))
// This will return an error because Name is required but not provided.
err := bind.Bind(ctx, &test, []bind.Supplier{jsonSup})Example:
var test struct {
Retries int `json:"retries"`
Name string `json:"name" options:"level=1"`
DBURL string `json:"db_url" options:"level=2"`
}
jsonSup, _ := bind.NewJSONSupplier(strings.NewReader(`{"retries":3,"name":"Alice","db_url":"postgres://user:pass@localhost/db"}`))
// Only the Retries and Name fields will be set.
bind.Bind(ctx, &test, []bind.Supplier{jsonSup})Bind supplies a bind.Lazy type that can be used to defer the loading of a value until needed. This is useful for operations that aren't always required, such as fetching data from a database or making an API call.
Example:
type test struct {
Name string `json:"name"`
Age bind.Lazy[int] `database:"age"`
}This exposes an Get method on the Age field that will call the supplier function to fetch the value when needed.
var t test
jsonSup, _ := bind.NewJSONSupplier(strings.NewReader(`{"name":"Alice"}`))
dbSup, _ := bind.NewSelfSupplier(func(ctx context.Context, filter map[string]any) (int, error) {
// Simulate a database call
return 30, nil
}, "database", &t)
err := bind.Bind(ctx, &t, []bind.Supplier{jsonSup, dbSup})
fmt.Println(t.Name) // "Alice"
age, err := t.Age.Get(ctx) // Calls the supplier function to get the age
fmt.Println(age) // 30Golang's type system does not allow for generic types to be used directly in reflection. Therefore, you must register each bind.Lazy type you intend to use with the bind.RegisterLazyType function.
type myType struct {
Value string
}
func init() {
bind.RegisterLazy(func(loader LazyLoader) Lazy[myType] {
return AsLazy[myType](loader)
})
}This registers it for bind.Cache as well.
Bind also provides a bind.Cache type that can be used to cache the result of a supplier function. This is useful for expensive operations that you want to avoid repeating.
The usage is exactly the same as bind.Lazy, but the result is cached after the first call. If you want up-to-date values, use bind.Lazy instead.
Parses raw JSON into a map[string]json.RawMessage and extracts values by json tags.
func NewJSONSupplier(src io.Reader) (*JSONSupplier, error)Example:
jsonData := `{"name":"Alice","age":30}`
sup, _ := bind.NewJSONSupplier(strings.NewReader(jsonData))
var age int
sup.Fill(ctx, "age", nil, &age)
// Or bind directly to a struct:
var test struct {
Name string `json:"name"`
Age int `json:"age"`
}
bind.Bind(ctx, &test, []bind.Supplier{sup})Parses raw YAML into a map[string]yaml.Node and extracts values by yaml tags.
func NewYAMLSupplier(r io.Reader) (*YAMLSupplier, error)Example:
yamlData := `name: Bob
age: 42`
sup, _ := bind.NewYAMLSupplier(strings.NewReader(yamlData))
var age int
sup.Fill(ctx, "age", nil, &age)
// Or bind directly to a struct:
var test struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
}
bind.Bind(ctx, &test, []bind.Supplier{sup})Looks up environment variables based on env:"..." tags.
Example:
os.Setenv("PORT", "9090")
sup := bind.NewEnvSupplier()
var port int
sup.Fill(ctx, "PORT", nil, &port)
// Or bind directly to a struct:
var test struct {
Port int `env:"PORT"`
}
bind.Bind(ctx, &test, []bind.Supplier{sup})The SelfSupplier is used to populate fields in a struct based on values from the struct itself. This is particularly useful for scenarios where you want to use certain fields as keys to look up additional data from a store or database.
Example:
type User struct {
ID int
Phone string
Name string `test:"id=ID"`
Age int `test2:"num=Phone,other=ID"`
}
u := User{
ID: 9001,
Phone: "970-4133",
}
testSup, _ := bind.NewSelfSupplier(func(ctx context.Context, filter map[string]any) (string, error) {
// filter == map[string]any{"id": 9001}
// Notice how the filter includes the value of ID from the struct
return "found!", nil
}, "test", &u)
test2Sup, _ := bind.NewSelfSupplier(func(ctx context.Context, filter map[string]any) (int, error) {
// filter == map[string]any{"num": "970-4133", "other": 9001}
// Notice how the filter includes the values of Phone and ID from the struct
return 42, nil
}, "test2", &u)
bind.Bind(ctx, &u, []bind.Supplier{testSup, test2Sup})
// u.Name == "found!"
// u.Age == 42- PathSupplier: Extracts values from HTTP request paths via
req.PathValue. Usingpath:"..."tags. - QuerySupplier: Extracts values from URL query parameters using
query:"..."tags. - HeaderSupplier: Extracts values from HTTP headers using
header:"..."tags. - FormSupplier: Extracts values from form data using
form:"..."tags. - RequestSuppliers: From a given
*http.Request, creates a PathSupplier, QuerySupplier, HeaderSupplier and, FormSupplier. - FlagSupplier: Binds values from CLI flags using
flag:"..."tags. - FuncSupplier: Uses a user-defined function to supply values based on a given tag.
- FuncStringSupplier: Uses a user-defined function that returns strings to supply values based on a given tag. The strings are then attempted to be converted to the target field type.
Run all tests with:
go test ./... ./modules*Benchmark graphs can be seen here.
Run all benchmarks with:
go test -bench=. -benchmem -run=^$ -benchtime=2s -count=4 ./... ./modules/*PRs and issues are welcome! If you add a new supplier, please include:
- Unit tests
- Example usage in the README
- Documentation comments
MIT License. See LICENSE for details.