From 38f7491069d90d4080e7ad98b09bf9ce19138195 Mon Sep 17 00:00:00 2001 From: Ainar Garipov Date: Tue, 10 Dec 2024 19:57:54 +0300 Subject: [PATCH] next: add json merge patch utils --- internal/next/agh/agh.go | 1 + internal/next/jsonpatch/jsonpatch.go | 43 +++++++++++++++++++ internal/next/jsonpatch/jsonpatch_test.go | 29 +++++++++++++ internal/next/websvc/dns.go | 51 +++++++++++------------ internal/next/websvc/dns_test.go | 2 +- internal/next/websvc/http.go | 34 +++++++-------- scripts/make/go-build.sh | 7 ++++ 7 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 internal/next/jsonpatch/jsonpatch.go create mode 100644 internal/next/jsonpatch/jsonpatch_test.go diff --git a/internal/next/agh/agh.go b/internal/next/agh/agh.go index 2248bc81..baf825af 100644 --- a/internal/next/agh/agh.go +++ b/internal/next/agh/agh.go @@ -13,6 +13,7 @@ import ( type ServiceWithConfig[ConfigType any] interface { service.Interface + // Config returns a deep clone of the configuration of the service. Config() (c ConfigType) } diff --git a/internal/next/jsonpatch/jsonpatch.go b/internal/next/jsonpatch/jsonpatch.go new file mode 100644 index 00000000..b5e10044 --- /dev/null +++ b/internal/next/jsonpatch/jsonpatch.go @@ -0,0 +1,43 @@ +// Package jsonpatch contains utilities for JSON Merge Patch APIs. +// +// See https://www.rfc-editor.org/rfc/rfc7396. +package jsonpatch + +import ( + "bytes" + "encoding/json" + + "github.com/AdguardTeam/golibs/errors" +) + +// NonRemovable is a type that prevents JSON null from being used to try and +// remove a value. +type NonRemovable[T any] struct { + Value T + IsSet bool +} + +// type check +var _ json.Unmarshaler = (*NonRemovable[struct{}])(nil) + +// UnmarshalJSON implements the [json.Unmarshaler] interface for *NonRemovable. +func (v *NonRemovable[T]) UnmarshalJSON(b []byte) (err error) { + if v == nil { + return errors.Error("jsonpatch.NonRemovable is nil") + } + + if bytes.Equal(b, []byte("null")) { + return errors.Error("property cannot be removed") + } + + v.IsSet = true + + return json.Unmarshal(b, &v.Value) +} + +// Set sets ptr if the value has been provided. +func (v NonRemovable[T]) Set(ptr *T) { + if v.IsSet { + *ptr = v.Value + } +} diff --git a/internal/next/jsonpatch/jsonpatch_test.go b/internal/next/jsonpatch/jsonpatch_test.go new file mode 100644 index 00000000..3f9537de --- /dev/null +++ b/internal/next/jsonpatch/jsonpatch_test.go @@ -0,0 +1,29 @@ +package jsonpatch_test + +import ( + "encoding/json" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch" + "github.com/AdguardTeam/golibs/testutil" + "github.com/stretchr/testify/assert" +) + +func TestNonRemovable(t *testing.T) { + type T struct { + Value jsonpatch.NonRemovable[int] `json:"value"` + } + + var v T + + err := json.Unmarshal([]byte(`{"value":null}`), &v) + testutil.AssertErrorMsg(t, "property cannot be removed", err) + + err = json.Unmarshal([]byte(`{"value":42}`), &v) + assert.NoError(t, err) + + var got int + v.Value.Set(&got) + + assert.Equal(t, 42, got) +} diff --git a/internal/next/websvc/dns.go b/internal/next/websvc/dns.go index 9c2a222f..cf185e2c 100644 --- a/internal/next/websvc/dns.go +++ b/internal/next/websvc/dns.go @@ -5,10 +5,9 @@ import ( "fmt" "net/http" "net/netip" - "time" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" - "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch" ) // ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns @@ -16,13 +15,15 @@ import ( type ReqPatchSettingsDNS struct { // TODO(a.garipov): Add more as we go. - Addresses []netip.AddrPort `json:"addresses"` - BootstrapServers []string `json:"bootstrap_servers"` - UpstreamServers []string `json:"upstream_servers"` - DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"` - UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"` - BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"` - UseDNS64 bool `json:"use_dns64"` + Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"` + BootstrapServers jsonpatch.NonRemovable[[]string] `json:"bootstrap_servers"` + UpstreamServers jsonpatch.NonRemovable[[]string] `json:"upstream_servers"` + DNS64Prefixes jsonpatch.NonRemovable[[]netip.Prefix] `json:"dns64_prefixes"` + + UpstreamTimeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:"upstream_timeout"` + + BootstrapPreferIPv6 jsonpatch.NonRemovable[bool] `json:"bootstrap_prefer_ipv6"` + UseDNS64 jsonpatch.NonRemovable[bool] `json:"use_dns64"` } // HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the @@ -42,13 +43,7 @@ type HTTPAPIDNSSettings struct { // handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP // API. func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) { - req := &ReqPatchSettingsDNS{ - Addresses: []netip.AddrPort{}, - BootstrapServers: []string{}, - UpstreamServers: []string{}, - } - - // TODO(a.garipov): Validate nulls and proper JSON patch. + req := &ReqPatchSettingsDNS{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -57,16 +52,20 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques return } - newConf := &dnssvc.Config{ - Logger: svc.logger, - Addresses: req.Addresses, - BootstrapServers: req.BootstrapServers, - UpstreamServers: req.UpstreamServers, - DNS64Prefixes: req.DNS64Prefixes, - UpstreamTimeout: time.Duration(req.UpstreamTimeout), - BootstrapPreferIPv6: req.BootstrapPreferIPv6, - UseDNS64: req.UseDNS64, - } + dnsSvc := svc.confMgr.DNS() + newConf := dnsSvc.Config() + + // TODO(a.garipov): Add more as we go. + + req.Addresses.Set(&newConf.Addresses) + req.BootstrapServers.Set(&newConf.BootstrapServers) + req.UpstreamServers.Set(&newConf.UpstreamServers) + req.DNS64Prefixes.Set(&newConf.DNS64Prefixes) + + req.UpstreamTimeout.Set((*aghhttp.JSONDuration)(&newConf.UpstreamTimeout)) + + req.BootstrapPreferIPv6.Set(&newConf.BootstrapPreferIPv6) + req.UseDNS64.Set(&newConf.UseDNS64) ctx := r.Context() err = svc.confMgr.UpdateDNS(ctx, newConf) diff --git a/internal/next/websvc/dns_test.go b/internal/next/websvc/dns_test.go index bb546778..1965b3e6 100644 --- a/internal/next/websvc/dns_test.go +++ b/internal/next/websvc/dns_test.go @@ -41,7 +41,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) { return nil }, OnShutdown: func(_ context.Context) (err error) { panic("not implemented") }, - OnConfig: func() (c *dnssvc.Config) { panic("not implemented") }, + OnConfig: func() (c *dnssvc.Config) { return &dnssvc.Config{} }, } } confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) { diff --git a/internal/next/websvc/http.go b/internal/next/websvc/http.go index 3fe8bce7..3c123831 100644 --- a/internal/next/websvc/http.go +++ b/internal/next/websvc/http.go @@ -10,6 +10,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch" "github.com/AdguardTeam/golibs/logutil/slogutil" ) @@ -20,9 +21,12 @@ type ReqPatchSettingsHTTP struct { // // TODO(a.garipov): Add wait time. - Addresses []netip.AddrPort `json:"addresses"` - SecureAddresses []netip.AddrPort `json:"secure_addresses"` - Timeout aghhttp.JSONDuration `json:"timeout"` + Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"` + SecureAddresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"secure_addresses"` + + Timeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:"timeout"` + + ForceHTTPS jsonpatch.NonRemovable[bool] `json:"force_https"` } // HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the @@ -41,8 +45,6 @@ type HTTPAPIHTTPSettings struct { func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) { req := &ReqPatchSettingsHTTP{} - // TODO(a.garipov): Validate nulls and proper JSON patch. - err := json.NewDecoder(r.Body).Decode(&req) if err != nil { aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err)) @@ -50,20 +52,14 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque return } - newConf := &Config{ - Logger: svc.logger, - Pprof: &PprofConfig{ - Port: svc.pprofPort, - Enabled: svc.pprof != nil, - }, - ConfigManager: svc.confMgr, - Frontend: svc.frontend, - TLS: svc.tls, - Addresses: req.Addresses, - SecureAddresses: req.SecureAddresses, - Timeout: time.Duration(req.Timeout), - ForceHTTPS: svc.forceHTTPS, - } + newConf := svc.Config() + + // TODO(a.garipov): Add more as we go. + + req.Addresses.Set(&newConf.Addresses) + req.SecureAddresses.Set(&newConf.SecureAddresses) + req.Timeout.Set((*aghhttp.JSONDuration)(&newConf.Timeout)) + req.ForceHTTPS.Set(&newConf.ForceHTTPS) aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{ Addresses: newConf.Addresses, diff --git a/scripts/make/go-build.sh b/scripts/make/go-build.sh index 9a7459b4..f10cbfac 100644 --- a/scripts/make/go-build.sh +++ b/scripts/make/go-build.sh @@ -133,7 +133,14 @@ if [ "$verbose" -gt '0' ]; then "$go" env fi +if [ "${COVER:-0}" -eq '1' ]; then + cover_flags='--cover=1' +else + cover_flags='--cover=0' +fi + "$go" build \ + "$cover_flags" \ --ldflags="$ldflags" \ "$race_flags" \ "$tags_flags" \