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
}
// 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.
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)
}
if err := srv.validate(); err != nil {
err = srv.validate()
if err != nil {
return fmt.Errorf("%s: %w", errFormat, err)
}
return nil
}
@ -39,11 +40,9 @@ func (server Server) validate(datatypes ...datatype.T) error {
}
}
// check for collisions
if err := server.collide(); err != nil {
return fmt.Errorf("%s: %w", errFormat, err)
}
return nil
}
@ -58,7 +57,11 @@ func (server Server) Find(r *http.Request) *Service {
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 {
length := len(server.Services)
@ -68,103 +71,104 @@ func (server *Server) collide() error {
aService := server.Services[a]
bService := server.Services[b]
// ignore different method
if aService.Method != bService.Method {
continue
}
aParts := SplitURL(aService.Pattern)
bParts := SplitURL(bService.Pattern)
// not same size
if len(aParts) != len(bParts) {
aURIParts := SplitURL(aService.Pattern)
bURIParts := SplitURL(bService.Pattern)
if len(aURIParts) != len(bURIParts) {
continue
}
partErrors := make([]error, 0)
// for each part
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)
err := checkURICollision(aURIParts, bURIParts, aService.Input, bService.Input)
if err != nil {
return fmt.Errorf("(%s '%s') vs (%s '%s'): %w", aService.Method, aService.Pattern, bService.Method, bService.Pattern, err)
}
// 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
}
// 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
func SplitURL(url string) []string {
trimmed := strings.Trim(url, " /\t\r\n")

View File

@ -877,6 +877,36 @@ func TestMatchSimple(t *testing.T) {
"/a",
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",
@ -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)
}
// errRead - a problem ocurred when trying to read the configuration file
// errRead - read error
const errRead = cerr("cannot read config")
// errUnknownMethod - invalid http method
// errUnknownMethod - 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")
// errPatternCollision - there is a collision between 2 services' patterns (same method)
// errPatternCollision - collision between 2 services' patterns
const errPatternCollision = cerr("pattern collision")
// errInvalidPattern - a service pattern is malformed
const errInvalidPattern = cerr("must begin with a '/' and not end with")
// errInvalidPattern - malformed service pattern
const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with")
// errInvalidPatternBraceCapture - a service pattern brace capture is invalid
const errInvalidPatternBraceCapture = cerr("invalid uri capturing braces")
// errInvalidPatternBraceCapture - invalid brace capture
const errInvalidPatternBraceCapture = cerr("invalid uri parameter")
// errUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
const errUnspecifiedBraceCapture = cerr("capturing brace missing in the path")
// errUnspecifiedBraceCapture - missing path brace capture
const errUnspecifiedBraceCapture = cerr("missing uri parameter")
// errMandatoryRename - capture/query parameters must have a rename
const errMandatoryRename = cerr("capture and query parameters must have a 'name'")
// errUndefinedBraceCapture - missing capturing brace definition
const errUndefinedBraceCapture = cerr("missing uri parameter definition")
// errUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
const errUndefinedBraceCapture = cerr("capturing brace missing input definition")
// errMandatoryRename - capture/query parameters must be renamed
const errMandatoryRename = cerr("uri and query parameters must be renamed")
// errMissingDescription - a service is missing its description
const errMissingDescription = cerr("missing description")
// errIllegalOptionalURIParam - an URI parameter cannot be optional
const errIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
// errIllegalOptionalURIParam - uri parameter cannot 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")
// errMissingParamDesc - a parameter is missing its description
// errMissingParamDesc - missing parameter description
const errMissingParamDesc = cerr("missing parameter description")
// errUnknownDataType - a parameter has an unknown datatype name
const errUnknownDataType = cerr("unknown data type")
// errUnknownDataType - unknown parameter datatype
const errUnknownDataType = cerr("unknown parameter datatype")
// errIllegalParamName - a parameter has an illegal name
// errIllegalParamName - 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")
// errParamNameConflict - a parameter has a conflict with its name/rename field
const errParamNameConflict = cerr("name conflict for parameter")
// errParamNameConflict - name/rename conflict
const errParamNameConflict = cerr("parameter name conflict")

View File

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

View File

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