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
139 changes: 139 additions & 0 deletions encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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
}

type encodingError string

func (e encodingError) Error() string {
return string(e)
}

const (
// ErrEncoderNotFound is returned when there is no encoder registered for a format.
ErrEncoderNotFound = encodingError("encoder not found for this format")
sagikazarmark marked this conversation as resolved.
Show resolved Hide resolved

// ErrDecoderNotFound is returned when there is no decoder registered for a format.
ErrDecoderNotFound = encodingError("decoder not found for this format")
)

// EncoderRegistry returns an [Encoder] for a given format.
//
// The error is [ErrEncoderNotFound] 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, error)
}

// DecoderRegistry returns an [Decoder] for a given format.
//
// The error is [ErrDecoderNotFound] if no [Decoder] is registered for the format.
type DecoderRegistry interface {
Decoder(format string) (Decoder, error)
}

// [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, error) {
encoder, ok := r.codec(format)
if !ok {
return nil, ErrEncoderNotFound
}

return encoder, nil
}

func (r codecRegistry) Decoder(format string) (Decoder, error) {
decoder, ok := r.codec(format)
if !ok {
return nil, ErrDecoderNotFound
}

return decoder, nil
}

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
}
22 changes: 20 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, err := v.decoderRegistry2.Decoder(format)
if err != nil {
return ConfigParseError{err}
}

err = decoder.Decode(buf.Bytes(), c)
if err != nil {
return ConfigParseError{err}
}
Expand All @@ -1730,7 +1743,12 @@ 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, err := v.encoderRegistry2.Encoder(configType)
if err != nil {
return ConfigMarshalError{err}
}

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