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" ) const errMsgConfigGen = "cannot generate config content: %w" 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(errMsgConfigGen, 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(errMsgConfigGen, 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(errMsgConfigGen, 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 from struct: %w", err) } var buf bytes.Buffer _, err = cfg.WriteTo(&buf) if err != nil { return nil, fmt.Errorf(errMsgConfigGen, err) } return buf.Bytes(), 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) }