Skip to content

Add compat layer #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 11, 2024
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ clean:
@rm -rf gen

setup:
go install gotest.tools/gotestsum@v1.11.0
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install gotest.tools/gotestsum@v1.12.0
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0

vendor:
go mod vendor
9 changes: 9 additions & 0 deletions compat/joho/godotenv/autoload/autoload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package autoload

import (
"github.com/ulyssessouza/envlang"
)

func init() {
envlang.Load() //nolint:errcheck // This is not necessary to check
}
191 changes: 191 additions & 0 deletions compat/joho/godotenv/facade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package godotenv

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"

"github.com/ulyssessouza/envlang"
"github.com/ulyssessouza/envlang/dao"
)

//nolint:gocognit
func GetEnvFromFile(currentEnv map[string]string, filenames []string) (map[string]string, error) {
envMap := make(map[string]string)

for _, dotEnvFile := range filenames {
abs, err := filepath.Abs(dotEnvFile)
if err != nil {
return envMap, err
}
dotEnvFile = abs

s, err := os.Stat(dotEnvFile)
if os.IsNotExist(err) {
return envMap, fmt.Errorf("couldn't find env file: %s", dotEnvFile)
}
if err != nil {
return envMap, err
}

if s.IsDir() {
if len(filenames) == 0 {
return envMap, nil
}
return envMap, fmt.Errorf("%s is a directory", dotEnvFile)
}

b, err := os.ReadFile(dotEnvFile)
if os.IsNotExist(err) {
return nil, fmt.Errorf("couldn't read env file: %s", dotEnvFile)
}
if err != nil {
return envMap, err
}

env, err := ParseWithLookup(bytes.NewReader(b), func(k string) (string, bool) {
v, ok := currentEnv[k]
if ok {
return v, true
}
v, ok = envMap[k]
return v, ok
})
if err != nil {
return envMap, fmt.Errorf("failed to read %s: %w", dotEnvFile, err)
}
for k, v := range env {
envMap[k] = v
}
}

return envMap, nil
}

// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (map[string]string, error) {
return ParseWithLookup(r, nil)
}

// ParseWithLookup reads an env file from io.Reader, returning a map of keys and values.
func ParseWithLookup(r io.Reader, lookupFn dao.LookupFn) (map[string]string, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}

// seek past the UTF-8 BOM if it exists (particularly on Windows, some
// editors tend to add it, and it'll cause parsing to fail)
utf8BOM := []byte("\uFEFF")
data = bytes.TrimPrefix(data, utf8BOM)

return UnmarshalBytesWithLookup(data, lookupFn)
}

// UnmarshalBytesWithLookup parses env file from byte slice of chars, returning a map of keys and values.
func UnmarshalBytesWithLookup(src []byte, lookupFn dao.LookupFn) (map[string]string, error) {
return UnmarshalWithLookup(string(src), lookupFn)
}

// UnmarshalWithLookup parses env file from string, returning a map of keys and values.
func UnmarshalWithLookup(src string, lookupFn dao.LookupFn) (map[string]string, error) {
m := make(map[string]string)
defaultDAO := dao.NewDefaultDaoFromEnv(os.Environ(), dao.WithLookupFn(lookupFn))
srcMap := envlang.GetVariablesFromInputStream(defaultDAO, bytes.NewBufferString(src))

for k, v := range srcMap {
value := ""
if v != nil {
value = *v
}
m[k] = value
}
return m, nil
}

func Load(filenames ...string) error {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err := loadFile(filename)
if err != nil {
return err
}
}
return nil
}

func filenamesOrDefault(filenames []string) []string {
if len(filenames) == 0 {
return []string{".env"}
}
return filenames
}

func loadFile(filename string) error {
defaultDAO := dao.NewDefaultDaoFromEnv(os.Environ())
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
m := envlang.GetVariablesFromInputStream(defaultDAO, f)
for k, v := range m {
if _, ok := os.LookupEnv(k); ok {
continue
}
if v == nil {
v = new(string)
}
os.Setenv(k, *v)
}
return nil
}

func Read(filenames ...string) (map[string]string, error) {
return ReadWithLookup(nil, filenames...)
}

func ReadWithLookup(_ dao.LookupFn, filenames ...string) (map[string]string, error) {
filenames = filenamesOrDefault(filenames)
retMap := make(map[string]string)
for _, filename := range filenames {
m, err := lookupFile(filename)
if err != nil {
return nil, err
}
for k, v := range m {
retMap[k] = v
}
}
return retMap, nil
}

func lookupFile(filename string) (map[string]string, error) {
defaultDAO := dao.NewDefaultDaoFromEnv(os.Environ())
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
m := envlang.GetVariablesFromInputStream(defaultDAO, f)
retMap := make(map[string]string)
for k, v := range m {
if v == nil {
v = new(string)
}
retMap[k] = *v
}
return retMap, nil
}

func ReadFile(filename string, lookupFn dao.LookupFn) (map[string]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

return ParseWithLookup(file, lookupFn)
}
63 changes: 63 additions & 0 deletions compat/joho/godotenv/facade_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package godotenv

import (
"os"
"path/filepath"
"strings"
"testing"

"gotest.tools/v3/assert"
)

func TestEnvVariablePrecedence(t *testing.T) {
testcases := []struct {
name string
dotEnv string
osEnv []string
expected map[string]string
}{
{
"no value set in environment",
"FOO=foo\nBAR=${FOO}",
nil,
map[string]string{
"FOO": "foo",
"BAR": "foo",
},
},
{
"conflict with value set in environment",
"FOO=foo\nBAR=${FOO}",
[]string{"FOO=zot"},
map[string]string{
"FOO": "zot",
"BAR": "zot",
},
},
}

for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
wd := t.TempDir()
err := os.WriteFile(filepath.Join(wd, ".env"), []byte(test.dotEnv), 0o600)
assert.NilError(t, err)

envMap, err := GetEnvFromFile(env2Map(test.osEnv), []string{filepath.Join(wd, ".env")})
assert.NilError(t, err)

assert.DeepEqual(t, test.expected, envMap)
})
}
}

func env2Map(env []string) map[string]string {
m := make(map[string]string)
for _, e := range env {
k, v, b := strings.Cut(e, "=")
if !b {
v = ""
}
m[k] = v
}
return m
}
2 changes: 2 additions & 0 deletions dao/dao.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dao

type LookupFn func(string) (string, bool)

type EnvLangDao interface {
ImportList([]string)
ImportMap(map[string]string)
Expand Down
31 changes: 30 additions & 1 deletion dao/default_dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type DefaultDao struct {

env map[string]*string
m map[string]*string

lookupFn LookupFn
}

func NewDefaultDao() EnvLangDao {
Expand All @@ -26,7 +28,17 @@ func NewDefaultDao() EnvLangDao {
}
}

func NewDefaultDaoFromEnv(env []string) EnvLangDao {
type EnvlangDaoOptionsFn func(d EnvLangDao)

func WithLookupFn(fn LookupFn) EnvlangDaoOptionsFn {
return func(d EnvLangDao) {
if d, ok := d.(*DefaultDao); ok {
d.lookupFn = fn
}
}
}

func NewDefaultDaoFromEnv(env []string, opts ...EnvlangDaoOptionsFn) EnvLangDao {
d := &DefaultDao{
m: make(map[string]*string),
env: make(map[string]*string),
Expand All @@ -37,6 +49,11 @@ func NewDefaultDaoFromEnv(env []string) EnvLangDao {
v := splitEnv[1]
d.env[splitEnv[0]] = &v
}

for _, opt := range opts {
opt(d)
}

return d
}

Expand Down Expand Up @@ -78,6 +95,12 @@ func (d *DefaultDao) Get(k string) (*string, bool) {
d.RLock()
defer d.RUnlock()

if d.lookupFn != nil {
if v, ok := d.lookupFn(k); ok {
return &v, true
}
}

v, ok := d.m[k]
if !ok {
v, ok = d.env[k]
Expand All @@ -89,6 +112,12 @@ func (d *DefaultDao) Put(k string, v *string) {
d.Lock()
defer d.Unlock()

if d.lookupFn != nil {
if lookupValue, ok := d.lookupFn(k); ok {
v = &lookupValue
}
}

d.m[k] = v
}

Expand Down
5 changes: 4 additions & 1 deletion envlang.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package envlang
import (
"io"
"os"
"strings"

antlr "github.com/antlr4-go/antlr/v4"
"github.com/ulyssessouza/envlang/dao"
Expand All @@ -24,8 +25,10 @@ func Load(paths ...string) error {
if len(paths) == 0 {
paths = []string{defaultEnvFile}
}

for _, p := range paths {
if strings.TrimSpace(p) == "" {
continue
}
f, err := os.Open(p)
if err != nil {
return err
Expand Down
Loading