Files
godoxy-yusing/internal/serialization/README.md
yusing 6da7227f9b refactor(errs): migrate from gperr.Error to standard Go error interface
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.
2026-02-08 12:07:36 +08:00

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)