Skip to content

Commit 46863b3

Browse files
dncckRestartFU
andauthored
feat: add dotenv struct support to DotenvMarshaler (#11)
Co-authored-by: RestartFU <me@restartfu.com>
1 parent b52a40a commit 46863b3

File tree

7 files changed

+152
-7
lines changed

7 files changed

+152
-7
lines changed

config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func extractContextValues(ctx context.Context) (string, Marshaler, error) {
9090
}
9191

9292
if len(missing) > 0 {
93-
return "", marshaler, errors.New(fmt.Sprintf("missing required values in context: %s", strings.Join(missing, ",")))
93+
return "", marshaler, fmt.Errorf("missing required values in context: %s", strings.Join(missing, ","))
9494
}
9595
return name, marshaler, nil
9696
}

dotenv.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package gophig
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/joho/godotenv"
9+
)
10+
11+
type DotenvMarshaler struct{}
12+
13+
func (DotenvMarshaler) Marshal(v any) ([]byte, error) {
14+
rv := reflect.ValueOf(v)
15+
if rv.Kind() == reflect.Pointer {
16+
rv = rv.Elem()
17+
}
18+
if !rv.IsValid() {
19+
return nil, fmt.Errorf("DotenvMarshaler: Marshal expects a valid value")
20+
}
21+
22+
var sb strings.Builder
23+
24+
switch rv.Kind() {
25+
case reflect.Map:
26+
m, ok := v.(map[string]string)
27+
if !ok {
28+
return nil, fmt.Errorf("DotenvMarshaler: Marshal expects map[string]string")
29+
}
30+
for key, val := range m {
31+
val = strings.ReplaceAll(val, "\n", `\n`)
32+
sb.WriteString(fmt.Sprintf("%s=%s\n", key, val))
33+
}
34+
35+
case reflect.Struct:
36+
rt := rv.Type()
37+
for i := 0; i < rt.NumField(); i++ {
38+
field := rt.Field(i)
39+
fv := rv.Field(i)
40+
if !fv.CanInterface() {
41+
continue
42+
}
43+
44+
key := field.Tag.Get("env")
45+
if key == "" {
46+
continue // skip fields without env tag
47+
}
48+
49+
val := fmt.Sprintf("%v", fv.Interface())
50+
val = strings.ReplaceAll(val, "\n", `\n`)
51+
sb.WriteString(fmt.Sprintf("%s=%s\n", key, val))
52+
}
53+
54+
default:
55+
return nil, fmt.Errorf("DotenvMarshaler: unsupported type %s", rv.Kind())
56+
}
57+
58+
return []byte(sb.String()), nil
59+
}
60+
61+
func (DotenvMarshaler) Unmarshal(data []byte, v any) error {
62+
envMap, err := godotenv.Unmarshal(string(data))
63+
if err != nil {
64+
return fmt.Errorf("DotenvMarshaler: failed to unmarshal dotenv: %w", err)
65+
}
66+
67+
rv := reflect.ValueOf(v)
68+
if rv.Kind() != reflect.Pointer || rv.IsNil() {
69+
return fmt.Errorf("DotenvMarshaler: Unmarshal expects a non-nil pointer")
70+
}
71+
72+
rvElem := rv.Elem()
73+
74+
switch rvElem.Kind() {
75+
case reflect.Map:
76+
if rvElem.Type().Key().Kind() != reflect.String || rvElem.Type().Elem().Kind() != reflect.String {
77+
return fmt.Errorf("DotenvMarshaler: only map[string]string supported")
78+
}
79+
rvElem.Set(reflect.ValueOf(envMap))
80+
81+
case reflect.Struct:
82+
for i := 0; i < rvElem.NumField(); i++ {
83+
field := rvElem.Type().Field(i)
84+
tag := field.Tag.Get("env")
85+
if tag == "" {
86+
tag = strings.ToUpper(field.Name)
87+
}
88+
if val, ok := envMap[tag]; ok {
89+
f := rvElem.Field(i)
90+
if f.CanSet() {
91+
switch f.Kind() {
92+
case reflect.String:
93+
f.SetString(val)
94+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
95+
var intVal int64
96+
_, err := fmt.Sscan(val, &intVal)
97+
if err != nil {
98+
return fmt.Errorf("DotenvMarshaler: failed to parse int for field %s: %w", field.Name, err)
99+
}
100+
f.SetInt(intVal)
101+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
102+
var uintVal uint64
103+
_, err := fmt.Sscan(val, &uintVal)
104+
if err != nil {
105+
return fmt.Errorf("DotenvMarshaler: failed to parse uint for field %s: %w", field.Name, err)
106+
}
107+
f.SetUint(uintVal)
108+
case reflect.Bool:
109+
var boolVal bool
110+
_, err := fmt.Sscan(val, &boolVal)
111+
if err != nil {
112+
return fmt.Errorf("DotenvMarshaler: failed to parse bool for field %s: %w", field.Name, err)
113+
}
114+
f.SetBool(boolVal)
115+
case reflect.Float32, reflect.Float64:
116+
var floatVal float64
117+
_, err := fmt.Sscan(val, &floatVal)
118+
if err != nil {
119+
return fmt.Errorf("DotenvMarshaler: failed to parse float for field %s: %w", field.Name, err)
120+
}
121+
f.SetFloat(floatVal)
122+
default:
123+
return fmt.Errorf("DotenvMarshaler: unsupported field type %s for field %s", f.Kind(), field.Name)
124+
}
125+
}
126+
}
127+
}
128+
129+
default:
130+
return fmt.Errorf("DotenvMarshaler: unsupported type %s", rvElem.Kind())
131+
}
132+
133+
return nil
134+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.20
44

55
require (
66
github.com/goccy/go-json v0.10.2
7+
github.com/joho/godotenv v1.5.1
78
github.com/pelletier/go-toml/v2 v2.0.7
89
github.com/stretchr/testify v1.9.0
910
gopkg.in/yaml.v3 v3.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
55
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
6+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
7+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
68
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
79
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
810
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

gophig_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package gophig_test
22

33
import (
4-
"github.com/restartfu/gophig"
5-
"github.com/stretchr/testify/require"
64
"os"
75
"testing"
6+
7+
"github.com/restartfu/gophig"
8+
"github.com/stretchr/testify/require"
89
)
910

1011
type MockMarshaler struct{}
1112

12-
func (m MockMarshaler) Marshal(interface{}) ([]byte, error) {
13+
func (m MockMarshaler) Marshal(any) ([]byte, error) {
1314
return []byte{}, nil
1415
}
15-
func (MockMarshaler) Unmarshal([]byte, interface{}) error {
16+
func (MockMarshaler) Unmarshal([]byte, any) error {
1617
return nil
1718
}
1819

@@ -34,6 +35,7 @@ func TestGophig_GetConf(t *testing.T) {
3435
"json",
3536
"toml",
3637
"yaml",
38+
"env",
3739
} {
3840
t.Run("sample unmarshals successfully into "+ext+" sample struct", func(t *testing.T) {
3941
marshaler, err := gophig.MarshalerFromExtension(ext)
@@ -62,6 +64,7 @@ func TestGophig_SetConf(t *testing.T) {
6264
"json",
6365
"toml",
6466
"yaml",
67+
"env",
6568
} {
6669
t.Run("sample marshals successfully into "+ext+" sample data", func(t *testing.T) {
6770
marshaler, err := gophig.MarshalerFromExtension(ext)

marshal.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ func IsUnsupportedExtensionErr(err error) bool {
2525

2626
// Marshaler is an interface that can marshal and unmarshal data.
2727
type Marshaler interface {
28-
Marshal(v interface{}) ([]byte, error)
29-
Unmarshal(data []byte, v interface{}) error
28+
Marshal(v any) ([]byte, error)
29+
Unmarshal(data []byte, v any) error
3030
}
3131

3232
// MarshalerFromExtension is a Marshaler that uses a file extension to determine which Marshaler to use.
@@ -39,6 +39,8 @@ func MarshalerFromExtension(ext string) (Marshaler, error) {
3939
return JSONMarshaler{}, nil
4040
case "yaml":
4141
return YAMLMarshaler{}, nil
42+
case "env":
43+
return DotenvMarshaler{}, nil
4244
}
4345
return nil, UnsupportedExtensionError{ext}
4446
}

tests/assets/sample.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
NAME=jane
2+
SURNAME=doe
3+
AGE=20

0 commit comments

Comments
 (0)