conf/adapters.go

244 lines
4.9 KiB
Go

package conf
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"unicode"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pelletier/go-toml/v2"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v3"
)
func parseError(err error) error {
return fmt.Errorf("cannot parse config file: %w", err)
}
type filetype int
const (
typeInvalid filetype = iota
typeJSON
typeTOML
typeHCL
typeYAML
typeINI
)
// getType returns the type of the config file.
func getType(filename string) filetype {
switch {
case strings.HasSuffix(filename, ".json"):
return typeJSON
case strings.HasSuffix(filename, ".toml"):
return typeTOML
case strings.HasSuffix(filename, ".hcl"):
return typeHCL
case strings.HasSuffix(filename, ".yml") ||
strings.HasSuffix(filename, ".yaml"):
return typeYAML
case strings.HasSuffix(filename, ".ini") ||
strings.HasSuffix(filename, ".cfg"):
return typeINI
default:
return typeInvalid
}
}
// unmarshal unmarshals the given data to the given struct.
func unmarshal(filepath string, data []byte, v any) error {
ft := getType(filepath)
switch ft {
case typeJSON:
return unmarshalJSON(data, v)
case typeTOML:
return unmarshalTOML(data, v)
case typeHCL:
return unmarshalHCL(filepath, data, v)
case typeYAML:
return unmarshalYAML(data, v)
case typeINI:
return unmarshalINI(data, v)
default:
return ErrUnsupportedFileType
}
}
// marshal marshals the given struct to bytes.
func marshal(ft filetype, v any) ([]byte, error) {
switch ft {
case typeJSON:
return marshalJSON(v)
case typeTOML:
return marshalTOML(v)
case typeHCL:
return marshalHCL(v)
case typeYAML:
return marshalYAML(v)
case typeINI:
return marshalINI(v)
default:
return nil, ErrUnsupportedFileType
}
}
// unmarshalJSON unmarshals the given data to the given struct.
func unmarshalJSON(data []byte, v any) error {
err := json.Unmarshal(data, v)
if err != nil {
return parseError(err)
}
return nil
}
// marshalJSON marshals the given struct to bytes.
func marshalJSON(v any) ([]byte, error) {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return nil, fmt.Errorf("cannot generate config content: %w", err)
}
return data, nil
}
// unmarshalTOML unmarshals the given data to the given struct.
func unmarshalTOML(data []byte, v any) error {
err := toml.Unmarshal(data, v)
if err != nil {
return parseError(err)
}
return nil
}
// marshalTOML marshals the given struct to bytes.
func marshalTOML(v any) ([]byte, error) {
data, err := toml.Marshal(v)
if err != nil {
return nil, fmt.Errorf("cannot generate config content: %w", err)
}
return data, nil
}
// unmarshalHCL unmarshals the given data to the given struct.
func unmarshalHCL(filepath string, data []byte, v any) error {
err := hclsimple.Decode(filepath, data, nil, v)
var diags hcl.Diagnostics
errors.As(err, &diags)
newDiags := hclFilterDiagnostics(diags)
if len(newDiags) > 0 {
return parseError(newDiags)
}
return nil
}
// marshalHCL marshals the given struct to bytes.
func marshalHCL(v any) ([]byte, error) {
f := hclwrite.NewEmptyFile()
gohcl.EncodeIntoBody(v, f.Body())
return f.Bytes(), nil
}
func hclFilterDiagnostics(diags hcl.Diagnostics) hcl.Diagnostics {
var newDiags hcl.Diagnostics
for _, diag := range diags {
if diag.Summary != "Unsupported argument" {
newDiags = append(newDiags, diag)
}
}
if len(newDiags) > 0 {
return newDiags
}
return nil
}
// unmarshalYAML unmarshals the given data to the given struct.
func unmarshalYAML(data []byte, v any) error {
err := yaml.Unmarshal(data, v)
if err != nil {
return parseError(err)
}
return nil
}
// marshalYAML marshals the given struct to bytes.
func marshalYAML(v any) ([]byte, error) {
data, err := yaml.Marshal(v)
if err != nil {
return nil, fmt.Errorf("cannot generate config content: %w", err)
}
return data, nil
}
// unmarshalINI unmarshals the given data to the given struct.
func unmarshalINI(data []byte, v any) error {
opts := ini.LoadOptions{}
cfg, err := ini.LoadSources(opts, data)
if err != nil {
return parseError(err)
}
cfg.NameMapper = iniNameMapper
err = cfg.MapTo(v)
if err != nil {
return parseError(err)
}
return nil
}
// marshalYAML marshals the given struct to bytes.
func marshalINI(v any) ([]byte, error) {
cfg := ini.Empty()
val := reflect.ValueOf(v)
if val.Kind() != reflect.Ptr {
v = &v
}
err := ini.ReflectFromWithMapper(cfg, v, iniNameMapper)
if err != nil {
return nil, fmt.Errorf("cannot generate config content: %w", err)
}
var buf bytes.Buffer
cfg.WriteTo(&buf)
data := buf.Bytes()
return data, nil
}
func iniNameMapper(raw string) string {
newstr := make([]rune, 0, len(raw))
for i, chr := range raw {
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
if i > 0 {
newstr = append(newstr, '_')
}
}
newstr = append(newstr, unicode.ToLower(chr))
}
return string(newstr)
}