From 66985dfbd09b8f5ac4146ad6f6b3bee237b5aa00 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:13:07 +0200 Subject: [PATCH 01/19] forbid unexported input/output name --- dynamic/errors.go | 3 +++ dynamic/spec.go | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dynamic/errors.go b/dynamic/errors.go index e3d7546..891f254 100644 --- a/dynamic/errors.go +++ b/dynamic/errors.go @@ -29,6 +29,9 @@ const ErrMissingRequestArgument = cerr("handler first argument must be of type a // ErrMissingParamArgument - missing parameters argument for handler const ErrMissingParamArgument = cerr("handler second argument must be a struct") +// ErrUnexportedParamName - argument is unexported in struct +const ErrUnexportedName = cerr("unexported name") + // ErrMissingParamOutput - missing output argument for handler const ErrMissingParamOutput = cerr("handler first output must be a *struct") diff --git a/dynamic/spec.go b/dynamic/spec.go index 354df52..83c6dd0 100644 --- a/dynamic/spec.go +++ b/dynamic/spec.go @@ -3,6 +3,7 @@ package dynamic import ( "fmt" "reflect" + "strings" "git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/internal/config" @@ -50,8 +51,15 @@ func (s spec) checkInput(fnv reflect.Value) error { return ErrMissingParamArgument } - // check for invlaid param + // check for invalid param for name, ptype := range s.Input { + if len(name) < 1 { + continue + } + if name[0] == strings.ToLower(name)[0] { + return fmt.Errorf("%s: %w", name, ErrUnexportedName) + } + field, exists := structArg.FieldByName(name) if !exists { return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig) @@ -100,6 +108,13 @@ func (s spec) checkOutput(fnv reflect.Value) error { // fail on invalid output for name, ptype := range s.Output { + if len(name) < 1 { + continue + } + if name[0] == strings.ToLower(name)[0] { + return fmt.Errorf("%s: %w", name, ErrUnexportedName) + } + field, exists := structOutput.FieldByName(name) if !exists { return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig) From 7e42c1b6d972f74471f5968fcac68d73de371d19 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:14:12 +0200 Subject: [PATCH 02/19] test: spec checkInput() method --- dynamic/spec_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 dynamic/spec_test.go diff --git a/dynamic/spec_test.go b/dynamic/spec_test.go new file mode 100644 index 0000000..95e76d3 --- /dev/null +++ b/dynamic/spec_test.go @@ -0,0 +1,103 @@ +package dynamic + +import ( + "errors" + "reflect" + "testing" +) + +func TestInputCheck(t *testing.T) { + tcases := []struct { + Input map[string]reflect.Type + Fn interface{} + Err error + }{ + // no input + { + Input: map[string]reflect.Type{}, + Fn: func() {}, + Err: nil, + }, + // func can have any arguments if not specified + { + Input: map[string]reflect.Type{}, + Fn: func(int, string) {}, + Err: nil, + }, + // missing input struct in func + { + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() {}, + Err: ErrMissingHandlerArgumentParam, + }, + // input not a struct + { + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func(int) {}, + Err: ErrMissingParamArgument, + }, + // unexported param name + { + Input: map[string]reflect.Type{ + "test1": reflect.TypeOf(int(0)), + }, + Fn: func(struct{}) {}, + Err: ErrUnexportedName, + }, + // input field missing + { + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func(struct{}) {}, + Err: ErrMissingParamFromConfig, + }, + // input field invalid type + { + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func(struct{ Test1 string }) {}, + Err: ErrWrongParamTypeFromConfig, + }, + // input field valid type + { + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func(struct{ Test1 int }) {}, + Err: nil, + }, + } + + for i, tcase := range tcases { + t.Run("case."+string(i), func(t *testing.T) { + // mock spec + s := spec{ + Input: tcase.Input, + Output: nil, + } + + err := s.checkInput(reflect.ValueOf(tcase.Fn)) + if err == nil && tcase.Err != nil { + t.Errorf("expected an error: '%s'", tcase.Err.Error()) + t.FailNow() + } + if err != nil && tcase.Err == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && tcase.Err != nil { + if !errors.Is(err, tcase.Err) { + t.Errorf("expected the error <%s> got <%s>", tcase.Err, err) + t.FailNow() + } + } + }) + } +} From 438e308f71e758d791fe5fbf1fe880141b889390 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:22:43 +0200 Subject: [PATCH 03/19] merge duplicate errors --- dynamic/errors.go | 5 +---- dynamic/spec.go | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dynamic/errors.go b/dynamic/errors.go index 891f254..f8260c3 100644 --- a/dynamic/errors.go +++ b/dynamic/errors.go @@ -29,7 +29,7 @@ const ErrMissingRequestArgument = cerr("handler first argument must be of type a // ErrMissingParamArgument - missing parameters argument for handler const ErrMissingParamArgument = cerr("handler second argument must be a struct") -// ErrUnexportedParamName - argument is unexported in struct +// ErrUnexportedName - argument is unexported in struct const ErrUnexportedName = cerr("unexported name") // ErrMissingParamOutput - missing output argument for handler @@ -44,8 +44,5 @@ const ErrMissingOutputFromConfig = cerr("missing a parameter from configuration" // ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct const ErrWrongParamTypeFromConfig = cerr("invalid struct field type") -// ErrWrongOutputTypeFromConfig - a configuration output type is invalid in the handler output struct -const ErrWrongOutputTypeFromConfig = cerr("invalid struct field type") - // ErrMissingHandlerErrorOutput - missing handler output error const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error") diff --git a/dynamic/spec.go b/dynamic/spec.go index 83c6dd0..d5fa04b 100644 --- a/dynamic/spec.go +++ b/dynamic/spec.go @@ -125,8 +125,7 @@ func (s spec) checkOutput(fnv reflect.Value) error { continue } - if !ptype.ConvertibleTo(field.Type) { - return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongOutputTypeFromConfig, field.Type, ptype) + return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype) } } From 261e25c127529dce83268f5659e5921c171e4a0b Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:23:02 +0200 Subject: [PATCH 04/19] fix: invert conversion check --- dynamic/spec.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dynamic/spec.go b/dynamic/spec.go index d5fa04b..35a5c5b 100644 --- a/dynamic/spec.go +++ b/dynamic/spec.go @@ -125,6 +125,7 @@ func (s spec) checkOutput(fnv reflect.Value) error { continue } + if !field.Type.ConvertibleTo(ptype) { return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype) } } From 307021bc8881e500b09150140eeb4ccf5691af32 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:23:13 +0200 Subject: [PATCH 05/19] test: spec checkOutput() method --- dynamic/spec_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/dynamic/spec_test.go b/dynamic/spec_test.go index 95e76d3..e0eb6a8 100644 --- a/dynamic/spec_test.go +++ b/dynamic/spec_test.go @@ -2,8 +2,11 @@ package dynamic import ( "errors" + "fmt" "reflect" "testing" + + "git.xdrm.io/go/aicra/api" ) func TestInputCheck(t *testing.T) { @@ -75,7 +78,7 @@ func TestInputCheck(t *testing.T) { } for i, tcase := range tcases { - t.Run("case."+string(i), func(t *testing.T) { + t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { // mock spec s := spec{ Input: tcase.Input, @@ -101,3 +104,113 @@ func TestInputCheck(t *testing.T) { }) } } + +func TestOutputCheck(t *testing.T) { + tcases := []struct { + Output map[string]reflect.Type + Fn interface{} + Err error + }{ + // no input -> missing api.Error + { + Output: map[string]reflect.Type{}, + Fn: func() {}, + Err: ErrMissingHandlerOutput, + }, + // no input -> with api.Error + { + Output: map[string]reflect.Type{}, + Fn: func() api.Error { return api.ErrorSuccess }, + Err: nil, + }, + // func can have output if not specified + { + Output: map[string]reflect.Type{}, + Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Err: nil, + }, + // missing output struct in func + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() api.Error { return api.ErrorSuccess }, + Err: ErrMissingParamOutput, + }, + // output not a pointer + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (int, api.Error) { return 0, api.ErrorSuccess }, + Err: ErrMissingParamOutput, + }, + // output not a pointer to struct + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess }, + Err: ErrMissingParamOutput, + }, + // unexported param name + { + Output: map[string]reflect.Type{ + "test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Err: ErrUnexportedName, + }, + // output field missing + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, + Err: ErrMissingParamFromConfig, + }, + // output field invalid type + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess }, + Err: ErrWrongParamTypeFromConfig, + }, + // output field valid type + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess }, + Err: nil, + }, + } + + for i, tcase := range tcases { + t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { + // mock spec + s := spec{ + Input: nil, + Output: tcase.Output, + } + + err := s.checkOutput(reflect.ValueOf(tcase.Fn)) + if err == nil && tcase.Err != nil { + t.Errorf("expected an error: '%s'", tcase.Err.Error()) + t.FailNow() + } + if err != nil && tcase.Err == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && tcase.Err != nil { + if !errors.Is(err, tcase.Err) { + t.Errorf("expected the error <%s> got <%s>", tcase.Err, err) + t.FailNow() + } + } + }) + } +} From b48c1d07bfc68716762622ac011c42ae96601204 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:31:08 +0200 Subject: [PATCH 06/19] test: spec add checkOutput() tests for : nil type (ignore type check) ; invalid last output (not api.Error) --- dynamic/spec_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dynamic/spec_test.go b/dynamic/spec_test.go index e0eb6a8..535d9e3 100644 --- a/dynamic/spec_test.go +++ b/dynamic/spec_test.go @@ -117,6 +117,12 @@ func TestOutputCheck(t *testing.T) { Fn: func() {}, Err: ErrMissingHandlerOutput, }, + // no input -> with last type not api.Error + { + Output: map[string]reflect.Type{}, + Fn: func() bool { return true }, + Err: ErrMissingHandlerErrorOutput, + }, // no input -> with api.Error { Output: map[string]reflect.Type{}, @@ -185,6 +191,14 @@ func TestOutputCheck(t *testing.T) { Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess }, Err: nil, }, + // ignore type check on nil type + { + Output: map[string]reflect.Type{ + "Test1": nil, + }, + Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess }, + Err: nil, + }, } for i, tcase := range tcases { From db4429b32901c955c043b8d9e52706b15fe3b6a1 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 19:33:26 +0200 Subject: [PATCH 07/19] ignore empty param renames when creating the spec, not after --- dynamic/spec.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dynamic/spec.go b/dynamic/spec.go index 35a5c5b..9e0871e 100644 --- a/dynamic/spec.go +++ b/dynamic/spec.go @@ -17,6 +17,9 @@ func makeSpec(service config.Service) spec { } for _, param := range service.Input { + if len(param.Rename) < 1 { + continue + } // make a pointer if optional if param.Optional { spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType) @@ -26,6 +29,9 @@ func makeSpec(service config.Service) spec { } for _, param := range service.Output { + if len(param.Rename) < 1 { + continue + } spec.Output[param.Rename] = param.ExtractType } @@ -53,9 +59,6 @@ func (s spec) checkInput(fnv reflect.Value) error { // check for invalid param for name, ptype := range s.Input { - if len(name) < 1 { - continue - } if name[0] == strings.ToLower(name)[0] { return fmt.Errorf("%s: %w", name, ErrUnexportedName) } @@ -108,9 +111,6 @@ func (s spec) checkOutput(fnv reflect.Value) error { // fail on invalid output for name, ptype := range s.Output { - if len(name) < 1 { - continue - } if name[0] == strings.ToLower(name)[0] { return fmt.Errorf("%s: %w", name, ErrUnexportedName) } From 8fa18cd61b6fc6192662f20890c2dd5bfd998eb0 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 10:02:48 +0200 Subject: [PATCH 08/19] enforce dynamic signature check: no input struct allowed when no input is specified --- dynamic/errors.go | 3 +++ dynamic/spec.go | 3 +++ dynamic/spec_test.go | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dynamic/errors.go b/dynamic/errors.go index f8260c3..9467bcf 100644 --- a/dynamic/errors.go +++ b/dynamic/errors.go @@ -17,6 +17,9 @@ const ErrNoServiceForHandler = cerr("no service found for this handler") // ErrMissingHandlerArgumentParam - missing params arguments for handler const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct") +// ErrUnexpectedInput - input argument is not expected +const ErrUnexpectedInput = cerr("unexpected input struct") + // ErrMissingHandlerOutput - missing output for handler const ErrMissingHandlerOutput = cerr("handler must have at least 1 output") diff --git a/dynamic/spec.go b/dynamic/spec.go index 9e0871e..ee6fd06 100644 --- a/dynamic/spec.go +++ b/dynamic/spec.go @@ -44,6 +44,9 @@ func (s spec) checkInput(fnv reflect.Value) error { // no input -> ok if len(s.Input) == 0 { + if fnt.NumIn() > 0 { + return ErrUnexpectedInput + } return nil } diff --git a/dynamic/spec_test.go b/dynamic/spec_test.go index 535d9e3..0ce92bc 100644 --- a/dynamic/spec_test.go +++ b/dynamic/spec_test.go @@ -21,11 +21,11 @@ func TestInputCheck(t *testing.T) { Fn: func() {}, Err: nil, }, - // func can have any arguments if not specified + // func must have noarguments if none specified { Input: map[string]reflect.Type{}, Fn: func(int, string) {}, - Err: nil, + Err: ErrUnexpectedInput, }, // missing input struct in func { From e1606273ddc2d48b54944756d070dd7fc9ca6e20 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 10:05:27 +0200 Subject: [PATCH 09/19] remove useless func type --- dynamic/handler.go | 12 ++++++------ dynamic/types.go | 5 +---- handler.go | 2 +- server.go | 3 +-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/dynamic/handler.go b/dynamic/handler.go index 54bded1..29b654a 100644 --- a/dynamic/handler.go +++ b/dynamic/handler.go @@ -8,16 +8,16 @@ import ( "git.xdrm.io/go/aicra/internal/config" ) -// Build a handler from a service configuration and a HandlerFn +// Build a handler from a service configuration and a dynamic function // -// a HandlerFn must have as a signature : `func(api.Request, inputStruct) (outputStruct, api.Error)` +// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)` // - `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) // // Special cases: -// - it there is no input, `inputStruct` can be omitted -// - it there is no output, `outputStruct` can be omitted -func Build(fn HandlerFn, service config.Service) (*Handler, error) { +// - it there is no input, `inputStruct` must be omitted +// - it there is no output, `outputStruct` must be omitted +func Build(fn interface{}, service config.Service) (*Handler, error) { h := &Handler{ spec: makeSpec(service), fn: fn, @@ -39,7 +39,7 @@ func Build(fn HandlerFn, service config.Service) (*Handler, error) { return h, nil } -// Handle binds input @data into HandleFn and returns map output +// 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) diff --git a/dynamic/types.go b/dynamic/types.go index a180e63..041ddb9 100644 --- a/dynamic/types.go +++ b/dynamic/types.go @@ -2,13 +2,10 @@ package dynamic import "reflect" -// HandlerFn defines a dynamic handler function -type HandlerFn interface{} - // Handler represents a dynamic api handler type Handler struct { spec spec - fn HandlerFn + fn interface{} } type spec struct { diff --git a/handler.go b/handler.go index 8af4f34..16af6ca 100644 --- a/handler.go +++ b/handler.go @@ -16,7 +16,7 @@ type handler struct { // createHandler builds a handler from its http method and path // also it checks whether the function signature is valid -func createHandler(method, path string, service config.Service, fn dynamic.HandlerFn) (*handler, error) { +func createHandler(method, path string, service config.Service, fn interface{}) (*handler, error) { method = strings.ToUpper(method) dynHandler, err := dynamic.Build(fn, service) diff --git a/server.go b/server.go index 54e53b4..7bfa323 100644 --- a/server.go +++ b/server.go @@ -6,7 +6,6 @@ import ( "os" "git.xdrm.io/go/aicra/datatype" - "git.xdrm.io/go/aicra/dynamic" "git.xdrm.io/go/aicra/internal/config" ) @@ -47,7 +46,7 @@ func New(configPath string, dtypes ...datatype.T) (*Server, error) { } // Handle sets a new handler for an HTTP method to a path -func (s *Server) Handle(method, path string, fn dynamic.HandlerFn) error { +func (s *Server) Handle(method, path string, fn interface{}) error { // find associated service var found *config.Service = nil for _, service := range s.config.Services { From eb690cf862a6b9dab3f8a2ae679248014bf07a57 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 10:08:11 +0200 Subject: [PATCH 10/19] add api errors for storage --- api/error.defaults.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/error.defaults.go b/api/error.defaults.go index 75ea528..4907ba5 100644 --- a/api/error.defaults.go +++ b/api/error.defaults.go @@ -22,6 +22,18 @@ var ( // ErrorConfig has to be set when there is a configuration error ErrorConfig Error = 4 + // ErrorCreation has to be set when there is a creation/insert error + ErrorCreation Error = 5 + + // ErrorModification has to be set when there is an update/modification error + ErrorModification Error = 6 + + // ErrorDeletion has to be set when there is a deletion/removal error + ErrorDeletion Error = 7 + + // ErrorTransaction has to be set when there is a transactional error + ErrorTransaction Error = 8 + // ErrorUpload has to be set when a file upload failed ErrorUpload Error = 100 @@ -79,6 +91,10 @@ var errorReasons = map[Error]string{ ErrorNoMatchFound: "resource not found", ErrorAlreadyExists: "already exists", ErrorConfig: "configuration error", + ErrorCreation: "create error", + ErrorModification: "update error", + ErrorDeletion: "delete error", + ErrorTransaction: "transactional error", ErrorUpload: "upload failed", ErrorDownload: "download failed", MissingDownloadHeaders: "download headers are missing", From b1498e59c19401f21380a67a2363d09850b6ab1e Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 10:36:52 +0200 Subject: [PATCH 11/19] clarity rename: dynamic package to dynfunc --- {dynamic => dynfunc}/errors.go | 2 +- {dynamic => dynfunc}/handler.go | 2 +- {dynamic => dynfunc}/spec.go | 2 +- {dynamic => dynfunc}/spec_test.go | 2 +- {dynamic => dynfunc}/types.go | 2 +- handler.go | 32 ----------------------------- server.go | 34 ++++++++++++++++++++++--------- 7 files changed, 29 insertions(+), 47 deletions(-) rename {dynamic => dynfunc}/errors.go (99%) rename {dynamic => dynfunc}/handler.go (99%) rename {dynamic => dynfunc}/spec.go (99%) rename {dynamic => dynfunc}/spec_test.go (99%) rename {dynamic => dynfunc}/types.go (92%) delete mode 100644 handler.go diff --git a/dynamic/errors.go b/dynfunc/errors.go similarity index 99% rename from dynamic/errors.go rename to dynfunc/errors.go index 9467bcf..c038171 100644 --- a/dynamic/errors.go +++ b/dynfunc/errors.go @@ -1,4 +1,4 @@ -package dynamic +package dynfunc // cerr allows you to create constant "const" error with type boxing. type cerr string diff --git a/dynamic/handler.go b/dynfunc/handler.go similarity index 99% rename from dynamic/handler.go rename to dynfunc/handler.go index 29b654a..ad00224 100644 --- a/dynamic/handler.go +++ b/dynfunc/handler.go @@ -1,4 +1,4 @@ -package dynamic +package dynfunc import ( "fmt" diff --git a/dynamic/spec.go b/dynfunc/spec.go similarity index 99% rename from dynamic/spec.go rename to dynfunc/spec.go index ee6fd06..7b3c4f6 100644 --- a/dynamic/spec.go +++ b/dynfunc/spec.go @@ -1,4 +1,4 @@ -package dynamic +package dynfunc import ( "fmt" diff --git a/dynamic/spec_test.go b/dynfunc/spec_test.go similarity index 99% rename from dynamic/spec_test.go rename to dynfunc/spec_test.go index 0ce92bc..79ccfe3 100644 --- a/dynamic/spec_test.go +++ b/dynfunc/spec_test.go @@ -1,4 +1,4 @@ -package dynamic +package dynfunc import ( "errors" diff --git a/dynamic/types.go b/dynfunc/types.go similarity index 92% rename from dynamic/types.go rename to dynfunc/types.go index 041ddb9..3b0aac8 100644 --- a/dynamic/types.go +++ b/dynfunc/types.go @@ -1,4 +1,4 @@ -package dynamic +package dynfunc import "reflect" diff --git a/handler.go b/handler.go deleted file mode 100644 index 16af6ca..0000000 --- a/handler.go +++ /dev/null @@ -1,32 +0,0 @@ -package aicra - -import ( - "fmt" - "strings" - - "git.xdrm.io/go/aicra/dynamic" - "git.xdrm.io/go/aicra/internal/config" -) - -type handler struct { - Method string - Path string - dynHandler *dynamic.Handler -} - -// createHandler builds a handler from its http method and path -// also it checks whether the function signature is valid -func createHandler(method, path string, service config.Service, fn interface{}) (*handler, error) { - method = strings.ToUpper(method) - - dynHandler, err := dynamic.Build(fn, service) - if err != nil { - return nil, fmt.Errorf("%s '%s' handler: %w", method, path, err) - } - - return &handler{ - Path: path, - Method: method, - dynHandler: dynHandler, - }, nil -} diff --git a/server.go b/server.go index 7bfa323..9dcae18 100644 --- a/server.go +++ b/server.go @@ -6,13 +6,20 @@ import ( "os" "git.xdrm.io/go/aicra/datatype" + "git.xdrm.io/go/aicra/dynfunc" "git.xdrm.io/go/aicra/internal/config" ) // Server represents an AICRA instance featuring: type checkers, services type Server struct { config *config.Server - handlers []*handler + handlers []*apiHandler +} + +type apiHandler struct { + Method string + Path string + dynHandler *dynfunc.Handler } // New creates a framework instance from a configuration file @@ -48,22 +55,29 @@ func New(configPath string, dtypes ...datatype.T) (*Server, error) { // Handle sets a new handler for an HTTP method to a path func (s *Server) Handle(method, path string, fn interface{}) error { // find associated service - var found *config.Service = nil - for _, service := range s.config.Services { - if method == service.Method && path == service.Pattern { - found = service + var service *config.Service + for _, s := range s.config.Services { + if method == s.Method && path == s.Pattern { + service = s break } } - if found == nil { - return fmt.Errorf("%s '%s': %w", method, path, ErrNoServiceForHandler) + + if service == nil { + return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService) } - handler, err := createHandler(method, path, *found, fn) + dynHandler, err := dynfunc.Build(fn, *service) if err != nil { - return err + return fmt.Errorf("%s '%s' handler: %w", method, path, err) } - s.handlers = append(s.handlers, handler) + + s.handlers = append(s.handlers, &apiHandler{ + Path: path, + Method: method, + dynHandler: dynHandler, + }) + return nil } From b0e25b431c14bde15def761896b80c148b947c63 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 10:39:02 +0200 Subject: [PATCH 12/19] ToHTTPServer now returns the exported field http.Handler instead of an unexported type --- http.go | 6 +++--- server.go | 20 ++++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/http.go b/http.go index d078ea1..ad8ebe9 100644 --- a/http.go +++ b/http.go @@ -8,11 +8,11 @@ import ( "git.xdrm.io/go/aicra/internal/reqdata" ) -// httpServer wraps the aicra server to allow handling http requests -type httpServer Server +// httpHandler wraps the aicra server to allow handling http requests +type httpHandler Server // ServeHTTP implements http.Handler and has to be called on each request -func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) { +func (server httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { defer req.Body.Close() // 1. find a matching service in the config diff --git a/server.go b/server.go index 9dcae18..59a6285 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,7 @@ package aicra import ( "fmt" - "io" + "net/http" "os" "git.xdrm.io/go/aicra/datatype" @@ -81,24 +81,20 @@ func (s *Server) Handle(method, path string, fn interface{}) error { return nil } -// ToHTTPServer converts the server to a http server -func (s Server) ToHTTPServer() (*httpServer, error) { - - // check if handlers are missing +// ToHTTPServer converts the server to a http.Handler +func (s Server) ToHTTPServer() (http.Handler, error) { for _, service := range s.config.Services { - found := false + var hasAssociatedHandler bool for _, handler := range s.handlers { if handler.Method == service.Method && handler.Path == service.Pattern { - found = true + hasAssociatedHandler = true break } } - if !found { - return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrNoHandlerForService) + if !hasAssociatedHandler { + return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler) } } - // 2. cast to http server - httpServer := httpServer(s) - return &httpServer, nil + return httpHandler(s), nil } From 1e0fb77d61c57995fec9a0e0d268d6f29509774a Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 11:45:49 +0200 Subject: [PATCH 13/19] standardize and simplify the config package --- internal/config/config_test.go | 47 +++++++++++++++++-------- internal/config/parameter.go | 19 ++++++++-- internal/config/server.go | 46 ++++++++++++------------- internal/config/service.go | 38 ++++++++++++++++---- internal/config/types.go | 63 ---------------------------------- 5 files changed, 103 insertions(+), 110 deletions(-) delete mode 100644 internal/config/types.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0bc3e2a..6ff7dcd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -80,7 +80,8 @@ func TestLegalServiceName(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -134,7 +135,8 @@ func TestAvailableMethods(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if test.ValidMethod && err != nil { t.Errorf("unexpected error: '%s'", err.Error()) @@ -150,20 +152,22 @@ func TestAvailableMethods(t *testing.T) { } func TestParseEmpty(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[]`) - _, err := Parse(reader) + r := strings.NewReader(`[]`) + srv := &Server{} + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error (got '%s')", err) t.FailNow() } } func TestParseJsonError(t *testing.T) { - reader := strings.NewReader(`{ + r := strings.NewReader(`{ "GET": { "info": "info }, }`) // trailing ',' is invalid JSON - _, err := Parse(reader) + srv := &Server{} + err := srv.Parse(r) if err == nil { t.Errorf("expected error") t.FailNow() @@ -205,7 +209,8 @@ func TestParseMissingMethodDescription(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if test.ValidDescription && err != nil { t.Errorf("unexpected error: '%s'", err) @@ -223,7 +228,7 @@ func TestParseMissingMethodDescription(t *testing.T) { func TestParamEmptyRenameNoRename(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[ + r := strings.NewReader(`[ { "method": "GET", "path": "/", @@ -233,7 +238,9 @@ func TestParamEmptyRenameNoRename(t *testing.T) { } } ]`) - srv, err := Parse(reader, builtin.AnyDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error: '%s'", err) t.FailNow() @@ -254,7 +261,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) { } func TestOptionalParam(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[ + r := strings.NewReader(`[ { "method": "GET", "path": "/", @@ -267,7 +274,10 @@ func TestOptionalParam(t *testing.T) { } } ]`) - srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + srv.Types = append(srv.Types, builtin.BoolDataType{}) + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error: '%s'", err) t.FailNow() @@ -577,7 +587,9 @@ func TestParseParameters(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + err := srv.Parse(strings.NewReader(test.Raw)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -814,7 +826,10 @@ func TestServiceCollision(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Config), builtin.StringDataType{}, builtin.UintDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.StringDataType{}) + srv.Types = append(srv.Types, builtin.UintDataType{}) + err := srv.Parse(strings.NewReader(test.Config)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -951,7 +966,11 @@ func TestMatchSimple(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + srv.Types = append(srv.Types, builtin.IntDataType{}) + srv.Types = append(srv.Types, builtin.BoolDataType{}) + err := srv.Parse(strings.NewReader(test.Config)) if err != nil { t.Errorf("unexpected error: '%s'", err) diff --git a/internal/config/parameter.go b/internal/config/parameter.go index 1f8be92..ca36a1c 100644 --- a/internal/config/parameter.go +++ b/internal/config/parameter.go @@ -1,11 +1,26 @@ package config import ( + "reflect" + "git.xdrm.io/go/aicra/datatype" ) -// Validate implements the validator interface -func (param *Parameter) Validate(datatypes ...datatype.T) error { +// Parameter represents a parameter definition (from api.json) +type Parameter struct { + Description string `json:"info"` + Type string `json:"type"` + Rename string `json:"name,omitempty"` + // ExtractType is the type of data the datatype returns + ExtractType reflect.Type + // Optional is set to true when the type is prefixed with '?' + Optional bool + + // Validator is inferred from @Type + Validator datatype.Validator +} + +func (param *Parameter) validate(datatypes ...datatype.T) error { // missing description if len(param.Description) < 1 { return ErrMissingParamDesc diff --git a/internal/config/server.go b/internal/config/server.go index d2c7e3a..192d98d 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -9,34 +9,30 @@ import ( "git.xdrm.io/go/aicra/datatype" ) -// Parse builds a server configuration from a json reader and checks for most format errors. -// you can provide additional DataTypes as variadic arguments -func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) { - server := &Server{ - Types: make([]datatype.T, 0), - Services: make([]*Service, 0), - } - - // add data types - for _, dtype := range dtypes { - server.Types = append(server.Types, dtype) - } - - if err := json.NewDecoder(r).Decode(&server.Services); err != nil { - return nil, fmt.Errorf("%s: %w", ErrRead, err) - } - - if err := server.Validate(); err != nil { - return nil, fmt.Errorf("%s: %w", ErrFormat, err) - } - - return server, nil +// Server definition +type Server struct { + Types []datatype.T + Services []*Service } -// Validate implements the validator interface -func (server Server) Validate(datatypes ...datatype.T) error { +// Parse a reader into a server. Server.Types must be set beforehand to +// make datatypes available when checking and formatting the read configuration. +func (srv *Server) Parse(r io.Reader) error { + if err := json.NewDecoder(r).Decode(&srv.Services); err != nil { + return fmt.Errorf("%s: %w", ErrRead, err) + } + + if err := srv.validate(); err != nil { + return fmt.Errorf("%s: %w", ErrFormat, err) + } + + return nil +} + +// validate implements the validator interface +func (server Server) validate(datatypes ...datatype.T) error { for _, service := range server.Services { - err := service.Validate(server.Types...) + err := service.validate(server.Types...) if err != nil { return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err) } diff --git a/internal/config/service.go b/internal/config/service.go index 87815d7..0f79764 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -11,6 +11,35 @@ import ( var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`) var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`) +var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} + +// Service definition +type Service struct { + Method string `json:"method"` + Pattern string `json:"path"` + Scope [][]string `json:"scope"` + Description string `json:"info"` + Input map[string]*Parameter `json:"in"` + Output map[string]*Parameter `json:"out"` + + // references to url parameters + // format: '/uri/{param}' + Captures []*BraceCapture + + // references to Query parameters + // format: 'GET@paranName' + Query map[string]*Parameter + + // references for form parameters (all but Captures and Query) + Form map[string]*Parameter +} + +// BraceCapture links to the related URI parameter +type BraceCapture struct { + Name string + Index int + Ref *Parameter +} // Match returns if this service would handle this HTTP request func (svc *Service) Match(req *http.Request) bool { @@ -24,9 +53,6 @@ func (svc *Service) Match(req *http.Request) bool { return false } - // check and extract input - // todo: check if input match and extract models - return true } @@ -76,7 +102,7 @@ func (svc *Service) matchPattern(uri string) bool { } // Validate implements the validator interface -func (svc *Service) Validate(datatypes ...datatype.T) error { +func (svc *Service) validate(datatypes ...datatype.T) error { // check method err := svc.isMethodAvailable() if err != nil { @@ -233,7 +259,7 @@ func (svc *Service) validateInput(types []datatype.T) error { param.Rename = paramName } - err := param.Validate(types...) + err := param.validate(types...) if err != nil { return fmt.Errorf("%s: %w", paramName, err) } @@ -283,7 +309,7 @@ func (svc *Service) validateOutput(types []datatype.T) error { param.Rename = paramName } - err := param.Validate(types...) + err := param.validate(types...) if err != nil { return fmt.Errorf("%s: %w", paramName, err) } diff --git a/internal/config/types.go b/internal/config/types.go deleted file mode 100644 index f3532a4..0000000 --- a/internal/config/types.go +++ /dev/null @@ -1,63 +0,0 @@ -package config - -import ( - "net/http" - "reflect" - - "git.xdrm.io/go/aicra/datatype" -) - -var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} - -// validator unifies the check and format routine -type validator interface { - Validate(...datatype.T) error -} - -// Server represents a full server configuration -type Server struct { - Types []datatype.T - Services []*Service -} - -// Service represents a service definition (from api.json) -type Service struct { - Method string `json:"method"` - Pattern string `json:"path"` - Scope [][]string `json:"scope"` - Description string `json:"info"` - Input map[string]*Parameter `json:"in"` - Output map[string]*Parameter `json:"out"` - - // references to url parameters - // format: '/uri/{param}' - Captures []*BraceCapture - - // references to Query parameters - // format: 'GET@paranName' - Query map[string]*Parameter - - // references for form parameters (all but Captures and Query) - Form map[string]*Parameter -} - -// Parameter represents a parameter definition (from api.json) -type Parameter struct { - Description string `json:"info"` - Type string `json:"type"` - Rename string `json:"name,omitempty"` - // ExtractType is the type of data the datatype returns - ExtractType reflect.Type - // Optional is set to true when the type is prefixed with '?' - Optional bool - - // Validator is inferred from @Type - Validator datatype.Validator -} - -// BraceCapture links to the related URI parameter -type BraceCapture struct { - Name string - Index int - Ref *Parameter -} From d69dd2508cdebcce7de2ebf261710de609466d2c Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 11:46:37 +0200 Subject: [PATCH 14/19] refactor aicra: meaningful defaults, stage renaming Builder.Build() -> Server --- errors.go | 17 ++++++++--- http.go | 26 +++++----------- server.go | 91 +++++++++++++++++++++++++++---------------------------- 3 files changed, 66 insertions(+), 68 deletions(-) diff --git a/errors.go b/errors.go index 252f8ee..f3c7145 100644 --- a/errors.go +++ b/errors.go @@ -8,8 +8,17 @@ func (err cerr) Error() string { return string(err) } -// ErrNoServiceForHandler - no service matching this handler -const ErrNoServiceForHandler = cerr("no service found for this handler") +// ErrLateType - cannot add datatype after setting up the definition +const ErrLateType = cerr("types cannot be added after Setup") -// ErrNoHandlerForService - no handler matching this service -const ErrNoHandlerForService = cerr("no handler found for this service") +// ErrNotSetup - not set up yet +const ErrNotSetup = cerr("not set up") + +// ErrAlreadySetup - already set up +const ErrAlreadySetup = cerr("already set up") + +// ErrUnknownService - no service matching this handler +const ErrUnknownService = cerr("unknown service") + +// ErrMissingHandler - missing handler +const ErrMissingHandler = cerr("missing handler") diff --git a/http.go b/http.go index ad8ebe9..0f95ec7 100644 --- a/http.go +++ b/http.go @@ -9,14 +9,14 @@ import ( ) // httpHandler wraps the aicra server to allow handling http requests -type httpHandler Server +type httpHandler Builder // ServeHTTP implements http.Handler and has to be called on each request func (server httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { defer req.Body.Close() // 1. find a matching service in the config - service := server.config.Find(req) + service := server.conf.Find(req) if service == nil { response := api.EmptyResponse().WithError(api.ErrorUnknownService) response.ServeHTTP(res, req) @@ -55,25 +55,15 @@ func (server httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) } // 6. find a matching handler - var foundHandler *handler - var found bool - - for _, handler := range server.handlers { - if handler.Method == service.Method && handler.Path == service.Pattern { - foundHandler = handler - found = true + var handler *apiHandler + for _, h := range server.handlers { + if h.Method == service.Method && h.Path == service.Pattern { + handler = h } } // 7. fail if found no handler - if foundHandler == nil { - if found { - r := api.EmptyResponse().WithError(api.ErrorUncallableService) - r.ServeHTTP(res, req) - logError(r) - return - } - + if handler == nil { r := api.EmptyResponse().WithError(api.ErrorUnknownService) r.ServeHTTP(res, req) logError(r) @@ -91,7 +81,7 @@ func (server httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) apireq.Param = dataset.Data // 10. execute - returned, apiErr := foundHandler.dynHandler.Handle(dataset.Data) + returned, apiErr := handler.dyn.Handle(dataset.Data) response := api.EmptyResponse().WithError(apiErr) for key, value := range returned { diff --git a/server.go b/server.go index 59a6285..75df07a 100644 --- a/server.go +++ b/server.go @@ -2,61 +2,59 @@ package aicra import ( "fmt" + "io" "net/http" - "os" "git.xdrm.io/go/aicra/datatype" "git.xdrm.io/go/aicra/dynfunc" "git.xdrm.io/go/aicra/internal/config" ) -// Server represents an AICRA instance featuring: type checkers, services -type Server struct { - config *config.Server +// Builder for an aicra server +type Builder struct { + conf *config.Server handlers []*apiHandler } +// represents an server handler type apiHandler struct { - Method string - Path string - dynHandler *dynfunc.Handler + Method string + Path string + dyn *dynfunc.Handler } -// New creates a framework instance from a configuration file -func New(configPath string, dtypes ...datatype.T) (*Server, error) { - var ( - err error - configFile io.ReadCloser - ) - - // 1. init instance - var i = &Server{ - config: nil, - handlers: make([]*handler, 0), +// AddType adds an available datatype to the api definition +func (b *Builder) AddType(t datatype.T) { + if b.conf == nil { + b.conf = &config.Server{} } - - // 2. open config file - configFile, err = os.Open(configPath) - if err != nil { - return nil, err + if b.conf.Services != nil { + panic(ErrLateType) } - defer configFile.Close() - - // 3. load configuration - i.config, err = config.Parse(configFile, dtypes...) - if err != nil { - return nil, err - } - - return i, nil - + b.conf.Types = append(b.conf.Types, t) } -// Handle sets a new handler for an HTTP method to a path -func (s *Server) Handle(method, path string, fn interface{}) error { +// Setup the builder with its api definition +// panics if already setup +func (b *Builder) Setup(r io.Reader) error { + if b.conf == nil { + b.conf = &config.Server{} + } + if b.conf.Services != nil { + panic(ErrAlreadySetup) + } + return b.conf.Parse(r) +} + +// Bind a dynamic handler to a REST service +func (b *Builder) Bind(method, path string, fn interface{}) error { + if b.conf.Services == nil { + return ErrNotSetup + } + // find associated service var service *config.Service - for _, s := range s.config.Services { + for _, s := range b.conf.Services { if method == s.Method && path == s.Pattern { service = s break @@ -67,25 +65,26 @@ func (s *Server) Handle(method, path string, fn interface{}) error { return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService) } - dynHandler, err := dynfunc.Build(fn, *service) + dyn, err := dynfunc.Build(fn, *service) if err != nil { return fmt.Errorf("%s '%s' handler: %w", method, path, err) } - s.handlers = append(s.handlers, &apiHandler{ - Path: path, - Method: method, - dynHandler: dynHandler, + b.handlers = append(b.handlers, &apiHandler{ + Path: path, + Method: method, + dyn: dyn, }) return nil } -// ToHTTPServer converts the server to a http.Handler -func (s Server) ToHTTPServer() (http.Handler, error) { - for _, service := range s.config.Services { +// Build a fully-featured HTTP server +func (b Builder) Build() (http.Handler, error) { + + for _, service := range b.conf.Services { var hasAssociatedHandler bool - for _, handler := range s.handlers { + for _, handler := range b.handlers { if handler.Method == service.Method && handler.Path == service.Pattern { hasAssociatedHandler = true break @@ -96,5 +95,5 @@ func (s Server) ToHTTPServer() (http.Handler, error) { } } - return httpHandler(s), nil + return httpHandler(b), nil } From 09362aad83232e0d11b7dddf30b6727c7e16fecd Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 11:49:33 +0200 Subject: [PATCH 15/19] make 'dynfunc' internal --- {dynfunc => internal/dynfunc}/errors.go | 0 {dynfunc => internal/dynfunc}/handler.go | 0 {dynfunc => internal/dynfunc}/spec.go | 0 {dynfunc => internal/dynfunc}/spec_test.go | 0 {dynfunc => internal/dynfunc}/types.go | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {dynfunc => internal/dynfunc}/errors.go (100%) rename {dynfunc => internal/dynfunc}/handler.go (100%) rename {dynfunc => internal/dynfunc}/spec.go (100%) rename {dynfunc => internal/dynfunc}/spec_test.go (100%) rename {dynfunc => internal/dynfunc}/types.go (100%) diff --git a/dynfunc/errors.go b/internal/dynfunc/errors.go similarity index 100% rename from dynfunc/errors.go rename to internal/dynfunc/errors.go diff --git a/dynfunc/handler.go b/internal/dynfunc/handler.go similarity index 100% rename from dynfunc/handler.go rename to internal/dynfunc/handler.go diff --git a/dynfunc/spec.go b/internal/dynfunc/spec.go similarity index 100% rename from dynfunc/spec.go rename to internal/dynfunc/spec.go diff --git a/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go similarity index 100% rename from dynfunc/spec_test.go rename to internal/dynfunc/spec_test.go diff --git a/dynfunc/types.go b/internal/dynfunc/types.go similarity index 100% rename from dynfunc/types.go rename to internal/dynfunc/types.go From c5cdba8007a887dfa9bbf5d5967f86a80659a4ba Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 11:50:01 +0200 Subject: [PATCH 16/19] move aicra builder and server into their own files --- builder.go | 99 +++++++++++++++++++++++++++++++++++ http.go | 106 ------------------------------------- server.go | 151 ++++++++++++++++++++++++++++------------------------- 3 files changed, 178 insertions(+), 178 deletions(-) create mode 100644 builder.go delete mode 100644 http.go diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..2d14067 --- /dev/null +++ b/builder.go @@ -0,0 +1,99 @@ +package aicra + +import ( + "fmt" + "io" + "net/http" + + "git.xdrm.io/go/aicra/datatype" + "git.xdrm.io/go/aicra/internal/config" + "git.xdrm.io/go/aicra/internal/dynfunc" +) + +// Builder for an aicra server +type Builder struct { + conf *config.Server + handlers []*apiHandler +} + +// represents an server handler +type apiHandler struct { + Method string + Path string + dyn *dynfunc.Handler +} + +// AddType adds an available datatype to the api definition +func (b *Builder) AddType(t datatype.T) { + if b.conf == nil { + b.conf = &config.Server{} + } + if b.conf.Services != nil { + panic(ErrLateType) + } + b.conf.Types = append(b.conf.Types, t) +} + +// Setup the builder with its api definition +// panics if already setup +func (b *Builder) Setup(r io.Reader) error { + if b.conf == nil { + b.conf = &config.Server{} + } + if b.conf.Services != nil { + panic(ErrAlreadySetup) + } + return b.conf.Parse(r) +} + +// Bind a dynamic handler to a REST service +func (b *Builder) Bind(method, path string, fn interface{}) error { + if b.conf.Services == nil { + return ErrNotSetup + } + + // find associated service + var service *config.Service + for _, s := range b.conf.Services { + if method == s.Method && path == s.Pattern { + service = s + break + } + } + + if service == nil { + return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService) + } + + dyn, err := dynfunc.Build(fn, *service) + if err != nil { + return fmt.Errorf("%s '%s' handler: %w", method, path, err) + } + + b.handlers = append(b.handlers, &apiHandler{ + Path: path, + Method: method, + dyn: dyn, + }) + + return nil +} + +// Build a fully-featured HTTP server +func (b Builder) Build() (http.Handler, error) { + + for _, service := range b.conf.Services { + var hasAssociatedHandler bool + for _, handler := range b.handlers { + if handler.Method == service.Method && handler.Path == service.Pattern { + hasAssociatedHandler = true + break + } + } + if !hasAssociatedHandler { + return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler) + } + } + + return Server(b), nil +} diff --git a/http.go b/http.go deleted file mode 100644 index 0f95ec7..0000000 --- a/http.go +++ /dev/null @@ -1,106 +0,0 @@ -package aicra - -import ( - "log" - "net/http" - - "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/aicra/internal/reqdata" -) - -// httpHandler wraps the aicra server to allow handling http requests -type httpHandler Builder - -// ServeHTTP implements http.Handler and has to be called on each request -func (server httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - - // 1. find a matching service in the config - service := server.conf.Find(req) - if service == nil { - response := api.EmptyResponse().WithError(api.ErrorUnknownService) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 2. build input parameter receiver - dataset := reqdata.New(service) - - // 3. extract URI data - err := dataset.ExtractURI(req) - if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 4. extract query data - err = dataset.ExtractQuery(req) - if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 5. extract form/json data - err = dataset.ExtractForm(req) - if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 6. find a matching handler - var handler *apiHandler - for _, h := range server.handlers { - if h.Method == service.Method && h.Path == service.Pattern { - handler = h - } - } - - // 7. fail if found no handler - if handler == nil { - r := api.EmptyResponse().WithError(api.ErrorUnknownService) - r.ServeHTTP(res, req) - logError(r) - return - } - - // 8. build api.Request from http.Request - apireq, err := api.NewRequest(req) - if err != nil { - log.Fatal(err) - } - - // 9. feed request with scope & parameters - apireq.Scope = service.Scope - apireq.Param = dataset.Data - - // 10. execute - returned, apiErr := handler.dyn.Handle(dataset.Data) - response := api.EmptyResponse().WithError(apiErr) - for key, value := range returned { - - // find original name from rename - for name, param := range service.Output { - if param.Rename == key { - response.SetData(name, value) - } - } - } - - // 11. apply headers - res.Header().Set("Content-Type", "application/json; charset=utf-8") - for key, values := range response.Headers { - for _, value := range values { - res.Header().Add(key, value) - } - } - - // 12. write to response - response.ServeHTTP(res, req) -} diff --git a/server.go b/server.go index 75df07a..4d11c75 100644 --- a/server.go +++ b/server.go @@ -1,99 +1,106 @@ package aicra import ( - "fmt" - "io" + "log" "net/http" - "git.xdrm.io/go/aicra/datatype" - "git.xdrm.io/go/aicra/dynfunc" - "git.xdrm.io/go/aicra/internal/config" + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/internal/reqdata" ) -// Builder for an aicra server -type Builder struct { - conf *config.Server - handlers []*apiHandler -} +// Server hides the builder and allows handling http requests +type Server Builder -// represents an server handler -type apiHandler struct { - Method string - Path string - dyn *dynfunc.Handler -} +// ServeHTTP implements http.Handler and is called on each request +func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() -// AddType adds an available datatype to the api definition -func (b *Builder) AddType(t datatype.T) { - if b.conf == nil { - b.conf = &config.Server{} - } - if b.conf.Services != nil { - panic(ErrLateType) - } - b.conf.Types = append(b.conf.Types, t) -} - -// Setup the builder with its api definition -// panics if already setup -func (b *Builder) Setup(r io.Reader) error { - if b.conf == nil { - b.conf = &config.Server{} - } - if b.conf.Services != nil { - panic(ErrAlreadySetup) - } - return b.conf.Parse(r) -} - -// Bind a dynamic handler to a REST service -func (b *Builder) Bind(method, path string, fn interface{}) error { - if b.conf.Services == nil { - return ErrNotSetup + // 1. find a matching service in the config + service := server.conf.Find(req) + if service == nil { + response := api.EmptyResponse().WithError(api.ErrorUnknownService) + response.ServeHTTP(res, req) + logError(response) + return } - // find associated service - var service *config.Service - for _, s := range b.conf.Services { - if method == s.Method && path == s.Pattern { - service = s - break + // 2. build input parameter receiver + dataset := reqdata.New(service) + + // 3. extract URI data + err := dataset.ExtractURI(req) + if err != nil { + response := api.EmptyResponse().WithError(api.ErrorMissingParam) + response.ServeHTTP(res, req) + logError(response) + return + } + + // 4. extract query data + err = dataset.ExtractQuery(req) + if err != nil { + response := api.EmptyResponse().WithError(api.ErrorMissingParam) + response.ServeHTTP(res, req) + logError(response) + return + } + + // 5. extract form/json data + err = dataset.ExtractForm(req) + if err != nil { + response := api.EmptyResponse().WithError(api.ErrorMissingParam) + response.ServeHTTP(res, req) + logError(response) + return + } + + // 6. find a matching handler + var handler *apiHandler + for _, h := range server.handlers { + if h.Method == service.Method && h.Path == service.Pattern { + handler = h } } - if service == nil { - return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService) + // 7. fail if found no handler + if handler == nil { + r := api.EmptyResponse().WithError(api.ErrorUnknownService) + r.ServeHTTP(res, req) + logError(r) + return } - dyn, err := dynfunc.Build(fn, *service) + // 8. build api.Request from http.Request + apireq, err := api.NewRequest(req) if err != nil { - return fmt.Errorf("%s '%s' handler: %w", method, path, err) + log.Fatal(err) } - b.handlers = append(b.handlers, &apiHandler{ - Path: path, - Method: method, - dyn: dyn, - }) + // 9. feed request with scope & parameters + apireq.Scope = service.Scope + apireq.Param = dataset.Data - return nil -} + // 10. execute + returned, apiErr := handler.dyn.Handle(dataset.Data) + response := api.EmptyResponse().WithError(apiErr) + for key, value := range returned { -// Build a fully-featured HTTP server -func (b Builder) Build() (http.Handler, error) { - - for _, service := range b.conf.Services { - var hasAssociatedHandler bool - for _, handler := range b.handlers { - if handler.Method == service.Method && handler.Path == service.Pattern { - hasAssociatedHandler = true - break + // find original name from rename + for name, param := range service.Output { + if param.Rename == key { + response.SetData(name, value) } } - if !hasAssociatedHandler { - return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler) + } + + // 11. apply headers + res.Header().Set("Content-Type", "application/json; charset=utf-8") + for key, values := range response.Headers { + for _, value := range values { + res.Header().Add(key, value) } } - return httpHandler(b), nil + // 12. write to response + response.ServeHTTP(res, req) } From 5cc3d2d45551a027af38246ca6635202c938cbf3 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 12:03:29 +0200 Subject: [PATCH 17/19] use http.Request instead of pointer --- internal/reqdata/set.go | 12 ++++++------ internal/reqdata/set_test.go | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/reqdata/set.go b/internal/reqdata/set.go index 4baca97..3930927 100644 --- a/internal/reqdata/set.go +++ b/internal/reqdata/set.go @@ -39,7 +39,7 @@ func New(service *config.Service) *Set { } // ExtractURI fills 'Set' with creating pointers inside 'Url' -func (i *Set) ExtractURI(req *http.Request) error { +func (i *Set) ExtractURI(req http.Request) error { uriparts := config.SplitURL(req.URL.RequestURI()) for _, capture := range i.service.Captures { @@ -71,7 +71,7 @@ func (i *Set) ExtractURI(req *http.Request) error { } // ExtractQuery data from the url query parameters -func (i *Set) ExtractQuery(req *http.Request) error { +func (i *Set) ExtractQuery(req http.Request) error { query := req.URL.Query() for name, param := range i.service.Query { @@ -108,7 +108,7 @@ func (i *Set) ExtractQuery(req *http.Request) error { // - parse 'form-data' if not supported for non-POST requests // - parse 'x-www-form-urlencoded' // - parse 'application/json' -func (i *Set) ExtractForm(req *http.Request) error { +func (i *Set) ExtractForm(req http.Request) error { // ignore GET method if req.Method == http.MethodGet { @@ -138,7 +138,7 @@ func (i *Set) ExtractForm(req *http.Request) error { // parseJSON parses JSON from the request body inside 'Form' // and 'Set' -func (i *Set) parseJSON(req *http.Request) error { +func (i *Set) parseJSON(req http.Request) error { parsed := make(map[string]interface{}, 0) @@ -178,7 +178,7 @@ func (i *Set) parseJSON(req *http.Request) error { // parseUrlencoded parses urlencoded from the request body inside 'Form' // and 'Set' -func (i *Set) parseUrlencoded(req *http.Request) error { +func (i *Set) parseUrlencoded(req http.Request) error { // use http.Request interface if err := req.ParseForm(); err != nil { return err @@ -215,7 +215,7 @@ func (i *Set) parseUrlencoded(req *http.Request) error { // parseMultipart parses multi-part from the request body inside 'Form' // and 'Set' -func (i *Set) parseMultipart(req *http.Request) error { +func (i *Set) parseMultipart(req http.Request) error { // 1. create reader boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] diff --git a/internal/reqdata/set_test.go b/internal/reqdata/set_test.go index efcb459..7496327 100644 --- a/internal/reqdata/set_test.go +++ b/internal/reqdata/set_test.go @@ -131,7 +131,7 @@ func TestStoreWithUri(t *testing.T) { store := New(service) req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil) - err := store.ExtractURI(req) + err := store.ExtractURI(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -242,7 +242,7 @@ func TestExtractQuery(t *testing.T) { store := New(getServiceWithQuery(test.ServiceParam...)) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil) - err := store.ExtractQuery(req) + err := store.ExtractQuery(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -324,7 +324,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) { // defer req.Body.Close() store := New(nil) - err := store.ExtractForm(req) + err := store.ExtractForm(*req) if err == nil { t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data)) t.FailNow() @@ -420,7 +420,7 @@ func TestExtractFormUrlEncoded(t *testing.T) { defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(req) + err := store.ExtractForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -563,7 +563,7 @@ func TestJsonParameters(t *testing.T) { defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(req) + err := store.ExtractForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -720,7 +720,7 @@ x defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(req) + err := store.ExtractForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { From 60ef4717a837fbf6218a63cd48b8029e2d84678a Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 12:05:17 +0200 Subject: [PATCH 18/19] clarity: aicra server request management --- api/request.go | 9 ++--- server.go | 92 ++++++++++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 55 deletions(-) diff --git a/api/request.go b/api/request.go index 0461ed8..8259836 100644 --- a/api/request.go +++ b/api/request.go @@ -22,21 +22,16 @@ type Request struct { } // NewRequest builds an interface request from a http.Request -func NewRequest(req *http.Request) (*Request, error) { - - // 1. get useful data +func NewRequest(req *http.Request) *Request { uri := normaliseURI(req.URL.Path) uriparts := strings.Split(uri, "/") - // 3. Init request - inst := &Request{ + return &Request{ URI: uriparts, Scope: nil, Request: req, Param: make(RequestParam), } - - return inst, nil } // normaliseURI removes the trailing '/' to always diff --git a/server.go b/server.go index 4d11c75..ab4423a 100644 --- a/server.go +++ b/server.go @@ -1,10 +1,10 @@ package aicra import ( - "log" "net/http" "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/reqdata" ) @@ -18,43 +18,18 @@ 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 { - response := api.EmptyResponse().WithError(api.ErrorUnknownService) - response.ServeHTTP(res, req) - logError(response) + errorHandler(api.ErrorUnknownService) return } - // 2. build input parameter receiver - dataset := reqdata.New(service) - - // 3. extract URI data - err := dataset.ExtractURI(req) + // 2. extract request data + dataset, err := extractRequestData(service, *req) if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) + errorHandler(api.ErrorMissingParam) return } - // 4. extract query data - err = dataset.ExtractQuery(req) - if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 5. extract form/json data - err = dataset.ExtractForm(req) - if err != nil { - response := api.EmptyResponse().WithError(api.ErrorMissingParam) - response.ServeHTTP(res, req) - logError(response) - return - } - - // 6. find a matching handler + // 3. find a matching handler var handler *apiHandler for _, h := range server.handlers { if h.Method == service.Method && h.Path == service.Pattern { @@ -62,26 +37,16 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { } } - // 7. fail if found no handler + // 4. fail if found no handler if handler == nil { - r := api.EmptyResponse().WithError(api.ErrorUnknownService) - r.ServeHTTP(res, req) - logError(r) + errorHandler(api.ErrorUncallableService) return } - // 8. build api.Request from http.Request - apireq, err := api.NewRequest(req) - if err != nil { - log.Fatal(err) - } - - // 9. feed request with scope & parameters - apireq.Scope = service.Scope - apireq.Param = dataset.Data - - // 10. execute + // 5. execute returned, apiErr := handler.dyn.Handle(dataset.Data) + + // 6. build response from returned data response := api.EmptyResponse().WithError(apiErr) for key, value := range returned { @@ -93,7 +58,7 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { } } - // 11. apply headers + // 7. apply headers res.Header().Set("Content-Type", "application/json; charset=utf-8") for key, values := range response.Headers { for _, value := range values { @@ -101,6 +66,37 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { } } - // 12. write to response 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) + logError(r) + } +} + +func extractRequestData(service *config.Service, req http.Request) (*reqdata.Set, error) { + dataset := reqdata.New(service) + + // 3. extract URI data + err := dataset.ExtractURI(req) + if err != nil { + return nil, err + } + + // 4. extract query data + err = dataset.ExtractQuery(req) + if err != nil { + return nil, err + } + + // 5. extract form/json data + err = dataset.ExtractForm(req) + if err != nil { + return nil, err + } + + return dataset, nil +} From 92da498d49d7caea9bb701e4f4c06f67918ae64e Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 4 Apr 2020 12:06:31 +0200 Subject: [PATCH 19/19] remove server logs and util file --- server.go | 1 - util.go | 15 --------------- 2 files changed, 16 deletions(-) delete mode 100644 util.go diff --git a/server.go b/server.go index ab4423a..15fd030 100644 --- a/server.go +++ b/server.go @@ -73,7 +73,6 @@ func errorHandler(err api.Error) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { r := api.EmptyResponse().WithError(err) r.ServeHTTP(res, req) - logError(r) } } diff --git a/util.go b/util.go deleted file mode 100644 index a143dcd..0000000 --- a/util.go +++ /dev/null @@ -1,15 +0,0 @@ -package aicra - -import ( - "log" - "net/http" - - "git.xdrm.io/go/aicra/api" -) - -var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} - -// Prints an error as HTTP response -func logError(res *api.Response) { - log.Printf("[http.fail] %v\n", res) -}