Skip to content

Commit e4c9d28

Browse files
authored
Merge pull request moby#50852 from thaJeztah/add_compat_wrapper
daemon/internal: add "compat" package for legacy responses
2 parents c463327 + 33a05ac commit e4c9d28

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

daemon/internal/compat/compat.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Package compat provides tools for backward-compatible API responses.
2+
package compat
3+
4+
import "encoding/json"
5+
6+
// Wrapper augments a struct to add or omit fields for legacy JSON responses.
7+
type Wrapper struct {
8+
Base any
9+
10+
extraFields map[string]any
11+
omitFields []string
12+
}
13+
14+
// MarshalJSON merges the JSON with extra fields or omits fields.
15+
func (w *Wrapper) MarshalJSON() ([]byte, error) {
16+
base, err := json.Marshal(w.Base)
17+
if err != nil {
18+
return nil, err
19+
}
20+
if len(w.omitFields) == 0 && len(w.extraFields) == 0 {
21+
return base, nil
22+
}
23+
24+
var merged map[string]any
25+
if err := json.Unmarshal(base, &merged); err != nil {
26+
return nil, err
27+
}
28+
29+
for _, key := range w.omitFields {
30+
delete(merged, key)
31+
}
32+
for key, val := range w.extraFields {
33+
merged[key] = val
34+
}
35+
36+
return json.Marshal(merged)
37+
}
38+
39+
type options struct {
40+
extraFields map[string]any
41+
omitFields []string
42+
}
43+
44+
// Option for Wrapper.
45+
type Option func(*options)
46+
47+
// WithExtraFields adds fields to the marshaled output.
48+
func WithExtraFields(fields map[string]any) Option {
49+
return func(c *options) {
50+
if c.extraFields == nil {
51+
c.extraFields = make(map[string]any)
52+
}
53+
for k, v := range fields {
54+
c.extraFields[k] = v
55+
}
56+
}
57+
}
58+
59+
// WithOmittedFields removes fields from the marshaled output.
60+
func WithOmittedFields(fields ...string) Option {
61+
return func(c *options) {
62+
c.omitFields = append(c.omitFields, fields...)
63+
}
64+
}
65+
66+
// Wrap constructs a Wrapper from the given type.
67+
func Wrap(base any, opts ...Option) *Wrapper {
68+
cfg := options{extraFields: make(map[string]any)}
69+
for _, opt := range opts {
70+
opt(&cfg)
71+
}
72+
return &Wrapper{
73+
Base: base,
74+
extraFields: cfg.extraFields,
75+
omitFields: cfg.omitFields,
76+
}
77+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package compat_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/moby/moby/v2/daemon/internal/compat"
8+
)
9+
10+
type Info struct {
11+
Name string `json:"name"`
12+
Version string `json:"version"`
13+
NewField string `json:"newfield"`
14+
Nested *NestedStruct `json:"legacy,omitempty"`
15+
}
16+
17+
type NestedStruct struct {
18+
Field1 string `json:"field1"`
19+
Field2 int `json:"field2"`
20+
}
21+
22+
func TestWrap(t *testing.T) {
23+
info := &Info{
24+
Name: "daemon",
25+
Version: "v2.0",
26+
NewField: "new field",
27+
}
28+
29+
tests := []struct {
30+
name string
31+
options []compat.Option
32+
expected string
33+
}{
34+
{
35+
name: "none",
36+
expected: `{"name":"daemon","version":"v2.0","newfield":"new field"}`,
37+
},
38+
{
39+
name: "extra fields",
40+
options: []compat.Option{compat.WithExtraFields(map[string]any{"legacy_field": "hello"})},
41+
expected: `{"legacy_field":"hello","name":"daemon","newfield":"new field","version":"v2.0"}`,
42+
},
43+
{
44+
name: "omit fields",
45+
options: []compat.Option{compat.WithOmittedFields("newfield", "version")},
46+
expected: `{"name":"daemon"}`,
47+
},
48+
{
49+
name: "omit and extra fields",
50+
options: []compat.Option{
51+
compat.WithExtraFields(map[string]any{"legacy_field": "hello"}),
52+
compat.WithOmittedFields("newfield", "version"),
53+
},
54+
expected: `{"legacy_field":"hello","name":"daemon"}`,
55+
},
56+
{
57+
name: "replace field",
58+
options: []compat.Option{compat.WithExtraFields(map[string]any{"version": struct {
59+
Major, Minor int
60+
}{Major: 1, Minor: 0}})},
61+
expected: `{"name":"daemon","newfield":"new field","version":{"Major":1,"Minor":0}}`,
62+
},
63+
}
64+
for _, tc := range tests {
65+
t.Run(tc.name, func(t *testing.T) {
66+
resp := compat.Wrap(info, tc.options...)
67+
data, err := json.Marshal(resp)
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
if string(data) != tc.expected {
72+
t.Errorf("\nExpected: %s\nGot: %s", tc.expected, string(data))
73+
}
74+
})
75+
}
76+
}
77+
78+
func TestNestedCompat(t *testing.T) {
79+
info := &Info{
80+
Name: "daemon",
81+
Version: "v2.0",
82+
NewField: "new field",
83+
}
84+
85+
detail := &NestedStruct{
86+
Field1: "ok",
87+
Field2: 42,
88+
}
89+
nested := compat.Wrap(detail, compat.WithExtraFields(map[string]any{
90+
"legacy_field": "hello",
91+
}))
92+
resp := compat.Wrap(info, compat.WithExtraFields(map[string]any{
93+
"nested": nested,
94+
}))
95+
96+
data, err := json.Marshal(resp)
97+
if err != nil {
98+
t.Fatalf("Marshal failed: %v", err)
99+
}
100+
101+
const expected = `{"name":"daemon","nested":{"field1":"ok","field2":42,"legacy_field":"hello"},"newfield":"new field","version":"v2.0"}`
102+
if string(data) != expected {
103+
t.Errorf("\nExpected: %s\nGot: %s", expected, string(data))
104+
}
105+
}

0 commit comments

Comments
 (0)