all: sync with master; upd chlog
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
package aghhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -61,23 +60,3 @@ func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainTe
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteJSONResponse sets the content-type header in w.Header() to
|
||||
// "application/json", writes a header with a "200 OK" status, encodes resp to
|
||||
// w, calls [Error] on any returned error, and returns it as well.
|
||||
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
|
||||
return WriteJSONResponseCode(w, r, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
|
||||
// redefine the status code.
|
||||
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
||||
w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
|
||||
w.WriteHeader(code)
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package aghhttp
|
||||
|
||||
// HTTP header value constants.
|
||||
const (
|
||||
HdrValApplicationJSON = "application/json"
|
||||
HdrValTextPlain = "text/plain"
|
||||
HdrValApplicationJSON = "application/json"
|
||||
HdrValStrictTransportSecurity = "max-age=31536000; includeSubDomains"
|
||||
HdrValTextPlain = "text/plain"
|
||||
)
|
||||
|
||||
142
internal/aghhttp/json.go
Normal file
142
internal/aghhttp/json.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package aghhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||
// into JSON according to our API conventions.
|
||||
type JSONDuration time.Duration
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONDuration(0)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||
// always nil.
|
||||
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("json duration is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||
// according to our API conventions.
|
||||
type JSONTime time.Time
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONTime{}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||
// always nil.
|
||||
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||
if t == nil {
|
||||
return fmt.Errorf("json time is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteJSONResponse writes headers with the code, encodes resp into w, and logs
|
||||
// any errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, code int, resp any) {
|
||||
h := w.Header()
|
||||
h.Set(httphdr.ContentType, HdrValApplicationJSON)
|
||||
h.Set(httphdr.Server, UserAgent())
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
err := json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
log.Error("aghhttp: writing json resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteJSONResponseOK writes headers with the code 200 OK, encodes v into w,
|
||||
// and logs any errors it encounters. r is used to get additional information
|
||||
// from the request.
|
||||
func WriteJSONResponseOK(w http.ResponseWriter, r *http.Request, v any) {
|
||||
WriteJSONResponse(w, r, http.StatusOK, v)
|
||||
}
|
||||
|
||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||
// definition in the OpenAPI specification.
|
||||
type ErrorCode string
|
||||
|
||||
// ErrorCode constants.
|
||||
//
|
||||
// TODO(a.garipov): Expand and document codes.
|
||||
const (
|
||||
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||
ErrorCodeTMP000 = ""
|
||||
)
|
||||
|
||||
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||
// specification.
|
||||
type HTTPAPIErrorResp struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// WriteJSONResponseError encodes err as a JSON error into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func WriteJSONResponseError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("aghhttp: writing json error to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
|
||||
WriteJSONResponse(w, r, http.StatusUnprocessableEntity, &HTTPAPIErrorResp{
|
||||
Code: ErrorCodeTMP000,
|
||||
Msg: err.Error(),
|
||||
})
|
||||
}
|
||||
114
internal/aghhttp/json_test.go
Normal file
114
internal/aghhttp/json_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package aghhttp_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testJSONTime is the JSON time for tests.
|
||||
var testJSONTime = aghhttp.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||
|
||||
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||
const testJSONTimeStr = "1234567890123.456"
|
||||
|
||||
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
in aghhttp.JSONTime
|
||||
want []byte
|
||||
}{{
|
||||
name: "unix_zero",
|
||||
wantErrMsg: "",
|
||||
in: aghhttp.JSONTime(time.Unix(0, 0)),
|
||||
want: []byte("0"),
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: "",
|
||||
in: aghhttp.JSONTime{},
|
||||
want: []byte("-6795364578871.345"),
|
||||
}, {
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
in: testJSONTime,
|
||||
want: []byte(testJSONTimeStr),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.in.MarshalJSON()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
in := &struct {
|
||||
A aghhttp.JSONTime
|
||||
}{
|
||||
A: testJSONTime,
|
||||
}
|
||||
|
||||
got, err := json.Marshal(in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
want aghhttp.JSONTime
|
||||
data []byte
|
||||
}{{
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
want: testJSONTime,
|
||||
data: []byte(testJSONTimeStr),
|
||||
}, {
|
||||
name: "bad",
|
||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||
`invalid syntax`,
|
||||
want: aghhttp.JSONTime{},
|
||||
data: []byte(`{}`),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got aghhttp.JSONTime
|
||||
err := got.UnmarshalJSON(tc.data)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
err := (*aghhttp.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||
require.Error(t, err)
|
||||
|
||||
msg := err.Error()
|
||||
assert.Equal(t, "json time is nil", msg)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
want := testJSONTime
|
||||
var got struct {
|
||||
A aghhttp.JSONTime
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, got.A)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user