config: refactor, simplify, test, remove redundant comments

This commit is contained in:
Adrien Marquès 2020-04-04 15:39:00 +02:00
parent 990bb86919
commit 30862195a1
Signed by: xdrm-brackets
GPG Key ID: D75243CA236D825E
5 changed files with 236 additions and 135 deletions

View File

@ -16,17 +16,18 @@ type Server struct {
Services []*Service Services []*Service
} }
// Parse a reader into a server. Server.Types must be set beforehand to // Parse a configuration into a server. Server.Types must be set beforehand to
// make datatypes available when checking and formatting the read configuration. // make datatypes available when checking and formatting the read configuration.
func (srv *Server) Parse(r io.Reader) error { func (srv *Server) Parse(r io.Reader) error {
if err := json.NewDecoder(r).Decode(&srv.Services); err != nil { err := json.NewDecoder(r).Decode(&srv.Services)
if err != nil {
return fmt.Errorf("%s: %w", errRead, err) return fmt.Errorf("%s: %w", errRead, err)
} }
if err := srv.validate(); err != nil { err = srv.validate()
if err != nil {
return fmt.Errorf("%s: %w", errFormat, err) return fmt.Errorf("%s: %w", errFormat, err)
} }
return nil return nil
} }
@ -39,11 +40,9 @@ func (server Server) validate(datatypes ...datatype.T) error {
} }
} }
// check for collisions
if err := server.collide(); err != nil { if err := server.collide(); err != nil {
return fmt.Errorf("%s: %w", errFormat, err) return fmt.Errorf("%s: %w", errFormat, err)
} }
return nil return nil
} }
@ -58,7 +57,11 @@ func (server Server) Find(r *http.Request) *Service {
return nil return nil
} }
// collide returns if there is collision between services // collide returns if there is collision between any service for the same method and colliding paths.
// Note that service path collision detection relies on datatypes:
// - example 1: `/user/{id}` and `/user/articles` will not collide as {id} is an int and "articles" is not
// - example 2: `/user/{name}` and `/user/articles` will collide as {name} is a string so as "articles"
// - example 3: `/user/{name}` and `/user/{id}` will collide as {name} and {id} cannot be checked against their potential values
func (server *Server) collide() error { func (server *Server) collide() error {
length := len(server.Services) length := len(server.Services)
@ -68,103 +71,104 @@ func (server *Server) collide() error {
aService := server.Services[a] aService := server.Services[a]
bService := server.Services[b] bService := server.Services[b]
// ignore different method
if aService.Method != bService.Method { if aService.Method != bService.Method {
continue continue
} }
aParts := SplitURL(aService.Pattern) aURIParts := SplitURL(aService.Pattern)
bParts := SplitURL(bService.Pattern) bURIParts := SplitURL(bService.Pattern)
if len(aURIParts) != len(bURIParts) {
// not same size
if len(aParts) != len(bParts) {
continue continue
} }
partErrors := make([]error, 0) err := checkURICollision(aURIParts, bURIParts, aService.Input, bService.Input)
if err != nil {
// for each part return fmt.Errorf("(%s '%s') vs (%s '%s'): %w", aService.Method, aService.Pattern, bService.Method, bService.Pattern, err)
for pi, aPart := range aParts {
bPart := bParts[pi]
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
// both captures -> as we cannot check, consider a collision
if aIsCapture && bIsCapture {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (path %s and %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, aPart, bPart))
continue
}
// no capture -> check equal
if !aIsCapture && !bIsCapture {
if aPart == bPart {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (same path '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, aPart))
continue
}
}
// A captures B -> check type (B is A ?)
if aIsCapture {
input, exists := aService.Input[aPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, aPart))
continue
}
// fail if not valid
if _, valid := input.Validator(bPart); valid {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, aPart, bPart))
continue
}
// B captures A -> check type (A is B ?)
} else if bIsCapture {
input, exists := bService.Input[bPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, bPart))
continue
}
// fail if not valid
if _, valid := input.Validator(aPart); valid {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, errPatternCollision, bPart, aPart))
continue
}
}
partErrors = append(partErrors, nil)
} }
// if at least 1 url part does not match -> ok
var firstError error
oneMismatch := false
for _, err := range partErrors {
if err != nil && firstError == nil {
firstError = err
}
if err == nil {
oneMismatch = true
continue
}
}
if !oneMismatch {
return firstError
}
} }
} }
return nil return nil
} }
// check if uri of services A and B collide
func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter) error {
var errors = []error{}
// for each part
for pi, aPart := range uriA {
bPart := uriB[pi]
// no need for further check as it has been done earlier in the validation process
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
// both captures -> as we cannot check, consider a collision
if aIsCapture && bIsCapture {
errors = append(errors, fmt.Errorf("%w (path %s and %s)", errPatternCollision, aPart, bPart))
continue
}
// no capture -> check strict equality
if !aIsCapture && !bIsCapture {
if aPart == bPart {
errors = append(errors, fmt.Errorf("%w (same path '%s')", errPatternCollision, aPart))
continue
}
}
// A captures B -> check type (B is A ?)
if aIsCapture {
input, exists := inputA[aPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, aPart))
continue
}
// fail if not valid
if _, valid := input.Validator(bPart); valid {
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, aPart, bPart))
continue
}
// B captures A -> check type (A is B ?)
} else if bIsCapture {
input, exists := inputB[bPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, bPart))
continue
}
// fail if not valid
if _, valid := input.Validator(aPart); valid {
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, bPart, aPart))
continue
}
}
errors = append(errors, nil)
}
// at least 1 URI part not matching -> no collision
var firstError error
for _, err := range errors {
if err != nil && firstError == nil {
firstError = err
}
if err == nil {
return nil
}
}
return firstError
}
// SplitURL without empty sets // SplitURL without empty sets
func SplitURL(url string) []string { func SplitURL(url string) []string {
trimmed := strings.Trim(url, " /\t\r\n") trimmed := strings.Trim(url, " /\t\r\n")

View File

@ -877,6 +877,36 @@ func TestMatchSimple(t *testing.T) {
"/a", "/a",
false, false,
}, },
{ // root url
`[ {
"method": "GET",
"path": "/a",
"info": "info",
"in": {}
} ]`,
"/",
false,
},
{
`[ {
"method": "GET",
"path": "/a",
"info": "info",
"in": {}
} ]`,
"/",
false,
},
{
`[ {
"method": "GET",
"path": "/",
"info": "info",
"in": {}
} ]`,
"/",
true,
},
{ {
`[ { `[ {
"method": "GET", "method": "GET",
@ -997,3 +1027,80 @@ func TestMatchSimple(t *testing.T) {
} }
} }
func TestFindPriority(t *testing.T) {
t.Parallel()
tests := []struct {
Config string
URL string
MatchingDesc string
}{
{
`[
{ "method": "GET", "path": "/a", "info": "s1" },
{ "method": "GET", "path": "/", "info": "s2" }
]`,
"/",
"s2",
},
{
`[
{ "method": "GET", "path": "/", "info": "s2" },
{ "method": "GET", "path": "/a", "info": "s1" }
]`,
"/",
"s2",
},
{
`[
{ "method": "GET", "path": "/a", "info": "s1" },
{ "method": "GET", "path": "/", "info": "s2" }
]`,
"/a",
"s1",
},
{
`[
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
{ "method": "GET", "path": "/a/b", "info": "s2" }
]`,
"/a/b/c",
"s1",
},
{
`[
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
{ "method": "GET", "path": "/a/b", "info": "s2" }
]`,
"/a/b/",
"s2",
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
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)
t.FailNow()
}
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
service := srv.Find(req)
if service == nil {
t.Errorf("expected to find a service")
t.FailNow()
}
if service.Description != test.MatchingDesc {
t.Errorf("expected description '%s', got '%s'", test.MatchingDesc, service.Description)
t.FailNow()
}
})
}
}

View File

@ -7,53 +7,53 @@ func (err cerr) Error() string {
return string(err) return string(err)
} }
// errRead - a problem ocurred when trying to read the configuration file // errRead - read error
const errRead = cerr("cannot read config") const errRead = cerr("cannot read config")
// errUnknownMethod - invalid http method // errUnknownMethod - unknown http method
const errUnknownMethod = cerr("unknown HTTP method") const errUnknownMethod = cerr("unknown HTTP method")
// errFormat - a invalid format has been detected // errFormat - invalid format
const errFormat = cerr("invalid config format") const errFormat = cerr("invalid config format")
// errPatternCollision - there is a collision between 2 services' patterns (same method) // errPatternCollision - collision between 2 services' patterns
const errPatternCollision = cerr("pattern collision") const errPatternCollision = cerr("pattern collision")
// errInvalidPattern - a service pattern is malformed // errInvalidPattern - malformed service pattern
const errInvalidPattern = cerr("must begin with a '/' and not end with") const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with")
// errInvalidPatternBraceCapture - a service pattern brace capture is invalid // errInvalidPatternBraceCapture - invalid brace capture
const errInvalidPatternBraceCapture = cerr("invalid uri capturing braces") const errInvalidPatternBraceCapture = cerr("invalid uri parameter")
// errUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern // errUnspecifiedBraceCapture - missing path brace capture
const errUnspecifiedBraceCapture = cerr("capturing brace missing in the path") const errUnspecifiedBraceCapture = cerr("missing uri parameter")
// errMandatoryRename - capture/query parameters must have a rename // errUndefinedBraceCapture - missing capturing brace definition
const errMandatoryRename = cerr("capture and query parameters must have a 'name'") const errUndefinedBraceCapture = cerr("missing uri parameter definition")
// errUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters // errMandatoryRename - capture/query parameters must be renamed
const errUndefinedBraceCapture = cerr("capturing brace missing input definition") const errMandatoryRename = cerr("uri and query parameters must be renamed")
// errMissingDescription - a service is missing its description // errMissingDescription - a service is missing its description
const errMissingDescription = cerr("missing description") const errMissingDescription = cerr("missing description")
// errIllegalOptionalURIParam - an URI parameter cannot be optional // errIllegalOptionalURIParam - uri parameter cannot optional
const errIllegalOptionalURIParam = cerr("URI parameter cannot be optional") const errIllegalOptionalURIParam = cerr("uri parameter cannot be optional")
// errOptionalOption - an output is optional // errOptionalOption - cannot have optional output
const errOptionalOption = cerr("output cannot be optional") const errOptionalOption = cerr("output cannot be optional")
// errMissingParamDesc - a parameter is missing its description // errMissingParamDesc - missing parameter description
const errMissingParamDesc = cerr("missing parameter description") const errMissingParamDesc = cerr("missing parameter description")
// errUnknownDataType - a parameter has an unknown datatype name // errUnknownDataType - unknown parameter datatype
const errUnknownDataType = cerr("unknown data type") const errUnknownDataType = cerr("unknown parameter datatype")
// errIllegalParamName - a parameter has an illegal name // errIllegalParamName - illegal parameter name
const errIllegalParamName = cerr("illegal parameter name") const errIllegalParamName = cerr("illegal parameter name")
// errMissingParamType - a parameter has an illegal type // errMissingParamType - missing parameter type
const errMissingParamType = cerr("missing parameter type") const errMissingParamType = cerr("missing parameter type")
// errParamNameConflict - a parameter has a conflict with its name/rename field // errParamNameConflict - name/rename conflict
const errParamNameConflict = cerr("name conflict for parameter") const errParamNameConflict = cerr("parameter name conflict")

View File

@ -11,33 +11,29 @@ type Parameter struct {
Description string `json:"info"` Description string `json:"info"`
Type string `json:"type"` Type string `json:"type"`
Rename string `json:"name,omitempty"` Rename string `json:"name,omitempty"`
// ExtractType is the type of data the datatype returns Optional bool
// ExtractType is the type the Validator will cast into
ExtractType reflect.Type ExtractType reflect.Type
// Optional is set to true when the type is prefixed with '?' // Validator is inferred from the "type" property
Optional bool
// Validator is inferred from @Type
Validator datatype.Validator Validator datatype.Validator
} }
func (param *Parameter) validate(datatypes ...datatype.T) error { func (param *Parameter) validate(datatypes ...datatype.T) error {
// missing description
if len(param.Description) < 1 { if len(param.Description) < 1 {
return errMissingParamDesc return errMissingParamDesc
} }
// invalid type
if len(param.Type) < 1 || param.Type == "?" { if len(param.Type) < 1 || param.Type == "?" {
return errMissingParamType return errMissingParamType
} }
// optional type transform // optional type
if param.Type[0] == '?' { if param.Type[0] == '?' {
param.Optional = true param.Optional = true
param.Type = param.Type[1:] param.Type = param.Type[1:]
} }
// assign the datatype // find validator
for _, dtype := range datatypes { for _, dtype := range datatypes {
param.Validator = dtype.Build(param.Type, datatypes...) param.Validator = dtype.Build(param.Type, datatypes...)
param.ExtractType = dtype.Type() param.ExtractType = dtype.Type()
@ -48,6 +44,5 @@ func (param *Parameter) validate(datatypes ...datatype.T) error {
if param.Validator == nil { if param.Validator == nil {
return errUnknownDataType return errUnknownDataType
} }
return nil return nil
} }

View File

@ -22,15 +22,15 @@ type Service struct {
Input map[string]*Parameter `json:"in"` Input map[string]*Parameter `json:"in"`
Output map[string]*Parameter `json:"out"` Output map[string]*Parameter `json:"out"`
// references to url parameters // Captures contains references to URI parameters from the `Input` map. The format
// format: '/uri/{param}' // of these parameter names is "{paramName}"
Captures []*BraceCapture Captures []*BraceCapture
// references to Query parameters // Query contains references to HTTP Query parameters from the `Input` map.
// format: 'GET@paranName' // Query parameters names are "GET@paramName", this map contains escaped names (e.g. "paramName")
Query map[string]*Parameter Query map[string]*Parameter
// references for form parameters (all but Captures and Query) // Form references form parameters from the `Input` map (all but Captures and Query).
Form map[string]*Parameter Form map[string]*Parameter
} }
@ -43,16 +43,12 @@ type BraceCapture struct {
// Match returns if this service would handle this HTTP request // Match returns if this service would handle this HTTP request
func (svc *Service) Match(req *http.Request) bool { func (svc *Service) Match(req *http.Request) bool {
// method
if req.Method != svc.Method { if req.Method != svc.Method {
return false return false
} }
// check path
if !svc.matchPattern(req.RequestURI) { if !svc.matchPattern(req.RequestURI) {
return false return false
} }
return true return true
} }
@ -61,13 +57,12 @@ func (svc *Service) matchPattern(uri string) bool {
uriparts := SplitURL(uri) uriparts := SplitURL(uri)
parts := SplitURL(svc.Pattern) parts := SplitURL(svc.Pattern)
// fail if size differ
if len(uriparts) != len(parts) { if len(uriparts) != len(parts) {
return false return false
} }
// root url '/' // root url '/'
if len(parts) == 0 { if len(parts) == 0 && len(uriparts) == 0 {
return true return true
} }