diff --git a/api/error.defaults.go b/api/error.defaults.go index 583126f..4a1e9e6 100644 --- a/api/error.defaults.go +++ b/api/error.defaults.go @@ -3,129 +3,82 @@ package api import "net/http" var ( - // ErrorUnknown represents any error which cause is unknown. + // ErrUnknown represents any error which cause is unknown. // It might also be used for debug purposes as this error // has to be used the less possible - ErrorUnknown Error = -1 + ErrUnknown = Err{-1, "unknown error", http.StatusOK} - // ErrorSuccess represents a generic successful service execution - ErrorSuccess Error = 0 + // ErrSuccess represents a generic successful service execution + ErrSuccess = Err{0, "all right", http.StatusOK} - // ErrorFailure is the most generic error - ErrorFailure Error = 1 + // ErrFailure is the most generic error + ErrFailure = Err{1, "it failed", http.StatusInternalServerError} - // ErrorNoMatchFound has to be set when trying to fetch data and there is no result - ErrorNoMatchFound Error = 2 + // ErrNoMatchFound is set when trying to fetch data and there is no result + ErrNoMatchFound = Err{2, "resource not found", http.StatusOK} - // ErrorAlreadyExists has to be set when trying to insert data, but identifiers or + // ErrAlreadyExists is set when trying to insert data, but identifiers or // unique fields already exists - ErrorAlreadyExists Error = 3 + ErrAlreadyExists = Err{3, "already exists", http.StatusOK} - // ErrorCreation has to be set when there is a creation/insert error - ErrorCreation Error = 4 + // ErrCreation is set when there is a creation/insert error + ErrCreation = Err{4, "create error", http.StatusOK} - // ErrorModification has to be set when there is an update/modification error - ErrorModification Error = 5 + // ErrModification is set when there is an update/modification error + ErrModification = Err{5, "update error", http.StatusOK} - // ErrorDeletion has to be set when there is a deletion/removal error - ErrorDeletion Error = 6 + // ErrDeletion is set when there is a deletion/removal error + ErrDeletion = Err{6, "delete error", http.StatusOK} - // ErrorTransaction has to be set when there is a transactional error - ErrorTransaction Error = 7 + // ErrTransaction is set when there is a transactional error + ErrTransaction = Err{7, "transactional error", http.StatusOK} - // ErrorUpload has to be set when a file upload failed - ErrorUpload Error = 100 + // ErrUpload is set when a file upload failed + ErrUpload = Err{100, "upload failed", http.StatusInternalServerError} - // ErrorDownload has to be set when a file download failed - ErrorDownload Error = 101 + // ErrDownload is set when a file download failed + ErrDownload = Err{101, "download failed", http.StatusInternalServerError} - // MissingDownloadHeaders has to be set when the implementation + // MissingDownloadHeaders is set when the implementation // of a service of type 'download' (which returns a file instead of // a set or output fields) is missing its HEADER field - MissingDownloadHeaders Error = 102 + MissingDownloadHeaders = Err{102, "download headers are missing", http.StatusBadRequest} - // ErrorMissingDownloadBody has to be set when the implementation + // ErrMissingDownloadBody is set when the implementation // of a service of type 'download' (which returns a file instead of // a set or output fields) is missing its BODY field - ErrorMissingDownloadBody Error = 103 + ErrMissingDownloadBody = Err{103, "download body is missing", http.StatusBadRequest} - // ErrorUnknownService is set when there is no service matching + // ErrUnknownService is set when there is no service matching // the http request URI. - ErrorUnknownService Error = 200 + ErrUnknownService = Err{200, "unknown service", http.StatusServiceUnavailable} - // ErrorUncallableService is set when there the requested service's + // ErrUncallableService is set when there the requested service's // implementation (plugin file) is not found/callable - ErrorUncallableService Error = 202 + ErrUncallableService = Err{202, "uncallable service", http.StatusServiceUnavailable} - // ErrorNotImplemented is set when a handler is not implemented yet - ErrorNotImplemented Error = 203 + // ErrNotImplemented is set when a handler is not implemented yet + ErrNotImplemented = Err{203, "not implemented", http.StatusNotImplemented} - // ErrorPermission is set when there is a permission error by default + // ErrPermission is set when there is a permission error by default // the api returns a permission error when the current scope (built // by middlewares) does not match the scope required in the config. // You can add your own permission policy and use this error - ErrorPermission Error = 300 + ErrPermission = Err{300, "permission error", http.StatusUnauthorized} - // ErrorToken has to be set (usually in authentication middleware) to tell + // ErrToken is set (usually in authentication middleware) to tell // the user that this authentication token is expired or invalid - ErrorToken Error = 301 + ErrToken = Err{301, "token error", http.StatusForbidden} - // ErrorMissingParam is set when a *required* parameter is missing from the + // ErrMissingParam is set when a *required* parameter is missing from the // http request - ErrorMissingParam Error = 400 + ErrMissingParam = Err{400, "missing parameter", http.StatusBadRequest} - // ErrorInvalidParam is set when a given parameter fails its type check as + // ErrInvalidParam is set when a given parameter fails its type check as // defined in the config file. - ErrorInvalidParam Error = 401 + ErrInvalidParam = Err{401, "invalid parameter", http.StatusBadRequest} - // ErrorInvalidDefaultParam is set when an optional parameter's default value + // ErrInvalidDefaultParam is set when an optional parameter's default value // does not match its type. - ErrorInvalidDefaultParam Error = 402 + ErrInvalidDefaultParam = Err{402, "invalid default param", http.StatusBadRequest} ) - -var errorReasons = map[Error]string{ - ErrorUnknown: "unknown error", - ErrorSuccess: "all right", - ErrorFailure: "it failed", - ErrorNoMatchFound: "resource not found", - ErrorAlreadyExists: "already exists", - ErrorCreation: "create error", - ErrorModification: "update error", - ErrorDeletion: "delete error", - ErrorTransaction: "transactional error", - ErrorUpload: "upload failed", - ErrorDownload: "download failed", - MissingDownloadHeaders: "download headers are missing", - ErrorMissingDownloadBody: "download body is missing", - ErrorUnknownService: "unknown service", - ErrorUncallableService: "uncallable service", - ErrorNotImplemented: "not implemented", - ErrorPermission: "permission error", - ErrorToken: "token error", - ErrorMissingParam: "missing parameter", - ErrorInvalidParam: "invalid parameter", - ErrorInvalidDefaultParam: "invalid default param", -} -var errorStatus = map[Error]int{ - ErrorUnknown: http.StatusOK, - ErrorSuccess: http.StatusOK, - ErrorFailure: http.StatusInternalServerError, - ErrorNoMatchFound: http.StatusOK, - ErrorAlreadyExists: http.StatusOK, - ErrorCreation: http.StatusOK, - ErrorModification: http.StatusOK, - ErrorDeletion: http.StatusOK, - ErrorTransaction: http.StatusOK, - ErrorUpload: http.StatusInternalServerError, - ErrorDownload: http.StatusInternalServerError, - MissingDownloadHeaders: http.StatusBadRequest, - ErrorMissingDownloadBody: http.StatusBadRequest, - ErrorUnknownService: http.StatusServiceUnavailable, - ErrorUncallableService: http.StatusServiceUnavailable, - ErrorNotImplemented: http.StatusNotImplemented, - ErrorPermission: http.StatusUnauthorized, - ErrorToken: http.StatusForbidden, - ErrorMissingParam: http.StatusBadRequest, - ErrorInvalidParam: http.StatusBadRequest, - ErrorInvalidDefaultParam: http.StatusBadRequest, -} diff --git a/api/error.go b/api/error.go index 52f97f7..36d8800 100644 --- a/api/error.go +++ b/api/error.go @@ -1,49 +1,21 @@ package api import ( - "encoding/json" "fmt" - "net/http" ) -// Error represents an http response error following the api format. +// Err represents an http response error following the api format. // These are used by the services to set the *execution status* // directly into the response as JSON alongside response output fields. -type Error int - -func (e Error) Error() string { - reason, ok := errorReasons[e] - if !ok { - return ErrorUnknown.Error() - } - return fmt.Sprintf("[%d] %s", e, reason) +type Err struct { + // error code (unique) + Code int `json:"code"` + // error small description + Reason string `json:"reason"` + // associated HTTP status + Status int } -// Status returns the associated HTTP status code -func (e Error) Status() int { - status, ok := errorStatus[e] - if !ok { - return http.StatusOK - } - return status -} - -// MarshalJSON implements encoding/json.Marshaler interface -func (e Error) MarshalJSON() ([]byte, error) { - // use unknown error if no reason - reason, ok := errorReasons[e] - if !ok { - return ErrorUnknown.MarshalJSON() - } - - // format to proper struct - formatted := struct { - Code int `json:"code"` - Reason string `json:"reason"` - }{ - Code: int(e), - Reason: reason, - } - - return json.Marshal(formatted) +func (e Err) Error() string { + return fmt.Sprintf("[%d] %s", e.Code, e.Reason) } diff --git a/api/response.go b/api/response.go index a7ce08c..ae403f8 100644 --- a/api/response.go +++ b/api/response.go @@ -13,7 +13,7 @@ type Response struct { Data ResponseData Status int Headers http.Header - err Error + err Err } // EmptyResponse creates an empty response. @@ -21,13 +21,13 @@ func EmptyResponse() *Response { return &Response{ Status: http.StatusOK, Data: make(ResponseData), - err: ErrorFailure, + err: ErrFailure, Headers: make(http.Header), } } // WithError sets the error -func (res *Response) WithError(err Error) *Response { +func (res *Response) WithError(err Err) *Response { res.err = err return res } @@ -53,7 +53,7 @@ func (res *Response) MarshalJSON() ([]byte, error) { } func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error { - w.WriteHeader(res.err.Status()) + w.WriteHeader(res.err.Status) encoded, err := json.Marshal(res) if err != nil { return err diff --git a/internal/dynfunc/errors.go b/internal/dynfunc/errors.go index 0d2a4b2..ca87692 100644 --- a/internal/dynfunc/errors.go +++ b/internal/dynfunc/errors.go @@ -23,7 +23,7 @@ const errUnexpectedInput = cerr("unexpected input struct") const errMissingHandlerOutput = cerr("handler must have at least 1 output") // errMissingHandlerOutputError - missing error output for handler -const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Error") +const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Err") // errMissingRequestArgument - missing request argument for handler const errMissingRequestArgument = cerr("handler first argument must be of type api.Request") @@ -47,4 +47,4 @@ const errMissingOutputFromConfig = cerr("missing a parameter from configuration" const errWrongParamTypeFromConfig = cerr("invalid struct field type") // errMissingHandlerErrorOutput - missing handler output error -const errMissingHandlerErrorOutput = cerr("last output must be of type api.Error") +const errMissingHandlerErrorOutput = cerr("last output must be of type api.Err") diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index f932a89..c12069c 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -16,7 +16,7 @@ type Handler struct { // Build a handler from a service configuration and a dynamic function // -// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)` +// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Err)` // - `inputStruct` is a struct{} containing a field for each service input (with valid reflect.Type) // - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type) // @@ -46,8 +46,9 @@ func Build(fn interface{}, service config.Service) (*Handler, error) { } // Handle binds input @data into the dynamic function and returns map output -func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Error) { - fnv := reflect.ValueOf(h.fn) +func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Err) { + var ert = reflect.TypeOf(api.Err{}) + var fnv = reflect.ValueOf(h.fn) callArgs := []reflect.Value{} @@ -80,7 +81,12 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a // no output OR pointer to output struct is nil outdata := make(map[string]interface{}) if len(h.spec.Output) < 1 || output[0].IsNil() { - return outdata, api.Error(output[len(output)-1].Int()) + var structerr = output[len(output)-1].Convert(ert) + return outdata, api.Err{ + Code: int(structerr.FieldByName("Code").Int()), + Reason: structerr.FieldByName("Reason").String(), + Status: int(structerr.FieldByName("Status").Int()), + } } // extract struct from pointer @@ -91,6 +97,11 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a outdata[name] = field.Interface() } - // extract api.Error - return outdata, api.Error(output[len(output)-1].Int()) + // extract api.Err + var structerr = output[len(output)-1].Convert(ert) + return outdata, api.Err{ + Code: int(structerr.FieldByName("Code").Int()), + Reason: structerr.FieldByName("Reason").String(), + Status: int(structerr.FieldByName("Status").Int()), + } } diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/spec.go index 2449a85..571c9ae 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/spec.go @@ -91,9 +91,9 @@ func (s spec) checkOutput(fnv reflect.Value) error { return errMissingHandlerOutput } - // last output must be api.Error + // last output must be api.Err errOutput := fnt.Out(fnt.NumOut() - 1) - if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) { + if !errOutput.AssignableTo(reflect.TypeOf(api.ErrUnknown)) { return errMissingHandlerErrorOutput } diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go index a9ad5e9..b8599bc 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/spec_test.go @@ -111,28 +111,28 @@ func TestOutputCheck(t *testing.T) { Fn interface{} Err error }{ - // no input -> missing api.Error + // no input -> missing api.Err { Output: map[string]reflect.Type{}, Fn: func() {}, Err: errMissingHandlerOutput, }, - // no input -> with last type not api.Error + // no input -> with last type not api.Err { Output: map[string]reflect.Type{}, Fn: func() bool { return true }, Err: errMissingHandlerErrorOutput, }, - // no input -> with api.Error + // no input -> with api.Err { Output: map[string]reflect.Type{}, - Fn: func() api.Error { return api.ErrorSuccess }, + Fn: func() api.Err { return api.ErrSuccess }, Err: nil, }, // func can have output if not specified { Output: map[string]reflect.Type{}, - Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Err: nil, }, // missing output struct in func @@ -140,7 +140,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() api.Error { return api.ErrorSuccess }, + Fn: func() api.Err { return api.ErrSuccess }, Err: errMissingParamOutput, }, // output not a pointer @@ -148,7 +148,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() (int, api.Error) { return 0, api.ErrorSuccess }, + Fn: func() (int, api.Err) { return 0, api.ErrSuccess }, Err: errMissingParamOutput, }, // output not a pointer to struct @@ -156,7 +156,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*int, api.Err) { return nil, api.ErrSuccess }, Err: errMissingParamOutput, }, // unexported param name @@ -164,7 +164,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "test1": reflect.TypeOf(int(0)), }, - Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Err: errUnexportedName, }, // output field missing @@ -172,7 +172,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Err: errMissingParamFromConfig, }, // output field invalid type @@ -180,7 +180,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess }, Err: errWrongParamTypeFromConfig, }, // output field valid type @@ -188,7 +188,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, Err: nil, }, // ignore type check on nil type @@ -196,7 +196,7 @@ func TestOutputCheck(t *testing.T) { Output: map[string]reflect.Type{ "Test1": nil, }, - Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess }, + Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, Err: nil, }, } diff --git a/server.go b/server.go index dbc2074..2a81e41 100644 --- a/server.go +++ b/server.go @@ -18,14 +18,14 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { // 1. find a matching service in the config service := server.conf.Find(req) if service == nil { - errorHandler(api.ErrorUnknownService).ServeHTTP(res, req) + handleError(api.ErrUnknownService, w, r) return } // 2. extract request data dataset, err := extractRequestData(service, *req) if err != nil { - errorHandler(api.ErrorMissingParam).ServeHTTP(res, req) + handleError(api.ErrMissingParam, w, r) return } @@ -39,7 +39,7 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { // 4. fail if found no handler if handler == nil { - errorHandler(api.ErrorUncallableService).ServeHTTP(res, req) + handleError(api.ErrUncallableService, w, r) return } @@ -69,29 +69,27 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { response.ServeHTTP(res, req) } -func errorHandler(err api.Error) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - r := api.EmptyResponse().WithError(err) - r.ServeHTTP(res, req) - } +func handleError(err api.Err, w http.ResponseWriter, r *http.Request) { + var response = api.EmptyResponse().WithError(err) + response.ServeHTTP(w, r) } func extractRequestData(service *config.Service, req http.Request) (*reqdata.T, error) { - dataset := reqdata.New(service) + var dataset = reqdata.New(service) - // 3. extract URI data - err := dataset.GetURI(req) + // URI data + var err = dataset.GetURI(req) if err != nil { return nil, err } - // 4. extract query data + // query data err = dataset.GetQuery(req) if err != nil { return nil, err } - // 5. extract form/json data + // form/json data err = dataset.GetForm(req) if err != nil { return nil, err