mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 16:58:31 +02:00
refactor(serialization): generalize unmarshal/load functions with pluggable format handlers
Replace YAML-specific functions with generic ones accepting unmarshaler/marshaler function parameters. This enables future support for JSON and other formats while maintaining current YAML behavior. - UnmarshalValidateYAML -> UnmarshalValidate(unmarshalFunc) - UnmarshalValidateYAMLXSync -> UnmarshalValidateXSync(unmarshalFunc) - SaveJSON -> SaveFile(marshalFunc) - LoadJSONIfExist -> LoadFileIfExist(unmarshalFunc) - Add UnmarshalValidateReader for reader-based decoding Testing: all 12 staged test files updated to use new API
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -25,9 +26,9 @@ func TestEABConfigRequired(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
yamlCfg := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||||
cfg := autocert.Config{}
|
cfg := autocert.Config{}
|
||||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
err := serialization.UnmarshalValidate(yamlCfg, &cfg, yaml.Unmarshal)
|
||||||
if (err != nil) != test.wantErr {
|
if (err != nil) != test.wantErr {
|
||||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -41,7 +42,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
|||||||
cfg.HTTPClient = acmeServer.httpClient()
|
cfg.HTTPClient = acmeServer.httpClient()
|
||||||
|
|
||||||
/* unmarshal yaml config with multiple certs */
|
/* unmarshal yaml config with multiple certs */
|
||||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
err := error(serialization.UnmarshalValidate(yamlConfig, &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||||
require.Len(t, cfg.Extra, 2)
|
require.Len(t, cfg.Extra, 2)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package autocert_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -42,7 +43,7 @@ extra:
|
|||||||
`
|
`
|
||||||
|
|
||||||
var cfg autocert.Config
|
var cfg autocert.Config
|
||||||
err := error(serialization.UnmarshalValidateYAML([]byte(cfgYAML), &cfg))
|
err := error(serialization.UnmarshalValidate([]byte(cfgYAML), &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (state *state) InitFromFile(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) Init(data []byte) error {
|
func (state *state) Init(data []byte) error {
|
||||||
err := serialization.UnmarshalValidateYAML(data, &state.Config)
|
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
@@ -43,7 +44,7 @@ type (
|
|||||||
|
|
||||||
func Validate(data []byte) gperr.Error {
|
func Validate(data []byte) gperr.Error {
|
||||||
var model Config
|
var model Config
|
||||||
return serialization.UnmarshalValidateYAML(data, &model)
|
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
|
|||||||
@@ -55,20 +55,20 @@ func init() {
|
|||||||
|
|
||||||
func InitCache() {
|
func InitCache() {
|
||||||
m := make(IconMap)
|
m := make(IconMap)
|
||||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, sonic.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// backward compatible
|
// backward compatible
|
||||||
oldFormat := struct {
|
oldFormat := struct {
|
||||||
Icons IconMap
|
Icons IconMap
|
||||||
LastUpdate time.Time
|
LastUpdate time.Time
|
||||||
}{}
|
}{}
|
||||||
err = serialization.LoadJSONIfExist(common.IconListCachePath, &oldFormat)
|
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, sonic.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to load icons")
|
log.Error().Err(err).Msg("failed to load icons")
|
||||||
} else {
|
} else {
|
||||||
m = oldFormat.Icons
|
m = oldFormat.Icons
|
||||||
// store it to disk immediately
|
// store it to disk immediately
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &m, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, sonic.Marshal)
|
||||||
}
|
}
|
||||||
} else if len(m) > 0 {
|
} else if len(m) > 0 {
|
||||||
log.Info().
|
log.Info().
|
||||||
@@ -84,7 +84,7 @@ func InitCache() {
|
|||||||
|
|
||||||
task.OnProgramExit("save_icons_cache", func() {
|
task.OnProgramExit("save_icons_cache", func() {
|
||||||
icons := iconsCache.Load()
|
icons := iconsCache.Load()
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &icons, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, sonic.Marshal)
|
||||||
})
|
})
|
||||||
|
|
||||||
go backgroundUpdateIcons()
|
go backgroundUpdateIcons()
|
||||||
@@ -105,7 +105,7 @@ func backgroundUpdateIcons() {
|
|||||||
// swap old cache with new cache
|
// swap old cache with new cache
|
||||||
iconsCache.Store(newCache)
|
iconsCache.Store(newCache)
|
||||||
// save it to disk
|
// save it to disk
|
||||||
err := serialization.SaveJSON(common.IconListCachePath, &newCache, 0o644)
|
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, sonic.Marshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("failed to save icons")
|
log.Warn().Err(err).Msg("failed to save icons")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ func loadNS[T store](ns namespace) T {
|
|||||||
func save() error {
|
func save() error {
|
||||||
errs := gperr.NewBuilder("failed to save data stores")
|
errs := gperr.NewBuilder("failed to save data stores")
|
||||||
for ns, store := range stores {
|
for ns, store := range stores {
|
||||||
if err := serialization.SaveJSON(filepath.Join(storesPath, string(ns)+".json"), &store, 0o644); err != nil {
|
path := filepath.Join(storesPath, string(ns)+".json")
|
||||||
|
if err := serialization.SaveFile(path, &store, 0o644, sonic.Marshal); err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package proxmox
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
)
|
)
|
||||||
@@ -43,7 +44,7 @@ func TestValidateCommandArgs(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var cfg NodeConfig
|
var cfg NodeConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte(tt.yamlCfg), &cfg)
|
err := serialization.UnmarshalValidate([]byte(tt.yamlCfg), &cfg, yaml.Unmarshal)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.ErrorContains(t, err, "input contains invalid characters")
|
require.ErrorContains(t, err, "input contains invalid characters")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
@@ -43,7 +44,7 @@ func removeXPrefix(m map[string]any) gperr.Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validate(data []byte) (routes route.Routes, err gperr.Error) {
|
func validate(data []byte) (routes route.Routes, err gperr.Error) {
|
||||||
err = serialization.UnmarshalValidateYAMLIntercept(data, &routes, removeXPrefix)
|
err = serialization.UnmarshalValidate(data, &routes, yaml.Unmarshal, removeXPrefix)
|
||||||
return routes, err
|
return routes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package serialization
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -85,6 +86,10 @@ func initPtr(dst reflect.Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate performs struct validation using go-playground/validator tags.
|
||||||
|
//
|
||||||
|
// It collects all validation errors and returns them as a single error.
|
||||||
|
// Field names in errors are prefixed with their namespace (e.g., "User.Email").
|
||||||
func ValidateWithFieldTags(s any) gperr.Error {
|
func ValidateWithFieldTags(s any) gperr.Error {
|
||||||
var errs gperr.Builder
|
var errs gperr.Builder
|
||||||
err := validate.Struct(s)
|
err := validate.Struct(s)
|
||||||
@@ -257,10 +262,11 @@ func initTypeKeyFieldIndexesMap(t reflect.Type) typeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapUnmarshalValidate takes a SerializedObject and a target value, and assigns the values in the SerializedObject to the target value.
|
// MapUnmarshalValidate takes a SerializedObject and a target value,
|
||||||
// MapUnmarshalValidate ignores case differences between the field names in the SerializedObject and the target.
|
// and assigns the values in the SerializedObject to the target value.
|
||||||
|
//
|
||||||
|
// It ignores case differences between the field names in the SerializedObject and the target.
|
||||||
//
|
//
|
||||||
// The target value must be a struct or a map[string]any.
|
|
||||||
// If the target value is a struct , and implements the MapUnmarshaller interface,
|
// If the target value is a struct , and implements the MapUnmarshaller interface,
|
||||||
// the UnmarshalMap method will be called.
|
// the UnmarshalMap method will be called.
|
||||||
//
|
//
|
||||||
@@ -455,6 +461,13 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
|
|||||||
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertSlice converts a source slice to a destination slice.
|
||||||
|
//
|
||||||
|
// - Elements are converted one by one using the Convert function.
|
||||||
|
// - Validation is performed on each element if checkValidateTag is true.
|
||||||
|
// - The destination slice is initialized with the source length.
|
||||||
|
// - On error, the destination slice is truncated to the number of
|
||||||
|
// successfully converted elements.
|
||||||
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
||||||
if dst.Kind() == reflect.Pointer {
|
if dst.Kind() == reflect.Pointer {
|
||||||
if dst.IsNil() && !dst.CanSet() {
|
if dst.IsNil() && !dst.CanSet() {
|
||||||
@@ -507,6 +520,12 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertString converts a string value to the destination reflect.Value.
|
||||||
|
// - It handles various types including numeric types, booleans, time.Duration,
|
||||||
|
// slices (comma-separated or YAML), maps, and structs (YAML).
|
||||||
|
// - If the destination implements the Parser interface, it is used for conversion.
|
||||||
|
// - Returns true if conversion was handled (even with error), false if
|
||||||
|
// conversion is unsupported.
|
||||||
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
||||||
convertible = true
|
convertible = true
|
||||||
dstT := dst.Type()
|
dstT := dst.Type()
|
||||||
@@ -618,48 +637,80 @@ func substituteEnv(data []byte) ([]byte, gperr.Error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error {
|
type (
|
||||||
|
marshalFunc func(src any) ([]byte, error)
|
||||||
|
unmarshalFunc func(data []byte, target any) error
|
||||||
|
newDecoderFunc func(r io.Reader) interface {
|
||||||
|
Decode(v any) error
|
||||||
|
}
|
||||||
|
interceptFunc func(m map[string]any) gperr.Error
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmarshalValidate unmarshals data into a map, applies optional intercept
|
||||||
|
// functions, and validates the result against the target struct using field tags.
|
||||||
|
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||||
|
// - The unmarshaler function converts data to a map[string]any.
|
||||||
|
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||||
|
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||||
data, err := substituteEnv(data)
|
data, err := substituteEnv(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
m := make(map[string]any)
|
||||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
if err := unmarshaler(data, &m); err != nil {
|
||||||
return gperr.Wrap(err)
|
return gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
for _, intercept := range interceptFns {
|
||||||
|
if err := intercept(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return MapUnmarshalValidate(m, target)
|
return MapUnmarshalValidate(m, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalValidateYAMLIntercept[T any](data []byte, target *T, intercept func(m map[string]any) gperr.Error) gperr.Error {
|
// UnmarshalValidateReader reads from an io.Reader, unmarshals to a map,
|
||||||
|
// - Applies optional intercept functions, and validates against the target struct.
|
||||||
|
// - Environment variables are substituted during reading using ${VAR} syntax.
|
||||||
|
// - The newDecoder function creates a decoder for the reader (e.g.,
|
||||||
|
// json.NewDecoder).
|
||||||
|
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err := newDecoder(NewSubstituteEnvReader(reader)).Decode(&m); err != nil {
|
||||||
|
return gperr.Wrap(err)
|
||||||
|
}
|
||||||
|
for _, intercept := range interceptFns {
|
||||||
|
if err := intercept(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MapUnmarshalValidate(m, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalValidateXSync unmarshals data into an xsync.Map[string, V].
|
||||||
|
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||||
|
// - The unmarshaler function converts data to a map[string]any.
|
||||||
|
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||||
|
// - Returns a thread-safe concurrent map with the unmarshaled values.
|
||||||
|
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error) {
|
||||||
data, err := substituteEnv(data)
|
data, err := substituteEnv(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
m := make(map[string]any)
|
||||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
if err := unmarshaler(data, &m); err != nil {
|
||||||
return gperr.Wrap(err)
|
return nil, gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
if err := intercept(m); err != nil {
|
for _, intercept := range interceptFns {
|
||||||
return err
|
if err := intercept(m); err != nil {
|
||||||
}
|
return nil, err
|
||||||
return MapUnmarshalValidate(m, target)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], err gperr.Error) {
|
|
||||||
data, err = substituteEnv(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
|
||||||
if err = gperr.Wrap(yaml.Unmarshal(data, &m)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m2 := make(map[string]V, len(m))
|
m2 := make(map[string]V, len(m))
|
||||||
if err = MapUnmarshalValidate(m, m2); err != nil {
|
if err = MapUnmarshalValidate(m, m2); err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
||||||
for k, v := range m2 {
|
for k, v := range m2 {
|
||||||
@@ -668,26 +719,27 @@ func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], er
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSerialized[T any](path string, dst *T, deserialize func(data []byte, dst any) error) error {
|
// SaveFile marshals a value to bytes and writes it to a file.
|
||||||
data, err := os.ReadFile(path)
|
// - The marshaler function converts the value to bytes.
|
||||||
if err != nil {
|
// - The file is written with the specified permissions.
|
||||||
return err
|
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error {
|
||||||
}
|
data, err := marshaler(src)
|
||||||
return deserialize(data, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveJSON[T any](path string, src *T, perm os.FileMode) error {
|
|
||||||
data, err := json.Marshal(src)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, perm)
|
return os.WriteFile(path, data, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadJSONIfExist[T any](path string, dst *T) error {
|
// LoadFileIfExist reads a file and unmarshals its contents to a value.
|
||||||
err := loadSerialized(path, dst, json.Unmarshal)
|
// - The unmarshaler function converts the bytes to a value.
|
||||||
if os.IsNotExist(err) {
|
// - If the file does not exist, nil is returned and dst remains unchanged.
|
||||||
return nil
|
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
return unmarshaler(data, dst)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
@@ -303,6 +304,6 @@ autocert:
|
|||||||
} `yaml:"options"`
|
} `yaml:"options"`
|
||||||
} `yaml:"autocert"`
|
} `yaml:"autocert"`
|
||||||
}
|
}
|
||||||
require.NoError(t, UnmarshalValidateYAML(data, &cfg))
|
require.NoError(t, UnmarshalValidate(data, &cfg, yaml.Unmarshal))
|
||||||
require.Equal(t, "test", cfg.Autocert.Options.AuthToken)
|
require.Equal(t, "test", cfg.Autocert.Options.AuthToken)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package types
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
)
|
)
|
||||||
@@ -10,14 +11,14 @@ import (
|
|||||||
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
|
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
|
||||||
t.Run("string", func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte("test: http://localhost:2375"), &cfg)
|
err := serialization.UnmarshalValidate([]byte("test: http://localhost:2375"), &cfg, yaml.Unmarshal)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
|
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("detailed", func(t *testing.T) {
|
t.Run("detailed", func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte(`
|
err := serialization.UnmarshalValidate([]byte(`
|
||||||
test:
|
test:
|
||||||
scheme: http
|
scheme: http
|
||||||
host: localhost
|
host: localhost
|
||||||
@@ -25,7 +26,7 @@ test:
|
|||||||
tls:
|
tls:
|
||||||
ca_file: /etc/ssl/ca.crt
|
ca_file: /etc/ssl/ca.crt
|
||||||
cert_file: /etc/ssl/cert.crt
|
cert_file: /etc/ssl/cert.crt
|
||||||
key_file: /etc/ssl/key.crt`), &cfg)
|
key_file: /etc/ssl/key.crt`), &cfg, yaml.Unmarshal)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
|
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
|
||||||
})
|
})
|
||||||
@@ -131,7 +132,7 @@ func TestDockerProviderConfigValidation(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte(test.yamlStr), &cfg)
|
err := serialization.UnmarshalValidate([]byte(test.yamlStr), &cfg, yaml.Unmarshal)
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user