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

New encoding layer #1869

Merged
merged 9 commits into from
Jun 24, 2024
113 changes: 113 additions & 0 deletions encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package viper

import (
"github.com/spf13/viper/internal/encoding/dotenv"
"github.com/spf13/viper/internal/encoding/hcl"
"github.com/spf13/viper/internal/encoding/ini"
"github.com/spf13/viper/internal/encoding/javaproperties"
"github.com/spf13/viper/internal/encoding/json"
"github.com/spf13/viper/internal/encoding/toml"
"github.com/spf13/viper/internal/encoding/yaml"
)

// Encoder encodes Viper's internal data structures into a byte representation.
// It's primarily used for encoding a map[string]any into a file format.
type Encoder interface {
Encode(v map[string]any) ([]byte, error)
}

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

// Codec combines [Encoder] and [Decoder] interfaces.
type Codec interface {
Encoder
Decoder
}

// EncoderRegistry returns an [Encoder] for a given format.
// The second return value is false if no [Encoder] is registered for the format.
type EncoderRegistry interface {
sagikazarmark marked this conversation as resolved.
Show resolved Hide resolved
Encoder(format string) (Encoder, bool)
sagikazarmark marked this conversation as resolved.
Show resolved Hide resolved
}

// DecoderRegistry returns an [Decoder] for a given format.
// The second return value is false if no [Decoder] is registered for the format.
type DecoderRegistry interface {
Decoder(format string) (Decoder, bool)
}

// [CodecRegistry] combines [EncoderRegistry] and [DecoderRegistry] interfaces.
type CodecRegistry interface {
EncoderRegistry
DecoderRegistry
}

// WithEncoderRegistry sets a custom [EncoderRegistry].
func WithEncoderRegistry(r EncoderRegistry) Option {
return optionFunc(func(v *Viper) {
v.encoderRegistry2 = r
})
}

// WithDecoderRegistry sets a custom [DecoderRegistry].
func WithDecoderRegistry(r DecoderRegistry) Option {
return optionFunc(func(v *Viper) {
v.decoderRegistry2 = r
})
}

// WithCodecRegistry sets a custom [EncoderRegistry] and [DecoderRegistry].
func WithCodecRegistry(r CodecRegistry) Option {
return optionFunc(func(v *Viper) {
v.encoderRegistry2 = r
v.decoderRegistry2 = r
})
}

type codecRegistry struct {
v *Viper
}

func (r codecRegistry) Encoder(format string) (Encoder, bool) {
return r.codec(format)
}

func (r codecRegistry) Decoder(format string) (Decoder, bool) {
return r.codec(format)
}

func (r codecRegistry) codec(format string) (Codec, bool) {
switch format {
case "yaml", "yml":
return yaml.Codec{}, true

case "json":
return json.Codec{}, true

case "toml":
return toml.Codec{}, true

case "hcl", "tfvars":
return hcl.Codec{}, true

case "ini":
return ini.Codec{
KeyDelimiter: r.v.keyDelim,
LoadOptions: r.v.iniLoadOptions,
}, true

case "properties", "props", "prop": // Note: This breaks writing a properties file.
return &javaproperties.Codec{
KeyDelimiter: v.keyDelim,
}, true

case "dotenv", "env":
return &dotenv.Codec{}, true
}

return nil, false
}
23 changes: 21 additions & 2 deletions viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ type Viper struct {
encoderRegistry *encoding.EncoderRegistry
decoderRegistry *encoding.DecoderRegistry

encoderRegistry2 EncoderRegistry
decoderRegistry2 DecoderRegistry

experimentalFinder bool
experimentalBindStruct bool
}
Expand All @@ -217,6 +220,11 @@ func New() *Viper {
v.typeByDefValue = false
v.logger = slog.New(&discardHandler{})

codecRegistry := codecRegistry{v: v}

v.encoderRegistry2 = codecRegistry
v.decoderRegistry2 = codecRegistry

v.resetEncoding()

v.experimentalFinder = features.Finder
Expand Down Expand Up @@ -1715,7 +1723,12 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error {

switch format := strings.ToLower(v.getConfigType()); format {
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "properties", "props", "prop", "dotenv", "env":
err := v.decoderRegistry.Decode(format, buf.Bytes(), c)
decoder, ok := v.decoderRegistry2.Decoder(format)
if !ok {
return ConfigParseError{errors.New("decoder not found")}
}

err := decoder.Decode(buf.Bytes(), c)
sagikazarmark marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return ConfigParseError{err}
}
Expand All @@ -1730,7 +1743,13 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error {
c := v.AllSettings()
switch configType {
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "prop", "props", "properties", "dotenv", "env":
b, err := v.encoderRegistry.Encode(configType, c)
encoder, ok := v.encoderRegistry2.Encoder(configType)
if !ok {
// TODO: return a proper error
return ConfigMarshalError{errors.New("encoder not found")}
}

b, err := encoder.Encode(c)
if err != nil {
return ConfigMarshalError{err}
}
Expand Down
6 changes: 3 additions & 3 deletions viper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1849,11 +1849,11 @@ var jsonWriteExpected = []byte(`{
"type": "donut"
}`)

var propertiesWriteExpected = []byte(`p_id = 0001
p_type = donut
var propertiesWriteExpected = []byte(`p_batters.batter.type = Regular
p_id = 0001
p_name = Cake
p_ppu = 0.55
p_batters.batter.type = Regular
p_type = donut
`)

// var yamlWriteExpected = []byte(`age: 35
Expand Down