mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-23 23:28:17 +03:00
next: add json merge patch utils
This commit is contained in:
parent
dab608292a
commit
38f7491069
7 changed files with 121 additions and 46 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
internal/next/jsonpatch/jsonpatch.go
Normal file
43
internal/next/jsonpatch/jsonpatch.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
29
internal/next/jsonpatch/jsonpatch_test.go
Normal file
29
internal/next/jsonpatch/jsonpatch_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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" \
|
||||||
|
|
Loading…
Reference in a new issue