Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func extractContextValues(ctx context.Context) (string, Marshaler, error) {
}

if len(missing) > 0 {
return "", marshaler, errors.New(fmt.Sprintf("missing required values in context: %s", strings.Join(missing, ",")))
return "", marshaler, fmt.Errorf("missing required values in context: %s", strings.Join(missing, ","))
}
return name, marshaler, nil
}
134 changes: 134 additions & 0 deletions dotenv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package gophig

import (
"fmt"
"reflect"
"strings"

"github.com/joho/godotenv"
)

type DotenvMarshaler struct{}

func (DotenvMarshaler) Marshal(v any) ([]byte, error) {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
if !rv.IsValid() {
return nil, fmt.Errorf("DotenvMarshaler: Marshal expects a valid value")
}

var sb strings.Builder

switch rv.Kind() {
case reflect.Map:
m, ok := v.(map[string]string)
if !ok {
return nil, fmt.Errorf("DotenvMarshaler: Marshal expects map[string]string")
}
for key, val := range m {
val = strings.ReplaceAll(val, "\n", `\n`)
sb.WriteString(fmt.Sprintf("%s=%s\n", key, val))
}

case reflect.Struct:
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
fv := rv.Field(i)
if !fv.CanInterface() {
continue
}

key := field.Tag.Get("env")
if key == "" {
continue // skip fields without env tag
}

val := fmt.Sprintf("%v", fv.Interface())
val = strings.ReplaceAll(val, "\n", `\n`)
sb.WriteString(fmt.Sprintf("%s=%s\n", key, val))
}

default:
return nil, fmt.Errorf("DotenvMarshaler: unsupported type %s", rv.Kind())
}

return []byte(sb.String()), nil
}

func (DotenvMarshaler) Unmarshal(data []byte, v any) error {
envMap, err := godotenv.Unmarshal(string(data))
if err != nil {
return fmt.Errorf("DotenvMarshaler: failed to unmarshal dotenv: %w", err)
}

rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return fmt.Errorf("DotenvMarshaler: Unmarshal expects a non-nil pointer")
}

rvElem := rv.Elem()

switch rvElem.Kind() {
case reflect.Map:
if rvElem.Type().Key().Kind() != reflect.String || rvElem.Type().Elem().Kind() != reflect.String {
return fmt.Errorf("DotenvMarshaler: only map[string]string supported")
}
rvElem.Set(reflect.ValueOf(envMap))

case reflect.Struct:
for i := 0; i < rvElem.NumField(); i++ {
field := rvElem.Type().Field(i)
tag := field.Tag.Get("env")
if tag == "" {
tag = strings.ToUpper(field.Name)
}
if val, ok := envMap[tag]; ok {
f := rvElem.Field(i)
if f.CanSet() {
switch f.Kind() {
case reflect.String:
f.SetString(val)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var intVal int64
_, err := fmt.Sscan(val, &intVal)
if err != nil {
return fmt.Errorf("DotenvMarshaler: failed to parse int for field %s: %w", field.Name, err)
}
f.SetInt(intVal)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var uintVal uint64
_, err := fmt.Sscan(val, &uintVal)
if err != nil {
return fmt.Errorf("DotenvMarshaler: failed to parse uint for field %s: %w", field.Name, err)
}
f.SetUint(uintVal)
case reflect.Bool:
var boolVal bool
_, err := fmt.Sscan(val, &boolVal)
if err != nil {
return fmt.Errorf("DotenvMarshaler: failed to parse bool for field %s: %w", field.Name, err)
}
f.SetBool(boolVal)
case reflect.Float32, reflect.Float64:
var floatVal float64
_, err := fmt.Sscan(val, &floatVal)
if err != nil {
return fmt.Errorf("DotenvMarshaler: failed to parse float for field %s: %w", field.Name, err)
}
f.SetFloat(floatVal)
default:
return fmt.Errorf("DotenvMarshaler: unsupported field type %s for field %s", f.Kind(), field.Name)
}
}
}
}

default:
return fmt.Errorf("DotenvMarshaler: unsupported type %s", rvElem.Kind())
}

return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/goccy/go-json v0.10.2
github.com/joho/godotenv v1.5.1
github.com/pelletier/go-toml/v2 v2.0.7
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
11 changes: 7 additions & 4 deletions gophig_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package gophig_test

import (
"github.com/restartfu/gophig"
"github.com/stretchr/testify/require"
"os"
"testing"

"github.com/restartfu/gophig"
"github.com/stretchr/testify/require"
)

type MockMarshaler struct{}

func (m MockMarshaler) Marshal(interface{}) ([]byte, error) {
func (m MockMarshaler) Marshal(any) ([]byte, error) {
return []byte{}, nil
}
func (MockMarshaler) Unmarshal([]byte, interface{}) error {
func (MockMarshaler) Unmarshal([]byte, any) error {
return nil
}

Expand All @@ -34,6 +35,7 @@ func TestGophig_GetConf(t *testing.T) {
"json",
"toml",
"yaml",
"env",
} {
t.Run("sample unmarshals successfully into "+ext+" sample struct", func(t *testing.T) {
marshaler, err := gophig.MarshalerFromExtension(ext)
Expand Down Expand Up @@ -62,6 +64,7 @@ func TestGophig_SetConf(t *testing.T) {
"json",
"toml",
"yaml",
"env",
} {
t.Run("sample marshals successfully into "+ext+" sample data", func(t *testing.T) {
marshaler, err := gophig.MarshalerFromExtension(ext)
Expand Down
6 changes: 4 additions & 2 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func IsUnsupportedExtensionErr(err error) bool {

// Marshaler is an interface that can marshal and unmarshal data.
type Marshaler interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}

// MarshalerFromExtension is a Marshaler that uses a file extension to determine which Marshaler to use.
Expand All @@ -39,6 +39,8 @@ func MarshalerFromExtension(ext string) (Marshaler, error) {
return JSONMarshaler{}, nil
case "yaml":
return YAMLMarshaler{}, nil
case "env":
return DotenvMarshaler{}, nil
}
return nil, UnsupportedExtensionError{ext}
}
3 changes: 3 additions & 0 deletions tests/assets/sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NAME=jane
SURNAME=doe
AGE=20