Skip to content
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

Improve encoding layer #1167

Merged
merged 10 commits into from
Dec 15, 2021
6 changes: 3 additions & 3 deletions internal/encoding/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"sync"
)

// Decoder decodes the contents of b into a v representation.
// Decoder decodes the contents of b into v.
// It's primarily used for decoding contents of a file into a map[string]interface{}.
type Decoder interface {
Decode(b []byte, v interface{}) error
Decode(b []byte, v map[string]interface{}) error
}

const (
Expand Down Expand Up @@ -48,7 +48,7 @@ func (e *DecoderRegistry) RegisterDecoder(format string, enc Decoder) error {
}

// Decode calls the underlying Decoder based on the format.
func (e *DecoderRegistry) Decode(format string, b []byte, v interface{}) error {
func (e *DecoderRegistry) Decode(format string, b []byte, v map[string]interface{}) error {
e.mu.RLock()
decoder, ok := e.decoders[format]
e.mu.RUnlock()
Expand Down
26 changes: 15 additions & 11 deletions internal/encoding/decoder_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package encoding

import (
"reflect"
"testing"
)

type decoder struct {
v interface{}
v map[string]interface{}
}

func (d decoder) Decode(_ []byte, v interface{}) error {
rv := v.(*string)
*rv = d.v.(string)
func (d decoder) Decode(_ []byte, v map[string]interface{}) error {
for key, value := range d.v {
v[key] = value
}

return nil
}
Expand Down Expand Up @@ -44,32 +46,34 @@ func TestDecoderRegistry_Decode(t *testing.T) {
t.Run("OK", func(t *testing.T) {
registry := NewDecoderRegistry()
decoder := decoder{
v: "decoded value",
v: map[string]interface{}{
"key": "value",
},
}

err := registry.RegisterDecoder("myformat", decoder)
if err != nil {
t.Fatal(err)
}

var v string
v := map[string]interface{}{}

err = registry.Decode("myformat", []byte("some value"), &v)
err = registry.Decode("myformat", []byte("key: value"), v)
if err != nil {
t.Fatal(err)
}

if v != "decoded value" {
t.Fatalf("expected 'decoded value', got: %#v", v)
if !reflect.DeepEqual(decoder.v, v) {
t.Fatalf("decoded value does not match the expected one\nactual: %+v\nexpected: %+v", v, decoder.v)
}
})

t.Run("DecoderNotFound", func(t *testing.T) {
registry := NewDecoderRegistry()

var v string
v := map[string]interface{}{}

err := registry.Decode("myformat", []byte("some value"), &v)
err := registry.Decode("myformat", nil, v)
if err != ErrDecoderNotFound {
t.Fatalf("expected ErrDecoderNotFound, got: %v", err)
}
Expand Down
61 changes: 61 additions & 0 deletions internal/encoding/dotenv/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dotenv

import (
"bytes"
"fmt"
"sort"
"strings"

"github.com/subosito/gotenv"
)

const keyDelimiter = "_"

// Codec implements the encoding.Encoder and encoding.Decoder interfaces for encoding data containing environment variables
// (commonly called as dotenv format).
type Codec struct{}

func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
flattened := map[string]interface{}{}

flattened = flattenAndMergeMap(flattened, v, "", keyDelimiter)

keys := make([]string, 0, len(flattened))

for key := range flattened {
keys = append(keys, key)
}

sort.Strings(keys)

var buf bytes.Buffer

for _, key := range keys {
_, err := buf.WriteString(fmt.Sprintf("%v=%v\n", strings.ToUpper(key), flattened[key]))
if err != nil {
return nil, err
}
}

return buf.Bytes(), nil
}

func (Codec) Decode(b []byte, v map[string]interface{}) error {
var buf bytes.Buffer

_, err := buf.Write(b)
if err != nil {
return err
}

env, err := gotenv.StrictParse(&buf)
if err != nil {
return err
}

for key, value := range env {
v[key] = value
}

return nil
}
63 changes: 63 additions & 0 deletions internal/encoding/dotenv/codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package dotenv

import (
"reflect"
"testing"
)

// original form of the data
const original = `# key-value pair
KEY=value
`

// encoded form of the data
const encoded = `KEY=value
`

// Viper's internal representation
var data = map[string]interface{}{
"KEY": "value",
}

func TestCodec_Encode(t *testing.T) {
codec := Codec{}

b, err := codec.Encode(data)
if err != nil {
t.Fatal(err)
}

if encoded != string(b) {
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
}
}

func TestCodec_Decode(t *testing.T) {
t.Run("OK", func(t *testing.T) {
codec := Codec{}

v := map[string]interface{}{}

err := codec.Decode([]byte(original), v)
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(data, v) {
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
}
})

t.Run("InvalidData", func(t *testing.T) {
codec := Codec{}

v := map[string]interface{}{}

err := codec.Decode([]byte(`invalid data`), v)
if err == nil {
t.Fatal("expected decoding to fail")
}

t.Logf("decoding failed as expected: %s", err)
})
}
41 changes: 41 additions & 0 deletions internal/encoding/dotenv/map_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dotenv

import (
"strings"

"github.com/spf13/cast"
)

// flattenAndMergeMap recursively flattens the given map into a new map
// Code is based on the function with the same name in tha main package.
// TODO: move it to a common place
func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
if shadow != nil && prefix != "" && shadow[prefix] != nil {
// prefix is shadowed => nothing more to flatten
return shadow
}
if shadow == nil {
shadow = make(map[string]interface{})
}

var m2 map[string]interface{}
if prefix != "" {
prefix += delimiter
}
for k, val := range m {
fullKey := prefix + k
switch val.(type) {
case map[string]interface{}:
m2 = val.(map[string]interface{})
case map[interface{}]interface{}:
m2 = cast.ToStringMap(val)
default:
// immediate value
shadow[strings.ToLower(fullKey)] = val
continue
}
// recursively merge to shadow map
shadow = flattenAndMergeMap(shadow, m2, fullKey, delimiter)
}
return shadow
}
4 changes: 2 additions & 2 deletions internal/encoding/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
// Encoder encodes the contents of v into a byte representation.
// It's primarily used for encoding a map[string]interface{} into a file format.
type Encoder interface {
Encode(v interface{}) ([]byte, error)
Encode(v map[string]interface{}) ([]byte, error)
}

const (
Expand Down Expand Up @@ -47,7 +47,7 @@ func (e *EncoderRegistry) RegisterEncoder(format string, enc Encoder) error {
return nil
}

func (e *EncoderRegistry) Encode(format string, v interface{}) ([]byte, error) {
func (e *EncoderRegistry) Encode(format string, v map[string]interface{}) ([]byte, error) {
e.mu.RLock()
encoder, ok := e.encoders[format]
e.mu.RUnlock()
Expand Down
12 changes: 6 additions & 6 deletions internal/encoding/encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type encoder struct {
b []byte
}

func (e encoder) Encode(_ interface{}) ([]byte, error) {
func (e encoder) Encode(_ map[string]interface{}) ([]byte, error) {
return e.b, nil
}

Expand Down Expand Up @@ -41,28 +41,28 @@ func TestEncoderRegistry_Decode(t *testing.T) {
t.Run("OK", func(t *testing.T) {
registry := NewEncoderRegistry()
encoder := encoder{
b: []byte("encoded value"),
b: []byte("key: value"),
}

err := registry.RegisterEncoder("myformat", encoder)
if err != nil {
t.Fatal(err)
}

b, err := registry.Encode("myformat", "some value")
b, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
if err != nil {
t.Fatal(err)
}

if string(b) != "encoded value" {
t.Fatalf("expected 'encoded value', got: %#v", string(b))
if string(b) != "key: value" {
t.Fatalf("expected 'key: value', got: %#v", string(b))
}
})

t.Run("EncoderNotFound", func(t *testing.T) {
registry := NewEncoderRegistry()

_, err := registry.Encode("myformat", "some value")
_, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
if err != ErrEncoderNotFound {
t.Fatalf("expected ErrEncoderNotFound, got: %v", err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/encoding/hcl/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// TODO: add printer config to the codec?
type Codec struct{}

func (Codec) Encode(v interface{}) ([]byte, error) {
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
Expand All @@ -35,6 +35,6 @@ func (Codec) Encode(v interface{}) ([]byte, error) {
return buf.Bytes(), nil
}

func (Codec) Decode(b []byte, v interface{}) error {
return hcl.Unmarshal(b, v)
func (Codec) Decode(b []byte, v map[string]interface{}) error {
return hcl.Unmarshal(b, &v)
}
Loading