feat(render): add render package (#5751)

### Summary

Add `render` package

#### Related Issues / PR's

https://github.com/SigNoz/signoz/pull/5710
This commit is contained in:
Vibhu Pandey 2024-08-23 13:07:10 +05:30 committed by GitHub
parent 43ed49f9d9
commit bd7d14b1ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 219 additions and 1 deletions

View File

@ -4,6 +4,10 @@ import (
"fmt" "fmt"
) )
const (
codeUnknown string = "unknown"
)
// base is the fundamental struct that implements the error interface. // base is the fundamental struct that implements the error interface.
// The order of the struct is 'TCMEUA'. // The order of the struct is 'TCMEUA'.
type base struct { type base struct {
@ -100,7 +104,7 @@ func Unwrapb(cause error) (typ, string, string, error, string, []string) {
return base.t, base.c, base.m, base.e, base.u, base.a return base.t, base.c, base.m, base.e, base.u, base.a
} }
return TypeInternal, "", cause.Error(), cause, "", []string{} return TypeInternal, codeUnknown, cause.Error(), cause, "", []string{}
} }
// Ast checks if the provided error matches the specified custom error type. // Ast checks if the provided error matches the specified custom error type.

83
pkg/http/render/render.go Normal file
View File

@ -0,0 +1,83 @@
package render
import (
"net/http"
jsoniter "github.com/json-iterator/go"
"go.signoz.io/signoz/pkg/errors"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type response struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Error *responseerror `json:"error,omitempty"`
}
type responseerror struct {
Code string `json:"code"`
Message string `json:"message"`
Url string `json:"url,omitempty"`
Errors []responseerroradditional `json:"errors,omitempty"`
}
type responseerroradditional struct {
Message string `json:"message"`
}
func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
body, err := json.Marshal(&response{Status: StatusSuccess.s, Data: data})
if err != nil {
Error(rw, err)
return
}
if httpCode == 0 {
httpCode = http.StatusOK
}
rw.WriteHeader(httpCode)
_, _ = rw.Write(body)
}
func Error(rw http.ResponseWriter, cause error) {
// See if this is an instance of the base error or not
t, c, m, _, u, a := errors.Unwrapb(cause)
// Derive the http code from the error type
httpCode := http.StatusInternalServerError
switch t {
case errors.TypeInvalidInput:
httpCode = http.StatusBadRequest
case errors.TypeNotFound:
httpCode = http.StatusNotFound
case errors.TypeAlreadyExists:
httpCode = http.StatusConflict
case errors.TypeUnauthenticated:
httpCode = http.StatusUnauthorized
}
rea := make([]responseerroradditional, len(a))
for k, v := range a {
rea[k] = responseerroradditional{v}
}
body, err := json.Marshal(&response{
Status: StatusError.s,
Error: &responseerror{
Code: c,
Url: u,
Message: m,
Errors: rea,
},
})
if err != nil {
// this should never be the case
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.WriteHeader(httpCode)
_, _ = rw.Write(body)
}

View File

@ -0,0 +1,116 @@
package render
import (
"context"
"fmt"
"io"
"net"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/errors"
)
func TestSuccess(t *testing.T) {
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
data := map[string]any{
"int64": int64(9),
"string": "string",
"bool": true,
}
marshalled, err := json.Marshal(data)
require.NoError(t, err)
expected := []byte(fmt.Sprintf(`{"status":"success","data":%s}`, string(marshalled)))
server := &http.Server{
Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
Success(rw, http.StatusAccepted, data)
}),
}
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Shutdown(context.Background())
}()
req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.Equal(t, expected, actual)
}
func TestError(t *testing.T) {
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
testCases := map[string]struct {
name string
statusCode int
err error
expected []byte
}{
"/already_exists": {
name: "AlreadyExists",
statusCode: http.StatusConflict,
err: errors.New(errors.TypeAlreadyExists, "already_exists", "already exists").WithUrl("https://already_exists"),
expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
},
"/unauthenticated": {
name: "Unauthenticated",
statusCode: http.StatusUnauthorized,
err: errors.New(errors.TypeUnauthenticated, "not_allowed", "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"),
expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
},
}
server := &http.Server{
Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
tc, ok := testCases[req.URL.Path]
if ok {
Error(rw, tc.err)
return
}
}),
}
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Shutdown(context.Background())
}()
for path, tc := range testCases {
t.Run("", func(t *testing.T) {
req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+path, nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, tc.statusCode, res.StatusCode)
assert.Equal(t, tc.expected, actual)
})
}
}

View File

@ -0,0 +1,9 @@
package render
var (
StatusSuccess status = status{"success"}
StatusError = status{"error"}
)
// Defines custom error types
type status struct{ s string }

View File

@ -238,6 +238,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
return aH, nil return aH, nil
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
type structuredResponse struct { type structuredResponse struct {
Data interface{} `json:"data"` Data interface{} `json:"data"`
Total int `json:"total"` Total int `json:"total"`
@ -246,11 +247,13 @@ type structuredResponse struct {
Errors []structuredError `json:"errors"` Errors []structuredError `json:"errors"`
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
type structuredError struct { type structuredError struct {
Code int `json:"code,omitempty"` Code int `json:"code,omitempty"`
Msg string `json:"msg"` Msg string `json:"msg"`
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
type ApiResponse struct { type ApiResponse struct {
Status status `json:"status"` Status status `json:"status"`
Data interface{} `json:"data,omitempty"` Data interface{} `json:"data,omitempty"`
@ -258,6 +261,7 @@ type ApiResponse struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) { func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) {
json := jsoniter.ConfigCompatibleWithStandardLibrary json := jsoniter.ConfigCompatibleWithStandardLibrary
b, err := json.Marshal(&ApiResponse{ b, err := json.Marshal(&ApiResponse{
@ -301,6 +305,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa
} }
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
func writeHttpResponse(w http.ResponseWriter, data interface{}) { func writeHttpResponse(w http.ResponseWriter, data interface{}) {
json := jsoniter.ConfigCompatibleWithStandardLibrary json := jsoniter.ConfigCompatibleWithStandardLibrary
b, err := json.Marshal(&ApiResponse{ b, err := json.Marshal(&ApiResponse{
@ -351,6 +356,7 @@ func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *AuthMid
subRouter.HandleFunc("/metric/metric_metadata", am.ViewAccess(aH.getMetricMetadata)).Methods(http.MethodGet) subRouter.HandleFunc("/metric/metric_metadata", am.ViewAccess(aH.getMetricMetadata)).Methods(http.MethodGet)
} }
// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure
func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
writeHttpResponse(w, data) writeHttpResponse(w, data)
} }