From be1c262485afa232b7b799d1cc18a21ec5f0e87f Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 7 Aug 2024 00:21:14 -0300 Subject: [PATCH] docs: more examples --- env_examples_test.go | 143 ----------------- env_test.go | 26 ---- example_test.go | 360 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 169 deletions(-) delete mode 100644 env_examples_test.go create mode 100644 example_test.go diff --git a/env_examples_test.go b/env_examples_test.go deleted file mode 100644 index 8f9e23e..0000000 --- a/env_examples_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package env - -import ( - "fmt" - "log" - "os" -) - -func ExampleParse() { - type Config struct { - Home string `env:"HOME"` - } - os.Setenv("HOME", "/tmp/fakehome") - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", cfg) - // Output: {Home:/tmp/fakehome} -} - -func ExampleParseAs() { - type Config struct { - Home string `env:"HOME"` - } - os.Setenv("HOME", "/tmp/fakehome") - cfg, err := ParseAs[Config]() - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", cfg) - // Output: {Home:/tmp/fakehome} -} - -func ExampleParse_required() { - type Config struct { - Nope string `env:"NOPE,required"` - } - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", cfg) - // Output: env: required environment variable "NOPE" is not set - // {Nope:} -} - -func ExampleParse_notEmpty() { - type Config struct { - Nope string `env:"NOPE,notEmpty"` - } - os.Setenv("NOPE", "") - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", cfg) - // Output: env: environment variable "NOPE" should not be empty - // {Nope:} -} - -func ExampleParse_unset() { - type Config struct { - Secret string `env:"SECRET,unset"` - } - os.Setenv("SECRET", "1234") - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Printf("%+v - %s", cfg, os.Getenv("SECRET")) - // Output: {Secret:1234} - -} - -func ExampleParse_expand() { - type Config struct { - Expand1 string `env:"EXPAND_1,expand"` - Expand2 string `env:"EXPAND_2,expand" envDefault:"ABC_${EXPAND_1}"` - } - os.Setenv("EXPANDING", "HI") - os.Setenv("EXPAND_1", "HELLO_${EXPANDING}") - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", cfg) - // Output: {Expand1:HELLO_HI Expand2:ABC_HELLO_HI} -} - -func ExampleParse_init() { - type Inner struct { - A string `env:"OLA" envDefault:"HI"` - } - type Config struct { - NilInner *Inner - InitInner *Inner `env:",init"` - } - var cfg Config - if err := Parse(&cfg); err != nil { - log.Fatal(err) - } - fmt.Print(cfg.NilInner, cfg.InitInner) - // Output: &{HI} -} - -func ExampleParse_setDefaults() { - type Config struct { - Foo string `env:"FOO"` - Bar string `env:"BAR" envDefault:"bar"` - } - cfg := Config{ - Foo: "foo", - } - if err := Parse(&cfg); err != nil { - fmt.Println(err) - } - fmt.Printf("%+v", cfg) - // Output: {Foo:foo Bar:bar} -} - -func ExampleParse_onSet() { - type config struct { - Home string `env:"HOME,required"` - Port int `env:"PORT" envDefault:"3000"` - IsProduction bool `env:"PRODUCTION"` - NoEnvTag bool - Inner struct{} `envPrefix:"INNER_"` - } - os.Setenv("HOME", "/tmp/fakehome") - var cfg config - if err := ParseWithOptions(&cfg, Options{ - OnSet: func(tag string, value interface{}, isDefault bool) { - fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) - }, - }); err != nil { - fmt.Println("failed:", err) - } - fmt.Printf("%+v", cfg) - // Output: Set HOME to /tmp/fakehome (default? false) - // Set PORT to 3000 (default? true) - // Set PRODUCTION to (default? false) - // {Home:/tmp/fakehome Port:3000 IsProduction:false NoEnvTag:false Inner:{}} -} diff --git a/env_test.go b/env_test.go index 9e134f5..e17d474 100644 --- a/env_test.go +++ b/env_test.go @@ -1403,32 +1403,6 @@ func TestPrecedenceUnmarshalText(t *testing.T) { isEqual(t, []LogLevel{DebugLevel, InfoLevel}, cfg.LogLevels) } -func ExampleParseWithOptions() { - type thing struct { - desc string - } - - type conf struct { - Thing thing `env:"THING"` - } - - os.Setenv("THING", "my thing") - - c := conf{} - - err := ParseWithOptions(&c, Options{FuncMap: map[reflect.Type]ParserFunc{ - reflect.TypeOf(thing{}): func(v string) (interface{}, error) { - return thing{desc: v}, nil - }, - }}) - if err != nil { - fmt.Println(err) - } - fmt.Println(c.Thing.desc) - // Output: - // my thing -} - func TestFile(t *testing.T) { type config struct { SecretKey string `env:"SECRET_KEY,file"` diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..1f6906d --- /dev/null +++ b/example_test.go @@ -0,0 +1,360 @@ +package env + +import ( + "fmt" + "io" + "os" + "reflect" +) + +// Parse the environment into a struct. +func ExampleParse() { + type Config struct { + Home string `env:"HOME"` + } + os.Setenv("HOME", "/tmp/fakehome") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Home:/tmp/fakehome} +} + +// Parse the environment into a struct using generics. +func ExampleParseAs() { + type Config struct { + Home string `env:"HOME"` + } + os.Setenv("HOME", "/tmp/fakehome") + cfg, err := ParseAs[Config]() + if err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Home:/tmp/fakehome} +} + +func ExampleParse_required() { + type Config struct { + Nope string `env:"NOPE,required"` + } + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: env: required environment variable "NOPE" is not set + // {Nope:} +} + +// While `required` demands the environment variable to be set, it doesn't check +// its value. If you want to make sure the environment is set and not empty, you +// need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). +func ExampleParse_notEmpty() { + type Config struct { + Nope string `env:"NOPE,notEmpty"` + } + os.Setenv("NOPE", "") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: env: environment variable "NOPE" should not be empty + // {Nope:} +} + +// The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added +// to ensure that some environment variable is unset after reading it. +func ExampleParse_unset() { + type Config struct { + Secret string `env:"SECRET,unset"` + } + os.Setenv("SECRET", "1234") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v - %s", cfg, os.Getenv("SECRET")) + // Output: {Secret:1234} - +} + +// If you set the `expand` option, environment variables (either in `${var}` or +// `$var` format) in the string will be replaced according with the actual +// value of the variable. For example: +func ExampleParse_expand() { + type Config struct { + Expand1 string `env:"EXPAND_1,expand"` + Expand2 string `env:"EXPAND_2,expand" envDefault:"ABC_${EXPAND_1}"` + } + os.Setenv("EXPANDING", "HI") + os.Setenv("EXPAND_1", "HELLO_${EXPANDING}") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Expand1:HELLO_HI Expand2:ABC_HELLO_HI} +} + +// You can automatically initialize `nil` pointers regardless of if a variable +// is set for them or not. +// This behavior can be enabled by using the `init` tag option. +func ExampleParse_init() { + type Inner struct { + A string `env:"OLA" envDefault:"HI"` + } + type Config struct { + NilInner *Inner + InitInner *Inner `env:",init"` + } + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Print(cfg.NilInner, cfg.InitInner) + // Output: &{HI} +} + +// You can define the default value for a field by either using the +// `envDefault` tag, or when initializing the `struct`. +// +// Default values defined as `struct` tags will overwrite existing values +// during `Parse`. +func ExampleParse_setDefaults() { + type Config struct { + Foo string `env:"FOO"` + Bar string `env:"BAR" envDefault:"bar"` + } + cfg := Config{ + Foo: "foo", + } + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Foo:foo Bar:bar} +} + +// You might want to listen to value sets and, for example, log something or do +// some other kind of logic. +func ExampleParseWithOptions_onSet() { + type config struct { + Home string `env:"HOME,required"` + Port int `env:"PORT" envDefault:"3000"` + IsProduction bool `env:"PRODUCTION"` + NoEnvTag bool + Inner struct{} `envPrefix:"INNER_"` + } + os.Setenv("HOME", "/tmp/fakehome") + var cfg config + if err := ParseWithOptions(&cfg, Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) + }, + }); err != nil { + fmt.Println("failed:", err) + } + fmt.Printf("%+v", cfg) + // Output: Set HOME to /tmp/fakehome (default? false) + // Set PORT to 3000 (default? true) + // Set PRODUCTION to (default? false) + // {Home:/tmp/fakehome Port:3000 IsProduction:false NoEnvTag:false Inner:{}} +} + +// By default, env supports anything that implements the `TextUnmarshaler` +// interface, which includes `time.Time`. +// +// The upside is that depending on the format you need, you don't need to change +// anything. +// +// The downside is that if you do need time in another format, you'll need to +// create your own type and implement `TextUnmarshaler`. +func ExampleParse_customTimeFormat() { + // type MyTime time.Time + // + // func (t *MyTime) UnmarshalText(text []byte) error { + // tt, err := time.Parse("2006-01-02", string(text)) + // *t = MyTime(tt) + // return err + // } + + type Config struct { + SomeTime MyTime `env:"SOME_TIME"` + } + os.Setenv("SOME_TIME", "2021-05-06") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Print(cfg.SomeTime) + // Output: {0 63755856000 } +} + +// Parse using extra options. +func ExampleParseWithOptions_customTypes() { + type Thing struct { + desc string + } + + type Config struct { + Thing Thing `env:"THING"` + } + + os.Setenv("THING", "my thing") + + c := Config{} + err := ParseWithOptions(&c, Options{ + FuncMap: map[reflect.Type]ParserFunc{ + reflect.TypeOf(Thing{}): func(v string) (interface{}, error) { + return Thing{desc: v}, nil + }, + }, + }) + if err != nil { + fmt.Println(err) + } + fmt.Print(c.Thing.desc) + // Output: my thing +} + +// Make all fields required by default. +func ExampleParseWithOptions_allFieldsRequired() { + type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` + } + + var cfg Config + if err := ParseWithOptions(&cfg, Options{ + RequiredIfNoDef: true, + }); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v\n", cfg) + // Output: env: required environment variable "PASSWORD" is not set + // {Username:admin Password:} +} + +// Set a custom environment. +// By default, `os.Environ()` is used. +func ExampleParseWithOptions_setEnv() { + type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` + } + + var cfg Config + if err := ParseWithOptions(&cfg, Options{ + Environment: map[string]string{ + "USERNAME": "john", + "PASSWORD": "cena", + }, + }); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v\n", cfg) + // Output: {Username:john Password:cena} +} + +// Handling slices of complex types. +func ExampleParse_complexSlices() { + type Test struct { + Str string `env:"STR"` + Num int `env:"NUM"` + } + type Config struct { + Foo []Test `envPrefix:"FOO"` + } + + os.Setenv("FOO_0_STR", "a") + os.Setenv("FOO_0_NUM", "1") + os.Setenv("FOO_1_STR", "b") + os.Setenv("FOO_1_NUM", "2") + + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v\n", cfg) + // Output: {Foo:[{Str:a Num:1} {Str:b Num:2}]} +} + +// Setting prefixes for inner types. +func ExampleParse_prefix() { + type Inner struct { + Foo string `env:"FOO,required"` + } + type Config struct { + A Inner `envPrefix:"A_"` + B Inner `envPrefix:"B_"` + } + os.Setenv("A_FOO", "a") + os.Setenv("B_FOO", "b") + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {A:{Foo:a} B:{Foo:b}} +} + +// Use a different tag name than `env`. +func ExampleParseWithOptions_tagName() { + type Config struct { + Home string `json:"HOME"` + } + os.Setenv("HOME", "hello") + var cfg Config + if err := ParseWithOptions(&cfg, Options{ + TagName: "json", + }); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Home:hello} +} + +// If you don't want to set the `env` tag on every field, you can use the +// `UseFieldNameByDefault` option. +// +// It will use the field name to define the environment variable name. +// So, `Foo` becomes `FOO`, `FooBar` becomes `FOO_BAR`, and so on. +func ExampleParseWithOptions_useFieldName() { + type Config struct { + Foo string + } + os.Setenv("FOO", "bar") + var cfg Config + if err := ParseWithOptions(&cfg, Options{ + UseFieldNameByDefault: true, + }); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Foo:bar} +} + +// The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added in +// order to indicate that the value of the variable shall be loaded from a +// file. +// +// The path of that file is given by the environment variable associated with +// it. +func ExampleParse_fromFile() { + f, _ := os.CreateTemp("", "") + _, _ = io.WriteString(f, "super secret") + _ = f.Close() + + type Config struct { + Secret string `env:"SECRET,file"` + } + os.Setenv("SECRET", f.Name()) + var cfg Config + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + fmt.Printf("%+v", cfg) + // Output: {Secret:super secret} +}