2024-05-06 11:19:26 +02:00
|
|
|
package conf
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2025-01-15 01:49:44 +01:00
|
|
|
"errors"
|
2024-05-06 11:19:26 +02:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2025-01-15 01:49:44 +01:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
2024-05-06 11:19:26 +02:00
|
|
|
"github.com/pelletier/go-toml/v2"
|
2025-01-15 15:06:02 +01:00
|
|
|
"gopkg.in/yaml.v3"
|
2024-05-06 11:19:26 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
type filetype int
|
|
|
|
|
|
|
|
const (
|
|
|
|
typeInvalid filetype = iota
|
|
|
|
typeJSON
|
|
|
|
typeTOML
|
2025-01-15 01:49:44 +01:00
|
|
|
typeHCL
|
2025-01-15 15:06:02 +01:00
|
|
|
typeYAML
|
2024-05-06 11:19:26 +02:00
|
|
|
)
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// getType returns the type of the config file.
|
2024-05-06 11:19:26 +02:00
|
|
|
func getType(filename string) filetype {
|
|
|
|
switch {
|
|
|
|
case strings.HasSuffix(filename, ".json"):
|
|
|
|
return typeJSON
|
|
|
|
case strings.HasSuffix(filename, ".toml"):
|
|
|
|
return typeTOML
|
2025-01-15 01:49:44 +01:00
|
|
|
case strings.HasSuffix(filename, ".hcl"):
|
|
|
|
return typeHCL
|
2025-01-15 15:06:02 +01:00
|
|
|
case strings.HasSuffix(filename, ".yml") ||
|
|
|
|
strings.HasSuffix(filename, ".yaml"):
|
|
|
|
return typeYAML
|
2024-05-06 11:19:26 +02:00
|
|
|
default:
|
|
|
|
return typeInvalid
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// unmarshal unmarshals the given data to the given struct.
|
2025-01-15 01:49:44 +01:00
|
|
|
func unmarshal(filepath string, data []byte, v any) error {
|
|
|
|
ft := getType(filepath)
|
|
|
|
|
2024-05-06 11:19:26 +02:00
|
|
|
switch ft {
|
|
|
|
case typeJSON:
|
|
|
|
return unmarshalJSON(data, v)
|
|
|
|
case typeTOML:
|
|
|
|
return unmarshalTOML(data, v)
|
2025-01-15 01:49:44 +01:00
|
|
|
case typeHCL:
|
|
|
|
return unmarshalHCL(filepath, data, v)
|
2025-01-15 15:06:02 +01:00
|
|
|
case typeYAML:
|
|
|
|
return unmarshalYAML(data, v)
|
2024-05-06 11:19:26 +02:00
|
|
|
default:
|
|
|
|
return ErrUnsupportedFileType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// marshal marshals the given struct to bytes.
|
|
|
|
func marshal(ft filetype, v any) ([]byte, error) {
|
2024-05-06 11:19:26 +02:00
|
|
|
switch ft {
|
|
|
|
case typeJSON:
|
|
|
|
return marshalJSON(v)
|
|
|
|
case typeTOML:
|
|
|
|
return marshalTOML(v)
|
2025-01-15 01:49:44 +01:00
|
|
|
case typeHCL:
|
|
|
|
return marshalHCL(v)
|
2025-01-15 15:06:02 +01:00
|
|
|
case typeYAML:
|
|
|
|
return marshalYAML(v)
|
2024-05-06 11:19:26 +02:00
|
|
|
default:
|
|
|
|
return nil, ErrUnsupportedFileType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// unmarshalJSON unmarshals the given data to the given struct.
|
|
|
|
func unmarshalJSON(data []byte, v any) error {
|
2024-05-06 11:19:26 +02:00
|
|
|
err := json.Unmarshal(data, v)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot parse config file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// marshalJSON marshals the given struct to bytes.
|
|
|
|
func marshalJSON(v any) ([]byte, error) {
|
2024-05-06 11:19:26 +02:00
|
|
|
data, err := json.MarshalIndent(v, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot generate config content: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// unmarshalTOML unmarshals the given data to the given struct.
|
|
|
|
func unmarshalTOML(data []byte, v any) error {
|
2024-05-06 11:19:26 +02:00
|
|
|
err := toml.Unmarshal(data, v)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot parse config file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-01-14 01:06:47 +01:00
|
|
|
// marshalTOML marshals the given struct to bytes.
|
|
|
|
func marshalTOML(v any) ([]byte, error) {
|
2024-05-06 11:19:26 +02:00
|
|
|
data, err := toml.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot generate config content: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
}
|
2025-01-15 01:49:44 +01:00
|
|
|
|
|
|
|
// 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 fmt.Errorf("cannot parse config file: %w", newDiags)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// marshalHCL marshals the given struct to bytes.
|
2025-01-15 15:06:02 +01:00
|
|
|
func marshalHCL(v any) ([]byte, error) {
|
2025-01-15 01:49:44 +01:00
|
|
|
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
|
|
|
|
}
|
2025-01-15 15:06:02 +01:00
|
|
|
|
|
|
|
// 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 fmt.Errorf("cannot parse config file: %w", 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
|
|
|
|
}
|