mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-22 10:27:46 +01:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
364 lines
9.5 KiB
Markdown
364 lines
9.5 KiB
Markdown
# Serialization Package
|
|
|
|
Flexible, type-safe serialization/deserialization with validation support for GoDoxy configuration.
|
|
|
|
## Overview
|
|
|
|
### Purpose
|
|
|
|
This package provides robust YAML/JSON serialization with:
|
|
|
|
- Case-insensitive field matching using FNV-1a hashing
|
|
- Environment variable substitution (`${VAR}` syntax)
|
|
- Field-level validation with go-playground/validator tags
|
|
- Custom type conversion with pluggable format handlers
|
|
|
|
### Primary Consumers
|
|
|
|
- `internal/config/` - Configuration file loading
|
|
- `internal/autocert/` - ACME provider configuration
|
|
- `internal/route/` - Route configuration
|
|
|
|
### Non-goals
|
|
|
|
- Binary serialization (MsgPack, etc.)
|
|
- Schema evolution/migration
|
|
- Partial deserialization (unknown fields error)
|
|
|
|
### Stability
|
|
|
|
Internal package with stable public APIs. Exported functions are production-ready.
|
|
|
|
## Public API
|
|
|
|
### Core Types
|
|
|
|
```go
|
|
// Intermediate representation during deserialization
|
|
type SerializedObject = map[string]any
|
|
```
|
|
|
|
### Interfaces
|
|
|
|
```go
|
|
// For custom map unmarshaling logic
|
|
type MapUnmarshaller interface {
|
|
UnmarshalMap(m map[string]any) error
|
|
}
|
|
|
|
// For custom validation logic
|
|
type CustomValidator interface {
|
|
Validate() error
|
|
}
|
|
```
|
|
|
|
### Deserialization Functions
|
|
|
|
```go
|
|
// Generic unmarshal with pluggable format handler
|
|
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) error
|
|
|
|
// Read from io.Reader with format decoder
|
|
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) error
|
|
|
|
// Direct map deserialization
|
|
func MapUnmarshalValidate(src SerializedObject, dst any) error
|
|
|
|
// To xsync.Map with pluggable format handler
|
|
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], error)
|
|
```
|
|
|
|
### File I/O Functions
|
|
|
|
```go
|
|
// Write marshaled data to file
|
|
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error
|
|
|
|
// Read and unmarshal file if it exists
|
|
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error
|
|
```
|
|
|
|
### Conversion Functions
|
|
|
|
```go
|
|
// Convert any value to target reflect.Value
|
|
func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) error
|
|
|
|
// String to target type conversion
|
|
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr error)
|
|
```
|
|
|
|
### Validation Functions
|
|
|
|
```go
|
|
// Validate using struct tags
|
|
func ValidateWithFieldTags(s any) error
|
|
|
|
// Register custom validator
|
|
func MustRegisterValidation(tag string, fn validator.Func)
|
|
|
|
// Validate using CustomValidator interface
|
|
func ValidateWithCustomValidator(v reflect.Value) error
|
|
|
|
// Get underlying validator
|
|
func Validator() *validator.Validate
|
|
```
|
|
|
|
### Utility Functions
|
|
|
|
```go
|
|
// Register default value factory
|
|
func RegisterDefaultValueFactory[T any](factory func() *T)
|
|
|
|
// Convert map to SerializedObject
|
|
func ToSerializedObject[VT any](m map[string]VT) SerializedObject
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Data Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Caller
|
|
participant U as UnmarshalValidate
|
|
participant E as Env Substitution
|
|
participant F as Format Parser
|
|
participant M as MapUnmarshalValidate
|
|
participant T as Type Info Cache
|
|
participant CV as Convert
|
|
participant V as Validator
|
|
|
|
C->>U: Data bytes + target struct + format handler
|
|
U->>E: Substitute ${ENV} vars
|
|
E-->>U: Substituted bytes
|
|
U->>F: Parse with format handler (YAML/JSON)
|
|
F-->>U: map[string]any
|
|
U->>M: Map + target
|
|
M->>T: Get type info
|
|
loop For each field in map
|
|
M->>T: Lookup field by name (case-insensitive)
|
|
T-->>M: Field reflect.Value
|
|
M->>CV: Convert value to field type
|
|
CV-->>M: Converted value or error
|
|
end
|
|
M->>V: Validate struct tags
|
|
V-->>M: Validation errors
|
|
M-->>U: Combined errors
|
|
U-->>C: Result
|
|
```
|
|
|
|
### Component Interactions
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph Input Processing
|
|
Bytes[Data Bytes] --> EnvSub[Env Substitution]
|
|
EnvSub --> FormatParse[Format Parse]
|
|
FormatParse --> Map[map<string,any>]
|
|
end
|
|
|
|
subgraph Type Inspection
|
|
Map --> TypeInfo[Type Info Cache]
|
|
TypeInfo -.-> FieldLookup[Field Lookup]
|
|
end
|
|
|
|
subgraph Conversion
|
|
FieldLookup --> Convert[Convert Function]
|
|
Convert --> StringConvert[String Conversion]
|
|
Convert --> NumericConvert[Numeric Conversion]
|
|
Convert --> MapConvert[Map/Struct Conversion]
|
|
Convert --> SliceConvert[Slice Conversion]
|
|
end
|
|
|
|
subgraph Validation
|
|
Convert --> Validate[ValidateWithFieldTags]
|
|
Convert --> CustomValidate[Custom Validator]
|
|
CustomValidate --> CustomValidator[CustomValidator Interface]
|
|
end
|
|
```
|
|
|
|
### Field Tag Reference
|
|
|
|
| Tag | Purpose | Example |
|
|
| ------------- | ---------------------------------- | --------------------------- |
|
|
| `json` | Field name for serialization | `json:"auth_token"` |
|
|
| `deserialize` | Exclude field from deserialization | `deserialize:"-"` |
|
|
| `validate` | go-playground/validator tags | `validate:"required,email"` |
|
|
| `aliases` | Alternative field names | `aliases:"key,api_key"` |
|
|
|
|
## Configuration Surface
|
|
|
|
### Supported Field Types
|
|
|
|
- Primitives (string, int, bool, float)
|
|
- Pointers to primitives
|
|
- Slices of primitives
|
|
- Maps with string keys
|
|
- Nested structs
|
|
- Time.Duration (with extended units: `d`, `w`, `M`)
|
|
|
|
### Environment Variable Substitution
|
|
|
|
```yaml
|
|
autocert:
|
|
auth_token: ${CLOUDFLARE_AUTH_TOKEN}
|
|
# Lookup order: GODOXY_VAR, GOPROXY_VAR, VAR
|
|
```
|
|
|
|
### String Conversion Formats
|
|
|
|
| Type | Format Examples |
|
|
| ---------- | --------------------------- |
|
|
| Duration | `1h30m`, `2d`, `1w`, `3M` |
|
|
| Numeric | `123`, `0xFF`, `-42` |
|
|
| Slice | `a,b,c` or YAML list format |
|
|
| Map/Struct | YAML key: value format |
|
|
|
|
## Dependency and Integration Map
|
|
|
|
### External Dependencies
|
|
|
|
- `github.com/goccy/go-yaml` - YAML parsing
|
|
- `github.com/go-playground/validator/v10` - Validation
|
|
- `github.com/puzpuzpuz/xsync/v4` - Type cache
|
|
- `github.com/bytedance/sonic` - JSON operations
|
|
|
|
### Internal Dependencies
|
|
|
|
- `github.com/yusing/goutils/errs` - Error handling
|
|
- `github.com/yusing/gointernals` - Reflection utilities
|
|
|
|
## Observability
|
|
|
|
### Errors
|
|
|
|
All errors use `gperr` with structured subjects:
|
|
|
|
```go
|
|
ErrUnknownField.Subject("field_name").With(gperr.DoYouMeanField("field_name", ["fieldName"]))
|
|
ErrValidationError.Subject("Namespace").Withf("required")
|
|
ErrUnsupportedConversion.Subjectf("string to int")
|
|
```
|
|
|
|
## Performance Characteristics
|
|
|
|
| Operation | Complexity | Notes |
|
|
| ---------------- | ---------- | -------------------------------- |
|
|
| Type info lookup | O(1) | Cached in xsync.Map |
|
|
| Field matching | O(1) | FNV-1a hash lookup |
|
|
| Conversion | O(n) | n = number of fields |
|
|
| Validation | O(n) | n = number of validatable fields |
|
|
|
|
## Failure Modes and Recovery
|
|
|
|
| Failure Mode | Result | Recovery |
|
|
| ------------------ | ---------------------- | ------------------------ |
|
|
| Unknown field | Error with suggestions | Fix config field name |
|
|
| Validation failure | Structured error | Fix field value |
|
|
| Type mismatch | Error | Check field type |
|
|
| Missing env var | Error | Set environment variable |
|
|
| Invalid format | Error | Fix YAML/JSON syntax |
|
|
|
|
## Usage Examples
|
|
|
|
### YAML Deserialization
|
|
|
|
```go
|
|
type ServerConfig struct {
|
|
Host string `json:"host" validate:"required,hostname_port"`
|
|
Port int `json:"port" validate:"required,min=1,max=65535"`
|
|
MaxConns int `json:"max_conns"`
|
|
TLSEnabled bool `json:"tls_enabled"`
|
|
}
|
|
|
|
yamlData := []byte(`
|
|
host: localhost
|
|
port: 8080
|
|
max_conns: 100
|
|
tls_enabled: true
|
|
`)
|
|
|
|
var config ServerConfig
|
|
if err := serialization.UnmarshalValidate(yamlData, &config, yaml.Unmarshal); err != nil {
|
|
panic(err)
|
|
}
|
|
```
|
|
|
|
### JSON Deserialization
|
|
|
|
```go
|
|
var config ServerConfig
|
|
if err := serialization.UnmarshalValidate(jsonData, &config, json.Unmarshal); err != nil {
|
|
panic(err)
|
|
}
|
|
```
|
|
|
|
### Custom Validator
|
|
|
|
```go
|
|
type Config struct {
|
|
URL string `json:"url" validate:"required"`
|
|
}
|
|
|
|
func (c *Config) Validate() error {
|
|
if !strings.HasPrefix(c.URL, "https://") {
|
|
return errors.New("url must use https")
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Custom Type with Parser Interface
|
|
|
|
```go
|
|
type Duration struct {
|
|
Value int
|
|
Unit string
|
|
}
|
|
|
|
func (d *Duration) Parse(v string) error {
|
|
// custom parsing logic
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Reading from File
|
|
|
|
```go
|
|
var config ServerConfig
|
|
if err := serialization.LoadFileIfExist("config.yml", &config, yaml.Unmarshal); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Save back to file
|
|
if err := serialization.SaveFile("config.yml", &config, 0644, yaml.Marshal); err != nil {
|
|
panic(err)
|
|
}
|
|
```
|
|
|
|
### Reading from io.Reader
|
|
|
|
```go
|
|
var config ServerConfig
|
|
file, _ := os.Open("config.yml")
|
|
defer file.Close()
|
|
if err := serialization.UnmarshalValidateReader(file, &config, yaml.NewDecoder); err != nil {
|
|
panic(err)
|
|
}
|
|
```
|
|
|
|
## Testing Notes
|
|
|
|
- `serialization_test.go` - Core functionality tests
|
|
- `validation_*_test.go` - Tag validation tests
|
|
- Golden files for complex configurations
|
|
- Tests cover:
|
|
- Case-insensitive field matching
|
|
- Anonymous struct handling
|
|
- Pointer primitives
|
|
- String conversions
|
|
- Environment substitution
|
|
- Custom validators
|
|
- Multiple format handlers (YAML/JSON)
|