-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4d62ab9
commit 5ac8051
Showing
17 changed files
with
349 additions
and
365 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package kcd | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
) | ||
|
||
type inputError struct { | ||
message string | ||
fieldType reflect.Type | ||
field string | ||
extractor string | ||
err error | ||
} | ||
|
||
func (b *inputError) withMessage(msg string) *inputError { | ||
b.message = msg | ||
return b | ||
} | ||
|
||
func (b *inputError) withErr(err error) *inputError { | ||
b.err = err | ||
return b | ||
} | ||
|
||
// ErrorDescription implements the builtin error interface for inputError. | ||
func (b inputError) Error() string { | ||
if b.field != "" && b.fieldType != nil { | ||
return fmt.Sprintf( | ||
"binding error on field '%s' of type '%s': %s", | ||
b.field, | ||
b.fieldType.Name(), | ||
b.message, | ||
) | ||
} | ||
return fmt.Sprintf("binding error: %s", b.message) | ||
} | ||
|
||
type outputError struct { | ||
Err error | ||
} | ||
|
||
// ErrorDescription implements the builtin error interface for outputError. | ||
func (o outputError) Error() string { | ||
return "unable to marshal response into json format" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package kcd | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/go-chi/chi" | ||
) | ||
|
||
// defaultHeaderExtractor is an extractor that operates on the headers | ||
// of a request. | ||
func defaultHeaderExtractor(_ http.ResponseWriter, r *http.Request, tag string) ([]string, error) { | ||
header := r.Header.Get(tag) | ||
|
||
return []string{header}, nil | ||
} | ||
|
||
// defaultQueryExtractor is an extractor that operates on the path | ||
// parameters of a request. | ||
func defaultQueryExtractor(_ http.ResponseWriter, r *http.Request, tag string) ([]string, error) { | ||
var params []string | ||
query := r.URL.Query()[tag] | ||
|
||
splitFn := func(c rune) bool { | ||
return c == ',' | ||
} | ||
|
||
for _, q := range query { | ||
params = append(params, strings.FieldsFunc(q, splitFn)...) | ||
} | ||
|
||
return params, nil | ||
} | ||
|
||
// defaultPathExtractor is an extractor that operates on the path | ||
// parameters of a request. | ||
func defaultPathExtractor(_ http.ResponseWriter, r *http.Request, tag string) ([]string, error) { | ||
p := chi.URLParam(r, tag) | ||
|
||
return []string{p}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package kcd | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
|
||
"github.com/expectedsh/errors" | ||
"github.com/go-chi/chi/middleware" | ||
validation "github.com/go-ozzo/ozzo-validation/v4" | ||
) | ||
|
||
// defaultValidateHook is the default validation hook. | ||
// It use 'ozzo-validation' to validate structure. | ||
// A structure must implement 'ValidatableWithContext' or 'Validatable' | ||
func defaultValidateHook(ctx context.Context, input interface{}) error { | ||
var err validation.Errors | ||
|
||
switch v := input.(type) { | ||
case validation.ValidatableWithContext: | ||
err = v.ValidateWithContext(ctx).(validation.Errors) | ||
case validation.Validatable: | ||
err = v.Validate().(validation.Errors) | ||
} | ||
|
||
if len(err) == 0 { | ||
return nil | ||
} | ||
|
||
return errors. | ||
NewWithKind(errors.KindInvalidArgument, "the request has one or multiple invalid fields"). | ||
WithMetadata("kcd.fields", err) | ||
} | ||
|
||
// defaultRenderHook is the default render hook. | ||
// It marshals the payload to JSON, or returns an empty body if the payload is nil. | ||
func defaultRenderHook(w http.ResponseWriter, _ *http.Request, statusCode int, response interface{}) error { | ||
if response != nil { | ||
marshal, err := json.Marshal(response) | ||
if err != nil { | ||
return outputError{Err: err} | ||
} | ||
|
||
w.WriteHeader(statusCode) | ||
if _, err := w.Write(marshal); err != nil { | ||
return err | ||
} | ||
|
||
} else { | ||
w.WriteHeader(statusCode) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// errorResponse is the default response that send the default error hook | ||
type errorResponse struct { | ||
ErrorDescription string `json:"error_description"` | ||
Error errors.Kind `json:"error"` | ||
|
||
Fields map[string]string `json:"fields,omitempty"` | ||
Metadata map[string]interface{} `json:"metadata,omitempty"` | ||
} | ||
|
||
// defaultErrorHook is the default error hook. | ||
// It check the error and return the corresponding response to the client. | ||
func defaultErrorHook(w http.ResponseWriter, r *http.Request, err error) { | ||
|
||
response := errorResponse{ | ||
ErrorDescription: "internal server error", | ||
Fields: map[string]string{}, | ||
Metadata: map[string]interface{}{}, | ||
} | ||
|
||
reqID := middleware.GetReqID(r.Context()) | ||
if reqID != "" { | ||
response.Metadata["request_id"] = reqID | ||
} | ||
|
||
switch e := err.(type) { | ||
case *errors.Error: | ||
w.WriteHeader(e.Kind.ToStatusCode()) | ||
|
||
response.ErrorDescription = e.Message | ||
response.Error = e.Kind | ||
|
||
// todo: don't use string literal for kcd.* | ||
|
||
metadata, ok := e.GetMetadata(errorKeyFields) | ||
if ok { | ||
m, okMap := metadata.(validation.Errors) | ||
if okMap { | ||
for k, v := range m { | ||
response.Fields[k] = v.Error() | ||
} | ||
} | ||
} | ||
|
||
metadata, ok = e.GetMetadata(errorKeyMetadata) | ||
if ok { | ||
m, okMap := metadata.(map[string]interface{}) | ||
if okMap { | ||
for k, v := range m { | ||
response.Metadata[k] = v | ||
} | ||
} | ||
} | ||
|
||
case inputError: | ||
w.WriteHeader(http.StatusBadRequest) | ||
response.Error = errors.KindInvalidArgument | ||
|
||
switch e.extractor { | ||
case queryTag, pathTag, headerTag: | ||
response.ErrorDescription = http.StatusText(http.StatusBadRequest) | ||
response.Fields[e.field] = fmt.Sprintf("with %s parameter: %s", e.fieldType, e.message) | ||
case jsonTag: | ||
response.ErrorDescription = e.message | ||
} | ||
case outputError: | ||
w.WriteHeader(http.StatusInternalServerError) | ||
|
||
response.Error = errors.KindInternal | ||
response.ErrorDescription = e.Error() | ||
} | ||
|
||
// todo: use a log hook to log kcd real (critic) error | ||
|
||
marshal, err := json.Marshal(response) | ||
if err != nil { | ||
return | ||
} | ||
|
||
_, _ = w.Write(marshal) | ||
|
||
return | ||
} | ||
|
||
// defaultBindHook returns a Bind hook with the default logic, with configurable MaxBodyBytes. | ||
func defaultBindHook(maxBodyBytes int64) BindHook { | ||
return func(w http.ResponseWriter, r *http.Request, in interface{}) error { | ||
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) | ||
if r.ContentLength == 0 { | ||
return nil | ||
} | ||
|
||
bytesBody, err := ioutil.ReadAll(r.Body) | ||
if err != nil { | ||
return inputError{extractor: jsonTag, message: "unable to read body"} | ||
} | ||
|
||
if err := json.Unmarshal(bytesBody, in); err != nil { | ||
return inputError{extractor: jsonTag, message: "unable to unmarshal request"} | ||
} | ||
|
||
return nil | ||
} | ||
} |
Oops, something went wrong.