blob: d80f14afe5d6eba71e088ecc0c2c754974d88afe [file] [edit]
// Package compat provides tools for backward-compatible API responses.
package compat
import (
"bytes"
"encoding/json"
)
// Wrapper augments a struct to add or omit fields for legacy JSON responses.
type Wrapper struct {
Base any
extraFields map[string]any
omitFields []string
}
// MarshalJSON merges the JSON with extra fields or omits fields.
func (w *Wrapper) MarshalJSON() ([]byte, error) {
base, err := toJSON(w.Base)
if err != nil {
return nil, err
}
if len(w.omitFields) == 0 && len(w.extraFields) == 0 {
return base, nil
}
var merged map[string]any
if err := json.Unmarshal(base, &merged); err != nil {
return nil, err
}
for _, key := range w.omitFields {
delete(merged, key)
}
appendFields(w.extraFields, merged)
return toJSON(merged)
}
func appendFields(src, dst map[string]any) {
for k, v := range src {
if vmap, ok := v.(map[string]any); ok {
if dmap, ok := dst[k].(map[string]any); ok {
appendFields(vmap, dmap)
continue
}
}
if existing, ok := dst[k]; !ok || existing == nil {
dst[k] = v
}
}
}
func toJSON(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
out, _ := bytes.CutSuffix(buf.Bytes(), []byte{'\n'})
return out, nil
}
type options struct {
extraFields map[string]any
omitFields []string
}
// Option for Wrapper.
type Option func(*options)
// WithExtraFields adds extra JSON object fields to the marshaled output.
// The merge is recursive and additive-only: if a key already exists in
// the output, its existing value is preserved and the incoming value is
// ignored. For nested objects, missing keys are created as needed
// (depth-first traversal).
//
// If a conflict occurs between an object and a non-object at the same key,
// the existing (output) value is kept.
//
// Repeated calls accumulate; on conflicts, earlier calls win.
// This affects only the marshaled JSON, and does not mutate the source struct.
func WithExtraFields(fields map[string]any) Option {
return func(c *options) {
if c.extraFields == nil {
c.extraFields = make(map[string]any)
}
appendFields(fields, c.extraFields)
}
}
// WithOmittedFields removes fields from the marshaled output.
func WithOmittedFields(fields ...string) Option {
return func(c *options) {
c.omitFields = append(c.omitFields, fields...)
}
}
// Wrap constructs a Wrapper from the given type.
func Wrap(base any, opts ...Option) *Wrapper {
cfg := options{extraFields: make(map[string]any)}
for _, opt := range opts {
opt(&cfg)
}
return &Wrapper{
Base: base,
extraFields: cfg.extraFields,
omitFields: cfg.omitFields,
}
}