From a6f5083f0dfbecbaca0f2f28c9ede9e041889ac9 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 14 Mar 2020 15:24:17 +0100 Subject: [PATCH] bcupdate: make config flat, rewrite, simplify, test --- config/config_test.go | 632 ++++++++++++++++++ config/errors.go | 48 ++ config/parameter.go | 22 + config/service.go | 116 ++++ config/services.go | 79 +++ internal/config/config.go => config/types.go | 27 +- internal/config/config_test.go | 636 ------------------- internal/config/errors.go | 27 - internal/config/method.go | 69 -- internal/config/service.go | 104 --- 10 files changed, 909 insertions(+), 851 deletions(-) create mode 100644 config/config_test.go create mode 100644 config/errors.go create mode 100644 config/parameter.go create mode 100644 config/service.go create mode 100644 config/services.go rename internal/config/config.go => config/types.go (63%) delete mode 100644 internal/config/config_test.go delete mode 100644 internal/config/errors.go delete mode 100644 internal/config/method.go delete mode 100644 internal/config/service.go diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..2776292 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,632 @@ +package config + +import ( + "errors" + "fmt" + "strings" + "testing" +) + +func TestLegalServiceName(t *testing.T) { + tests := []struct { + Raw string + Error error + }{ + // empty + { + `[ { "method": "GET", "info": "a", "path": "" } ]`, + ErrInvalidPattern, + }, + { + `[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`, + ErrInvalidPattern, + }, + { + `[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`, + ErrInvalidPattern, + }, + { + `[ { "method": "GET", "info": "a", "path": "/" } ]`, + nil, + }, + { + `[ { "method": "GET", "info": "a", "path": "/valid-name" } ]`, + nil, + }, + { + `[ { "method": "GET", "info": "a", "path": "/valid/nested/name" } ]`, + nil, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`, + ErrInvalidPatternBracePosition, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`, + ErrInvalidPatternBracePosition, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`, + nil, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`, + ErrInvalidPatternBracePosition, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`, + ErrInvalidPatternBracePosition, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`, + nil, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`, + ErrInvalidPatternOpeningBrace, + }, + { + `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`, + ErrInvalidPatternClosingBrace, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if err == nil && test.Error != nil { + t.Errorf("expected an error: '%s'", test.Error.Error()) + t.FailNow() + } + if err != nil && test.Error == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && test.Error != nil { + if !errors.Is(err, test.Error) { + t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) + t.FailNow() + } + } + }) + } +} +func TestAvailableMethods(t *testing.T) { + tests := []struct { + Raw string + ValidMethod bool + }{ + { // missing description + `[ { "method": "GET", "path": "/", "info": "valid-description" }]`, + true, + }, + { // missing description + `[ { "method": "POST", "path": "/", "info": "valid-description" }]`, + true, + }, + { // empty description + `[ { "method": "PUT", "path": "/", "info": "valid-description" }]`, + true, + }, + { // empty trimmed description + `[ { "method": "DELETE", "path": "/", "info": "valid-description" }]`, + true, + }, + { // valid description + `[ { "method": "get", "path": "/", "info": "valid-description" }]`, + false, + }, + { // valid description + `[ { "method": "UNknOwN", "path": "/", "info": "valid-description" }]`, + false, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if test.ValidMethod && err != nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) { + t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err) + t.FailNow() + } + }) + } +} +func TestParseEmpty(t *testing.T) { + reader := strings.NewReader(`[]`) + _, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error (got '%s')", err) + t.FailNow() + } +} +func TestParseJsonError(t *testing.T) { + reader := strings.NewReader(`{ + "GET": { + "info": "info + }, + }`) // trailing ',' is invalid JSON + _, err := Parse(reader) + if err == nil { + t.Errorf("expected error") + t.FailNow() + } +} + +func TestParseMissingMethodDescription(t *testing.T) { + tests := []struct { + Raw string + ValidDescription bool + }{ + { // missing description + `[ { "method": "GET", "path": "/" }]`, + false, + }, + { // missing description + `[ { "method": "GET", "path": "/subservice" }]`, + false, + }, + { // empty description + `[ { "method": "GET", "path": "/", "info": "" }]`, + false, + }, + { // empty trimmed description + `[ { "method": "GET", "path": "/", "info": " " }]`, + false, + }, + { // valid description + `[ { "method": "GET", "path": "/", "info": "a" }]`, + true, + }, + { // valid description + `[ { "method": "GET", "path": "/", "info": "some description" }]`, + true, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if test.ValidDescription && err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) { + t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err) + t.FailNow() + } + }) + } + +} + +func TestParamEmptyRenameNoRename(t *testing.T) { + reader := strings.NewReader(`[ + { + "method": "GET", + "path": "/", + "info": "valid-description", + "in": { + "original": { "info": "valid-desc", "type": "valid-type", "name": "" } + } + } + ]`) + srv, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + if len(srv) < 1 { + t.Errorf("expected a service") + t.FailNow() + } + + for _, param := range (srv)[0].Input { + if param.Rename != "original" { + t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename) + t.FailNow() + } + } + +} +func TestOptionalParam(t *testing.T) { + reader := strings.NewReader(`[ + { + "method": "GET", + "path": "/", + "info": "valid-description", + "in": { + "optional": { "info": "valid-desc", "type": "?optional-type" }, + "required": { "info": "valid-desc", "type": "required-type" }, + "required2": { "info": "valid-desc", "type": "a" }, + "optional2": { "info": "valid-desc", "type": "?a" } + } + } + ]`) + srv, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + if len(srv) < 1 { + t.Errorf("expected a service") + t.FailNow() + } + for pName, param := range (srv)[0].Input { + + if pName == "optional" || pName == "optional2" { + if !param.Optional { + t.Errorf("expected parameter '%s' to be optional", pName) + t.Failed() + } + } + if pName == "required" || pName == "required2" { + if param.Optional { + t.Errorf("expected parameter '%s' to be required", pName) + t.Failed() + } + } + } + +} +func TestParseParameters(t *testing.T) { + tests := []struct { + Raw string + Error error + }{ + { // invalid param name prefix + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "_param1": { } + } + } + ]`, + ErrIllegalParamName, + }, + { // invalid param name suffix + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1_": { } + } + } + ]`, + ErrIllegalParamName, + }, + + { // missing param description + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { } + } + } + ]`, + ErrMissingParamDesc, + }, + { // empty param description + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "" } + } + } + ]`, + ErrMissingParamDesc, + }, + + { // missing param type + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid" } + } + } + ]`, + ErrMissingParamType, + }, + { // empty param type + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "" } + } + } + ]`, + ErrMissingParamType, + }, + { // invalid type (optional mark only) + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "?" } + } + } + ]`, + + ErrMissingParamType, + }, + { // valid description + valid type + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "a" } + } + } + ]`, + nil, + }, + { // valid description + valid OPTIONAL type + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "?valid" } + } + } + ]`, + nil, + }, + + { // name conflict with rename + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid" }, + "param2": { "info": "valid", "type": "valid", "name": "param1" } + } + } + ]`, + // 2 possible errors as map order is not deterministic + ErrParamNameConflict, + }, + { // rename conflict with name + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "param2" }, + "param2": { "info": "valid", "type": "valid" } + } + } + ]`, + // 2 possible errors as map order is not deterministic + ErrParamNameConflict, + }, + { // rename conflict with rename + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "conflict" }, + "param2": { "info": "valid", "type": "valid", "name": "conflict" } + } + } + ]`, + // 2 possible errors as map order is not deterministic + ErrParamNameConflict, + }, + + { // both renamed with no conflict + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "freename" }, + "param2": { "info": "valid", "type": "valid", "name": "freename2" } + } + } + ]`, + nil, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if err == nil && test.Error != nil { + t.Errorf("expected an error: '%s'", test.Error.Error()) + t.FailNow() + } + if err != nil && test.Error == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && test.Error != nil { + if !errors.Is(err, test.Error) { + t.Errorf("expected the error <%s> got <%s>", test.Error, err) + t.FailNow() + } + } + }) + } + +} + +// todo: rewrite with new api format +// func TestMatchSimple(t *testing.T) { +// tests := []struct { +// Raw string +// Path []string +// BrowseDepth int +// ValidDepth bool +// }{ +// { // false positive -1 +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// } +// } +// } +// }`, +// []string{"parent", "subdir"}, +// 1, +// false, +// }, +// { // false positive +1 +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// } +// } +// } +// }`, +// []string{"parent", "subdir"}, +// 3, +// false, +// }, + +// { +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// } +// } +// } +// }`, +// []string{"parent", "subdir"}, +// 2, +// true, +// }, +// { // unknown path +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// } +// } +// } +// }`, +// []string{"x", "y"}, +// 2, +// false, +// }, +// { // unknown path +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// } +// } +// } +// }`, +// []string{"parent", "y"}, +// 1, +// true, +// }, +// { // Warning: this case is important to understand the precedence of service paths over +// // the value of some variables. Here if we send a string parameter in the GET method that +// // unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead +// // of the service /parent with its parameter set to the value 'subdir'. +// `{ +// "/" : { +// "parent": { +// "/": { +// "subdir": {} +// }, +// "GET": { +// "info": "valid-desc", +// "in": { +// "some-value": { +// "info": "valid-desc", +// "type": "valid-type" +// } +// } +// } +// } +// } +// }`, +// []string{"parent", "subdir"}, +// 2, +// true, +// }, +// } + +// for i, test := range tests { + +// t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { +// srv, err := Parse(strings.NewReader(test.Raw)) + +// if err != nil { +// t.Errorf("unexpected error: '%s'", err) +// t.FailNow() +// } + +// _, depth := srv.Match(test.Path) +// if test.ValidDepth { +// if depth != test.BrowseDepth { +// t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth) +// t.FailNow() +// } +// } else { +// if depth == test.BrowseDepth { +// t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth) +// t.FailNow() +// } + +// } +// }) +// } + +// } diff --git a/config/errors.go b/config/errors.go new file mode 100644 index 0000000..ff9d5d1 --- /dev/null +++ b/config/errors.go @@ -0,0 +1,48 @@ +package config + +// Error allows you to create constant "const" error with type boxing. +type Error string + +// Error implements the error builtin interface. +func (err Error) Error() string { + return string(err) +} + +// ErrRead - a problem ocurred when trying to read the configuration file +const ErrRead = Error("cannot read config") + +// ErrUnknownMethod - invalid http method +const ErrUnknownMethod = Error("unknown HTTP method") + +// ErrFormat - a invalid format has been detected +const ErrFormat = Error("invalid config format") + +// ErrPatternCollision - there is a collision between 2 services' patterns (same method) +const ErrPatternCollision = Error("invalid config format") + +// ErrInvalidPattern - a service pattern is malformed +const ErrInvalidPattern = Error("must begin with a '/' and not end with") + +// ErrInvalidPatternBracePosition - a service pattern opening/closing brace is not directly between '/' +const ErrInvalidPatternBracePosition = Error("capturing braces must be alone between slashes") + +// ErrInvalidPatternOpeningBrace - a service pattern opening brace is invalid +const ErrInvalidPatternOpeningBrace = Error("opening brace already open") + +// ErrInvalidPatternClosingBrace - a service pattern closing brace is invalid +const ErrInvalidPatternClosingBrace = Error("closing brace already closed") + +// ErrMissingDescription - a service is missing its description +const ErrMissingDescription = Error("missing description") + +// ErrMissingParamDesc - a parameter is missing its description +const ErrMissingParamDesc = Error("missing parameter description") + +// ErrIllegalParamName - a parameter has an illegal name +const ErrIllegalParamName = Error("parameter name must not begin/end with '_'") + +// ErrMissingParamType - a parameter has an illegal type +const ErrMissingParamType = Error("missing parameter type") + +// ErrParamNameConflict - a parameter has a conflict with its name/rename field +const ErrParamNameConflict = Error("name conflict for parameter") diff --git a/config/parameter.go b/config/parameter.go new file mode 100644 index 0000000..a64ee53 --- /dev/null +++ b/config/parameter.go @@ -0,0 +1,22 @@ +package config + +func (param *Parameter) checkAndFormat() error { + + // missing description + if len(param.Description) < 1 { + return ErrMissingParamDesc + } + + // invalid type + if len(param.Type) < 1 || param.Type == "?" { + return ErrMissingParamType + } + + // set optional + type + if param.Type[0] == '?' { + param.Optional = true + param.Type = param.Type[1:] + } + + return nil +} diff --git a/config/service.go b/config/service.go new file mode 100644 index 0000000..cd2a0ed --- /dev/null +++ b/config/service.go @@ -0,0 +1,116 @@ +package config + +import ( + "fmt" + "net/http" + "strings" +) + +// Match returns if this service would handle this HTTP request +func (svc *Service) Match(req *http.Request) bool { + return false +} + +func (svc *Service) checkMethod() error { + for _, available := range availableHTTPMethods { + if svc.Method == available { + return nil + } + } + return ErrUnknownMethod +} + +func (svc *Service) checkPattern() error { + length := len(svc.Pattern) + + // empty pattern + if length < 1 { + return ErrInvalidPattern + } + + if length > 1 { + // pattern not starting with '/' or ending with '/' + if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' { + return ErrInvalidPattern + } + } + + // check capturing braces + depth := 0 + for c, l := 1, length; c < l; c++ { + char := svc.Pattern[c] + + if char == '{' { + // opening brace when already opened + if depth != 0 { + return ErrInvalidPatternOpeningBrace + } + + // not directly preceded by a slash + if svc.Pattern[c-1] != '/' { + return ErrInvalidPatternBracePosition + } + depth++ + } + if char == '}' { + // closing brace when already closed + if depth != 1 { + return ErrInvalidPatternClosingBrace + } + // not directly followed by a slash or end of pattern + if c+1 < l && svc.Pattern[c+1] != '/' { + return ErrInvalidPatternBracePosition + } + depth-- + } + } + + return nil +} + +func (svc *Service) checkAndFormatInput() error { + + // ignore no parameter + if svc.Input == nil || len(svc.Input) < 1 { + svc.Input = make(map[string]*Parameter, 0) + return nil + } + + // for each parameter + for paramName, param := range svc.Input { + + // fail on invalid name + if strings.Trim(paramName, "_") != paramName { + return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName) + } + + // use param name if no rename + if len(param.Rename) < 1 { + param.Rename = paramName + } + + err := param.checkAndFormat() + if err != nil { + return fmt.Errorf("%s: %w", paramName, err) + } + + // check for name/rename conflict + for paramName2, param2 := range svc.Input { + // ignore self + if paramName == paramName2 { + continue + } + + // 3.2.1. Same rename field + // 3.2.2. Not-renamed field matches a renamed field + // 3.2.3. Renamed field matches name + if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename { + return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict) + } + + } + + } + + return nil +} diff --git a/config/services.go b/config/services.go new file mode 100644 index 0000000..6743629 --- /dev/null +++ b/config/services.go @@ -0,0 +1,79 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// Parse builds a server configuration from a json reader and checks for most format errors. +func Parse(r io.Reader) (Services, error) { + services := make(Services, 0) + + err := json.NewDecoder(r).Decode(&services) + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrRead, err) + } + + err = services.checkAndFormat() + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrFormat, err) + } + + if services.collide() { + return nil, fmt.Errorf("%s: %w", ErrFormat, ErrPatternCollision) + } + + return services, nil +} + +// collide returns if there is collision between services +func (svc Services) collide() bool { + // todo: implement pattern collision using types to check if braces can be equal to fixed uri parts + return false +} + +// Find a service matching an incoming HTTP request +func (svc Services) Find(r *http.Request) *Service { + for _, service := range svc { + if service.Match(r) { + return service + } + } + + return nil +} + +// checkAndFormat checks for errors and missing fields and sets default values for optional fields. +func (svc Services) checkAndFormat() error { + for _, service := range svc { + + // check method + err := service.checkMethod() + if err != nil { + return fmt.Errorf("%s '%s' [method]: %w", service.Method, service.Pattern, err) + } + + // check pattern + service.Pattern = strings.Trim(service.Pattern, " \t\r\n") + err = service.checkPattern() + if err != nil { + return fmt.Errorf("%s '%s' [path]: %w", service.Method, service.Pattern, err) + } + + // check description + if len(strings.Trim(service.Description, " \t\r\n")) < 1 { + return fmt.Errorf("%s '%s' [description]: %w", service.Method, service.Pattern, ErrMissingDescription) + } + + // check input parameters + err = service.checkAndFormatInput() + if err != nil { + return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err) + } + + } + return nil +} diff --git a/internal/config/config.go b/config/types.go similarity index 63% rename from internal/config/config.go rename to config/types.go index 2634234..3dda3af 100644 --- a/internal/config/config.go +++ b/config/types.go @@ -4,29 +4,26 @@ import "net/http" var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} -// Service represents a service definition (from api.json) -type Service struct { - GET *Method `json:"GET"` - POST *Method `json:"POST"` - PUT *Method `json:"PUT"` - DELETE *Method `json:"DELETE"` - - Children map[string]*Service `json:"/"` -} - // Parameter represents a parameter definition (from api.json) type Parameter struct { Description string `json:"info"` Type string `json:"type"` Rename string `json:"name,omitempty"` Optional bool - Default *interface{} `json:"default"` + // Default *interface{} `json:"default"` } -// Method represents a method definition (from api.json) -type Method struct { - Description string `json:"info"` +// Service represents a service definition (from api.json) +type Service struct { + Method string `json:"method"` + Pattern string `json:"path"` Scope [][]string `json:"scope"` - Parameters map[string]*Parameter `json:"in"` + Description string `json:"info"` Download *bool `json:"download"` + Input map[string]*Parameter `json:"in"` + // Output map[string]*Parameter `json:"out"` + } + +// Services contains every service that represents a server configuration +type Services []*Service diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index deb3170..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,636 +0,0 @@ -package config - -import ( - "fmt" - "net/http" - "strings" - "testing" -) - -func TestLegalServiceName(t *testing.T) { - tests := []struct { - Raw string - Error error - }{ - { - `{ - "/": { - "invalid/service-name": { - - } - } - }`, - ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service-name")), - }, - { - `{ - "/": { - "invalid/service/name": { - - } - } - }`, - ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service/name")), - }, - { - `{ - "/": { - "invalid-service-name": { - - } - } - }`, - ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid-service-name")), - }, - - { - `{ - "/": { - "valid.service_name": { - } - } - }`, - nil, - }, - } - - for i, test := range tests { - - t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) - - if err == nil && test.Error != nil { - t.Errorf("expected an error: '%s'", test.Error.Error()) - t.FailNow() - } - if err != nil && test.Error == nil { - t.Errorf("unexpected error: '%s'", err.Error()) - t.FailNow() - } - - if err != nil && test.Error != nil { - if err.Error() != test.Error.Error() { - t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) - t.FailNow() - } - } - }) - } -} -func TestAvailableMethods(t *testing.T) { - reader := strings.NewReader(`{ - "GET": { "info": "info" }, - "POST": { "info": "info" }, - "PUT": { "info": "info" }, - "DELETE": { "info": "info" } - }`) - srv, err := Parse(reader) - if err != nil { - t.Errorf("unexpected error (got '%s')", err) - t.FailNow() - } - - if srv.Method(http.MethodGet) == nil { - t.Errorf("expected method GET to be available") - t.Fail() - } - if srv.Method(http.MethodPost) == nil { - t.Errorf("expected method POST to be available") - t.Fail() - } - if srv.Method(http.MethodPut) == nil { - t.Errorf("expected method PUT to be available") - t.Fail() - } - if srv.Method(http.MethodDelete) == nil { - t.Errorf("expected method DELETE to be available") - t.Fail() - } - - if srv.Method(http.MethodPatch) != nil { - t.Errorf("expected method PATH to be UNavailable") - t.Fail() - } -} -func TestParseEmpty(t *testing.T) { - reader := strings.NewReader(`{}`) - _, err := Parse(reader) - if err != nil { - t.Errorf("unexpected error (got '%s')", err) - t.FailNow() - } -} -func TestParseJsonError(t *testing.T) { - reader := strings.NewReader(`{ - "GET": { - "info": "info - }, - }`) // trailing ',' is invalid JSON - _, err := Parse(reader) - if err == nil { - t.Errorf("expected error") - t.FailNow() - } -} - -func TestParseMissingMethodDescription(t *testing.T) { - tests := []struct { - Raw string - Error error - }{ - { // missing description - `{ - "GET": { - - } - }`, - ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")), - }, - { // missing description - `{ - "/": { - "subservice": { - "GET": { - - } - } - } - }`, - ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET subservice")), - }, - { // empty description - `{ - "GET": { - "info": "" - } - }`, - ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")), - }, - { // valid description - `{ - "GET": { - "info": "a" - } - }`, - nil, - }, - { // valid description - `{ - "GET": { - "info": "some description" - } - }`, - nil, - }, - } - - for i, test := range tests { - - t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) - - if err == nil && test.Error != nil { - t.Errorf("expected an error: '%s'", test.Error.Error()) - t.FailNow() - } - if err != nil && test.Error == nil { - t.Errorf("unexpected error: '%s'", err.Error()) - t.FailNow() - } - - if err != nil && test.Error != nil { - if err.Error() != test.Error.Error() { - t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) - t.FailNow() - } - } - }) - } - -} - -func TestParamEmptyRenameNoRename(t *testing.T) { - reader := strings.NewReader(`{ - "GET": { - "info": "info", - "in": { - "original": { "info": "valid-desc", "type": "valid-type", "name": "" } - } - } - }`) - srv, err := Parse(reader) - if err != nil { - t.Errorf("unexpected error: '%s'", err) - t.FailNow() - } - - method := srv.Method(http.MethodGet) - if method == nil { - t.Errorf("expected GET method not to be nil") - t.FailNow() - } - for _, param := range method.Parameters { - - if param.Rename != "original" { - t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename) - t.FailNow() - } - } - -} -func TestOptionalParam(t *testing.T) { - reader := strings.NewReader(`{ - "GET": { - "info": "info", - "in": { - "optional": { "info": "valid-desc", "type": "?optional-type" }, - "required": { "info": "valid-desc", "type": "required-type" }, - "required2": { "info": "valid-desc", "type": "a" }, - "optional2": { "info": "valid-desc", "type": "?a" } - } - } - }`) - srv, err := Parse(reader) - if err != nil { - t.Errorf("unexpected error: '%s'", err) - t.FailNow() - } - - method := srv.Method(http.MethodGet) - if method == nil { - t.Errorf("expected GET method not to be nil") - t.FailNow() - } - for pName, param := range method.Parameters { - - if pName == "optional" || pName == "optional2" { - if !param.Optional { - t.Errorf("expected parameter '%s' to be optional", pName) - t.Failed() - } - } - if pName == "required" || pName == "required2" { - if param.Optional { - t.Errorf("expected parameter '%s' to be required", pName) - t.Failed() - } - } - } - -} -func TestParseParameters(t *testing.T) { - tests := []struct { - Raw string - Error error - ErrorAlternative error - }{ - { // invalid param name prefix - `{ - "GET": { - "info": "info", - "in": { - "_param1": {} - } - } - }`, - ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {_param1}")), - nil, - }, - { // invalid param name suffix - `{ - "GET": { - "info": "info", - "in": { - "param1_": {} - } - } - }`, - ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {param1_}")), - nil, - }, - - { // missing param description - `{ - "GET": { - "info": "info", - "in": { - "param1": {} - } - } - }`, - ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")), - nil, - }, - { // empty param description - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "" - } - } - } - }`, - ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")), - nil, - }, - - { // missing param type - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "valid" - } - } - } - }`, - ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), - nil, - }, - { // empty param type - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "valid", - "type": "" - } - } - } - }`, - ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), - nil, - }, - { // invalid type (optional mark only) - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "valid", - "type": "?" - } - } - } - }`, - - ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), - ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), - }, - { // valid description + valid type - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "valid", - "type": "a" - } - } - } - }`, - nil, - nil, - }, - { // valid description + valid OPTIONAL type - `{ - "GET": { - "info": "info", - "in": { - "param1": { - "info": "valid", - "type": "?valid" - } - } - } - }`, - nil, - nil, - }, - - { // name conflict with rename - `{ - "GET": { - "info": "info", - "in": { - "param1": { "info": "valid", "type": "valid" }, - "param2": { "info": "valid", "type": "valid", "name": "param1" } - - } - } - }`, - // 2 possible errors as map order is not deterministic - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), - }, - { // rename conflict with name - `{ - "GET": { - "info": "info", - "in": { - "param1": { "info": "valid", "type": "valid", "name": "param2" }, - "param2": { "info": "valid", "type": "valid" } - - } - } - }`, - // 2 possible errors as map order is not deterministic - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), - }, - { // rename conflict with rename - `{ - "GET": { - "info": "info", - "in": { - "param1": { "info": "valid", "type": "valid", "name": "conflict" }, - "param2": { "info": "valid", "type": "valid", "name": "conflict" } - - } - } - }`, - // 2 possible errors as map order is not deterministic - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), - ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), - }, - - { // both renamed with no conflict - `{ - "GET": { - "info": "info", - "in": { - "param1": { "info": "valid", "type": "valid", "name": "freename" }, - "param2": { "info": "valid", "type": "valid", "name": "freename2" } - - } - } - }`, - nil, - nil, - }, - } - - for i, test := range tests { - - t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) - - if err == nil && test.Error != nil { - t.Errorf("expected an error: '%s'", test.Error.Error()) - t.FailNow() - } - if err != nil && test.Error == nil { - t.Errorf("unexpected error: '%s'", err.Error()) - t.FailNow() - } - - if err != nil && test.Error != nil { - if err.Error() != test.Error.Error() && err.Error() != test.ErrorAlternative.Error() { - t.Errorf("got the error: '%s'", err.Error()) - t.Errorf("expected error (alternative 1): '%s'", test.Error.Error()) - t.Errorf("expected error (alternative 2): '%s'", test.ErrorAlternative.Error()) - t.FailNow() - } - } - }) - } - -} - -func TestBrowseSimple(t *testing.T) { - tests := []struct { - Raw string - Path []string - BrowseDepth int - ValidDepth bool - }{ - { // false positive -1 - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - } - } - } - }`, - []string{"parent", "subdir"}, - 1, - false, - }, - { // false positive +1 - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - } - } - } - }`, - []string{"parent", "subdir"}, - 3, - false, - }, - - { - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - } - } - } - }`, - []string{"parent", "subdir"}, - 2, - true, - }, - { // unknown path - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - } - } - } - }`, - []string{"x", "y"}, - 2, - false, - }, - { // unknown path - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - } - } - } - }`, - []string{"parent", "y"}, - 1, - true, - }, - { // Warning: this case is important to understand the precedence of service paths over - // the value of some variables. Here if we send a string parameter in the GET method that - // unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead - // of the service /parent with its parameter set to the value 'subdir'. - `{ - "/" : { - "parent": { - "/": { - "subdir": {} - }, - "GET": { - "info": "valid-desc", - "in": { - "some-value": { - "info": "valid-desc", - "type": "valid-type" - } - } - } - } - } - }`, - []string{"parent", "subdir"}, - 2, - true, - }, - } - - for i, test := range tests { - - t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - srv, err := Parse(strings.NewReader(test.Raw)) - - if err != nil { - t.Errorf("unexpected error: '%s'", err) - t.FailNow() - } - - _, depth := srv.Browse(test.Path) - if test.ValidDepth { - if depth != test.BrowseDepth { - t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth) - t.FailNow() - } - } else { - if depth == test.BrowseDepth { - t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth) - t.FailNow() - } - - } - }) - } - -} diff --git a/internal/config/errors.go b/internal/config/errors.go deleted file mode 100644 index ef413e8..0000000 --- a/internal/config/errors.go +++ /dev/null @@ -1,27 +0,0 @@ -package config - -import "git.xdrm.io/go/aicra/internal/cerr" - -// ErrRead - a problem ocurred when trying to read the configuration file -const ErrRead = cerr.Error("cannot read config") - -// ErrFormat - a invalid format has been detected -const ErrFormat = cerr.Error("invalid config format") - -// ErrIllegalServiceName - an illegal character has been found in a service name -const ErrIllegalServiceName = cerr.Error("service must not contain any slash '/' nor '-' symbols") - -// ErrMissingMethodDesc - a method is missing its description -const ErrMissingMethodDesc = cerr.Error("missing method description") - -// ErrMissingParamDesc - a parameter is missing its description -const ErrMissingParamDesc = cerr.Error("missing parameter description") - -// ErrIllegalParamName - a parameter has an illegal name -const ErrIllegalParamName = cerr.Error("illegal parameter name (must not begin/end with '_')") - -// ErrMissingParamType - a parameter has an illegal type -const ErrMissingParamType = cerr.Error("missing parameter type") - -// ErrParamNameConflict - a parameter has a conflict with its name/rename field -const ErrParamNameConflict = cerr.Error("name conflict for parameter") diff --git a/internal/config/method.go b/internal/config/method.go deleted file mode 100644 index 14c3539..0000000 --- a/internal/config/method.go +++ /dev/null @@ -1,69 +0,0 @@ -package config - -import ( - "strings" -) - -// checkAndFormat checks for errors and missing fields and sets default values for optional fields. -func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) error { - - // 1. fail on missing description - if len(methodDef.Description) < 1 { - return ErrMissingMethodDesc.WrapString(httpMethod + " " + servicePath) - } - - // 2. stop if no parameter - if methodDef.Parameters == nil || len(methodDef.Parameters) < 1 { - methodDef.Parameters = make(map[string]*Parameter, 0) - return nil - } - - // 3. for each parameter - for pName, pData := range methodDef.Parameters { - - // 3.1. check name - if strings.Trim(pName, "_") != pName { - return ErrIllegalParamName.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - - if len(pData.Rename) < 1 { - pData.Rename = pName - } - - // 3.2. Check for name/rename conflict - for paramName, param := range methodDef.Parameters { - - // ignore self - if pName == paramName { - continue - } - - // 3.2.1. Same rename field - // 3.2.2. Not-renamed field matches a renamed field - // 3.2.3. Renamed field matches name - if pData.Rename == param.Rename || pName == param.Rename || pData.Rename == paramName { - return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - - } - - // 3.3. Fail on missing description - if len(pData.Description) < 1 { - return ErrMissingParamDesc.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - - // 3.4. Manage invalid type - if len(pData.Type) < 1 || pData.Type == "?" { - return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - - // 3.5. Set optional + type - if pData.Type[0] == '?' { - pData.Optional = true - pData.Type = pData.Type[1:] - } - - } - - return nil -} diff --git a/internal/config/service.go b/internal/config/service.go deleted file mode 100644 index 1c97701..0000000 --- a/internal/config/service.go +++ /dev/null @@ -1,104 +0,0 @@ -package config - -import ( - "encoding/json" - "io" - "net/http" - "strings" -) - -// Parse builds a service from a json reader and checks for most format errors. -func Parse(r io.Reader) (*Service, error) { - receiver := &Service{} - - err := json.NewDecoder(r).Decode(receiver) - if err != nil { - return nil, ErrRead.Wrap(err) - } - - err = receiver.checkAndFormat("/") - if err != nil { - return nil, ErrFormat.Wrap(err) - } - - return receiver, nil -} - -// Method returns the actual method from the http method. -func (svc *Service) Method(httpMethod string) *Method { - httpMethod = strings.ToUpper(httpMethod) - - switch httpMethod { - case http.MethodGet: - return svc.GET - case http.MethodPost: - return svc.POST - case http.MethodPut: - return svc.PUT - case http.MethodDelete: - return svc.DELETE - } - - return nil -} - -// Browse the service childtree and returns the deepest matching child. The `path` is a formatted URL split by '/' -func (svc *Service) Browse(path []string) (*Service, int) { - currentService := svc - var depth int - - // for each URI depth - for depth = 0; depth < len(path); depth++ { - currentPath := path[depth] - - child, exists := currentService.Children[currentPath] - if !exists { - break - } - currentService = child - - } - - return currentService, depth -} - -// checkAndFormat checks for errors and missing fields and sets default values for optional fields. -func (svc *Service) checkAndFormat(servicePath string) error { - - // 1. check and format every method - for _, httpMethod := range availableHTTPMethods { - methodDef := svc.Method(httpMethod) - if methodDef == nil { - continue - } - - err := methodDef.checkAndFormat(servicePath, httpMethod) - if err != nil { - return err - } - } - - // 2. stop if no child */ - if svc.Children == nil || len(svc.Children) < 1 { - return nil - } - - // 3. for each service */ - for childService, ctl := range svc.Children { - - // 3.1. invalid name */ - if strings.ContainsAny(childService, "/-") { - return ErrIllegalServiceName.WrapString(childService) - } - - // 3.2. check recursively */ - err := ctl.checkAndFormat(childService) - if err != nil { - return err - } - - } - - return nil - -}