next: add json merge patch utils

This commit is contained in:
Ainar Garipov 2024-12-10 19:57:54 +03:00
parent dab608292a
commit 38f7491069
7 changed files with 121 additions and 46 deletions

View File

@ -13,6 +13,7 @@ import (
type ServiceWithConfig[ConfigType any] interface { type ServiceWithConfig[ConfigType any] interface {
service.Interface service.Interface
// Config returns a deep clone of the configuration of the service.
Config() (c ConfigType) Config() (c ConfigType)
} }

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -5,10 +5,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/netip" "net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "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 // ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
@ -16,13 +15,15 @@ import (
type ReqPatchSettingsDNS struct { type ReqPatchSettingsDNS struct {
// TODO(a.garipov): Add more as we go. // TODO(a.garipov): Add more as we go.
Addresses []netip.AddrPort `json:"addresses"` Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"`
BootstrapServers []string `json:"bootstrap_servers"` BootstrapServers jsonpatch.NonRemovable[[]string] `json:"bootstrap_servers"`
UpstreamServers []string `json:"upstream_servers"` UpstreamServers jsonpatch.NonRemovable[[]string] `json:"upstream_servers"`
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"` DNS64Prefixes jsonpatch.NonRemovable[[]netip.Prefix] `json:"dns64_prefixes"`
UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"` UpstreamTimeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:"upstream_timeout"`
UseDNS64 bool `json:"use_dns64"`
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 // 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 // handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
// API. // API.
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) { func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsDNS{ req := &ReqPatchSettingsDNS{}
Addresses: []netip.AddrPort{},
BootstrapServers: []string{},
UpstreamServers: []string{},
}
// TODO(a.garipov): Validate nulls and proper JSON patch.
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
@ -57,16 +52,20 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
return return
} }
newConf := &dnssvc.Config{ dnsSvc := svc.confMgr.DNS()
Logger: svc.logger, newConf := dnsSvc.Config()
Addresses: req.Addresses,
BootstrapServers: req.BootstrapServers, // TODO(a.garipov): Add more as we go.
UpstreamServers: req.UpstreamServers,
DNS64Prefixes: req.DNS64Prefixes, req.Addresses.Set(&newConf.Addresses)
UpstreamTimeout: time.Duration(req.UpstreamTimeout), req.BootstrapServers.Set(&newConf.BootstrapServers)
BootstrapPreferIPv6: req.BootstrapPreferIPv6, req.UpstreamServers.Set(&newConf.UpstreamServers)
UseDNS64: req.UseDNS64, 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() ctx := r.Context()
err = svc.confMgr.UpdateDNS(ctx, newConf) err = svc.confMgr.UpdateDNS(ctx, newConf)

View File

@ -41,7 +41,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
return nil return nil
}, },
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") }, 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) { confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {

View File

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh" "github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
) )
@ -20,9 +21,12 @@ type ReqPatchSettingsHTTP struct {
// //
// TODO(a.garipov): Add wait time. // TODO(a.garipov): Add wait time.
Addresses []netip.AddrPort `json:"addresses"` Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"`
SecureAddresses []netip.AddrPort `json:"secure_addresses"` SecureAddresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"secure_addresses"`
Timeout aghhttp.JSONDuration `json:"timeout"`
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 // 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) { func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsHTTP{} req := &ReqPatchSettingsHTTP{}
// TODO(a.garipov): Validate nulls and proper JSON patch.
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err)) aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
@ -50,20 +52,14 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
return return
} }
newConf := &Config{ newConf := svc.Config()
Logger: svc.logger,
Pprof: &PprofConfig{ // TODO(a.garipov): Add more as we go.
Port: svc.pprofPort,
Enabled: svc.pprof != nil, req.Addresses.Set(&newConf.Addresses)
}, req.SecureAddresses.Set(&newConf.SecureAddresses)
ConfigManager: svc.confMgr, req.Timeout.Set((*aghhttp.JSONDuration)(&newConf.Timeout))
Frontend: svc.frontend, req.ForceHTTPS.Set(&newConf.ForceHTTPS)
TLS: svc.tls,
Addresses: req.Addresses,
SecureAddresses: req.SecureAddresses,
Timeout: time.Duration(req.Timeout),
ForceHTTPS: svc.forceHTTPS,
}
aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{ aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{
Addresses: newConf.Addresses, Addresses: newConf.Addresses,

View File

@ -133,7 +133,14 @@ if [ "$verbose" -gt '0' ]; then
"$go" env "$go" env
fi fi
if [ "${COVER:-0}" -eq '1' ]; then
cover_flags='--cover=1'
else
cover_flags='--cover=0'
fi
"$go" build \ "$go" build \
"$cover_flags" \
--ldflags="$ldflags" \ --ldflags="$ldflags" \
"$race_flags" \ "$race_flags" \
"$tags_flags" \ "$tags_flags" \