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

// 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 {
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