Skip to content

Commit d815876

Browse files
committed
phase 6: comprehensive tests (93% coverage), examples, README with migration guide
1 parent ec6776b commit d815876

3 files changed

Lines changed: 262 additions & 2 deletions

File tree

README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,115 @@ A Go library that populates struct fields from environment variables. Drop-in re
88
go get github.com/agentine/envstruct
99
```
1010

11+
## Quick Start
12+
13+
```go
14+
package main
15+
16+
import (
17+
"fmt"
18+
"log"
19+
20+
"github.com/agentine/envstruct"
21+
)
22+
23+
type Config struct {
24+
Host string `default:"localhost" desc:"Server hostname"`
25+
Port int `default:"8080" desc:"Server port"`
26+
Debug bool ` desc:"Enable debug mode"`
27+
}
28+
29+
func main() {
30+
var c Config
31+
if err := envstruct.Process("APP", &c); err != nil {
32+
log.Fatal(err)
33+
}
34+
fmt.Printf("Listening on %s:%d\n", c.Host, c.Port)
35+
}
36+
```
37+
38+
Set environment variables and run:
39+
40+
```
41+
APP_HOST=0.0.0.0 APP_PORT=9090 go run main.go
42+
# Listening on 0.0.0.0:9090
43+
```
44+
45+
## Supported Types
46+
47+
| Type | Example env value |
48+
|------|-------------------|
49+
| `string` | `hello` |
50+
| `bool` | `true`, `false`, `1`, `0` |
51+
| `int`, `int8`..`int64` | `42`, `-1` |
52+
| `uint`, `uint8`..`uint64` | `42` |
53+
| `float32`, `float64` | `3.14` |
54+
| `time.Duration` | `5s`, `100ms` |
55+
| `url.URL` / `*url.URL` | `https://example.com` |
56+
| `[]T` (any scalar T) | `a,b,c` |
57+
| `map[string]T` | `key1=val1,key2=val2` |
58+
| Custom `Decoder` | User-defined |
59+
| Custom `Setter` | envconfig compat |
60+
| `encoding.TextUnmarshaler` | User-defined |
61+
62+
## Struct Tags
63+
64+
| Tag | Description |
65+
|-----|-------------|
66+
| `env:"VAR_NAME"` | Override env var name |
67+
| `env:"VAR_NAME,required"` | Mark field as required |
68+
| `env:"-"` | Skip field |
69+
| `envconfig:"VAR_NAME"` | envconfig compat tag |
70+
| `default:"value"` | Default if env var unset |
71+
| `desc:"description"` | Description for Usage() |
72+
73+
## Nested Structs
74+
75+
Nested struct fields are flattened with `_` separators:
76+
77+
```go
78+
type DB struct {
79+
Host string
80+
Port int
81+
}
82+
type Config struct {
83+
Database DB
84+
}
85+
// Reads APP_DATABASE_HOST, APP_DATABASE_PORT
86+
```
87+
88+
Embedded structs are flattened without adding a prefix segment.
89+
90+
## Usage Text
91+
92+
```go
93+
envstruct.Usage("APP", &Config{}, os.Stderr)
94+
```
95+
96+
Outputs:
97+
98+
```
99+
APP_HOST string [default: localhost] Server hostname
100+
APP_PORT int [default: 8080] Server port
101+
APP_DEBUG bool Enable debug mode
102+
```
103+
104+
## Migration from envconfig
105+
106+
envstruct is a drop-in replacement. The function signatures are identical:
107+
108+
```go
109+
// Before
110+
envconfig.Process("APP", &config)
111+
envconfig.MustProcess("APP", &config)
112+
113+
// After
114+
envstruct.Process("APP", &config)
115+
envstruct.MustProcess("APP", &config)
116+
```
117+
118+
Both `env` and `envconfig` struct tags are supported for smooth migration.
119+
11120
## License
12121

13122
MIT

decoder_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,98 @@ func TestDecodeBoolParseError(t *testing.T) {
358358
t.Fatalf("expected ParseError, got %T", err)
359359
}
360360
}
361+
362+
type testTextUnmarshaler struct{ val string }
363+
364+
func (u *testTextUnmarshaler) UnmarshalText(text []byte) error {
365+
u.val = "unmarshaled:" + string(text)
366+
return nil
367+
}
368+
369+
func TestDecodeTextUnmarshaler(t *testing.T) {
370+
type C struct{ TU testTextUnmarshaler }
371+
t.Setenv("TU", "hello")
372+
var c C
373+
if err := Process("", &c); err != nil {
374+
t.Fatal(err)
375+
}
376+
if c.TU.val != "unmarshaled:hello" {
377+
t.Fatalf("got %q", c.TU.val)
378+
}
379+
}
380+
381+
func TestParseErrorString(t *testing.T) {
382+
pe := &ParseError{
383+
FieldName: "Port",
384+
EnvVar: "APP_PORT",
385+
Value: "abc",
386+
TypeName: "int",
387+
Err: errors.New("invalid syntax"),
388+
}
389+
s := pe.Error()
390+
if s == "" {
391+
t.Fatal("empty error string")
392+
}
393+
if pe.Unwrap() == nil {
394+
t.Fatal("expected non-nil unwrap")
395+
}
396+
}
397+
398+
func TestRequiredErrorString(t *testing.T) {
399+
re := &RequiredError{
400+
FieldName: "Host",
401+
EnvVar: "APP_HOST",
402+
}
403+
s := re.Error()
404+
if s == "" {
405+
t.Fatal("empty error string")
406+
}
407+
}
408+
409+
func TestDecodeMapBadFormat(t *testing.T) {
410+
type C struct{ Labels map[string]string }
411+
t.Setenv("LABELS", "noequals")
412+
var c C
413+
err := Process("", &c)
414+
if err == nil {
415+
t.Fatal("expected error for bad map format")
416+
}
417+
}
418+
419+
func TestDecodeFloatParseError(t *testing.T) {
420+
type C struct{ Val float64 }
421+
t.Setenv("VAL", "not-a-float")
422+
var c C
423+
err := Process("", &c)
424+
if err == nil {
425+
t.Fatal("expected error")
426+
}
427+
}
428+
429+
func TestDecodeUintParseError(t *testing.T) {
430+
type C struct{ Val uint }
431+
t.Setenv("VAL", "-1")
432+
var c C
433+
err := Process("", &c)
434+
if err == nil {
435+
t.Fatal("expected error")
436+
}
437+
}
438+
439+
func TestDecodeDurationParseError(t *testing.T) {
440+
type C struct{ Val time.Duration }
441+
t.Setenv("VAL", "not-a-duration")
442+
var c C
443+
err := Process("", &c)
444+
if err == nil {
445+
t.Fatal("expected error")
446+
}
447+
}
448+
449+
func TestProcessNonStructPointer(t *testing.T) {
450+
s := "hello"
451+
err := Process("", &s)
452+
if err == nil {
453+
t.Fatal("expected error for non-struct pointer")
454+
}
455+
}

example_test.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ package envstruct_test
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/agentine/envstruct"
78
)
89

910
func ExampleProcess() {
11+
os.Setenv("APP_HOST", "localhost")
12+
os.Setenv("APP_PORT", "8080")
13+
defer os.Unsetenv("APP_HOST")
14+
defer os.Unsetenv("APP_PORT")
15+
1016
type Config struct {
1117
Host string
1218
Port int
@@ -17,6 +23,56 @@ func ExampleProcess() {
1723
fmt.Println("error:", err)
1824
return
1925
}
20-
fmt.Println("ok")
21-
// Output: ok
26+
fmt.Printf("Host=%s Port=%d\n", c.Host, c.Port)
27+
// Output: Host=localhost Port=8080
28+
}
29+
30+
func ExampleProcess_nested() {
31+
os.Setenv("APP_DATABASE_HOST", "db.local")
32+
os.Setenv("APP_DATABASE_PORT", "5432")
33+
defer os.Unsetenv("APP_DATABASE_HOST")
34+
defer os.Unsetenv("APP_DATABASE_PORT")
35+
36+
type DB struct {
37+
Host string
38+
Port int
39+
}
40+
type Config struct {
41+
Database DB
42+
}
43+
var c Config
44+
err := envstruct.Process("APP", &c)
45+
if err != nil {
46+
fmt.Println("error:", err)
47+
return
48+
}
49+
fmt.Printf("DB=%s:%d\n", c.Database.Host, c.Database.Port)
50+
// Output: DB=db.local:5432
51+
}
52+
53+
func ExampleProcess_required() {
54+
type Config struct {
55+
Secret string `env:"SECRET,required"`
56+
}
57+
var c Config
58+
err := envstruct.Process("APP", &c)
59+
if err != nil {
60+
fmt.Println("got expected error")
61+
return
62+
}
63+
fmt.Println("unexpected success")
64+
// Output: got expected error
65+
}
66+
67+
func ExampleUsage() {
68+
type Config struct {
69+
Host string `default:"localhost" desc:"Server hostname"`
70+
Port int `default:"8080" desc:"Server port"`
71+
Debug bool `desc:"Enable debug mode"`
72+
}
73+
envstruct.Usage("APP", &Config{}, os.Stdout)
74+
// Output:
75+
// APP_HOST string [default: localhost] Server hostname
76+
// APP_PORT int [default: 8080] Server port
77+
// APP_DEBUG bool Enable debug mode
2278
}

0 commit comments

Comments
 (0)