refactor: reduce cyclomatic complexity of service.validateInput()
- simplify matchPattern() - rename isMethodAvailable() to checkMethod() - rename isPatternValid() to checkPattern() - rename validateInput() to checkInput() - rename validateOutput() to checkOutput() - refactor per-type input param management in new method parseParam(); that returns the param type (added unexported enum) and the error - refactor collision detection from checkInput() and checkOutput() in new method nameConflicts()
This commit is contained in:
parent
140fbb8b23
commit
2b67655cfd
|
@ -9,9 +9,11 @@ import (
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
var (
|
||||||
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
captureRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
|
availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
)
|
||||||
|
|
||||||
// Service definition
|
// Service definition
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -43,19 +45,15 @@ 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 {
|
||||||
if req.Method != svc.Method {
|
return req.Method == svc.Method && svc.matchPattern(req.RequestURI)
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !svc.matchPattern(req.RequestURI) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if an uri matches the service's pattern
|
// checks if an uri matches the service's pattern
|
||||||
func (svc *Service) matchPattern(uri string) bool {
|
func (svc *Service) matchPattern(uri string) bool {
|
||||||
uriparts := SplitURL(uri)
|
var (
|
||||||
parts := SplitURL(svc.Pattern)
|
uriparts = SplitURL(uri)
|
||||||
|
parts = SplitURL(svc.Pattern)
|
||||||
|
)
|
||||||
|
|
||||||
if len(uriparts) != len(parts) {
|
if len(uriparts) != len(parts) {
|
||||||
return false
|
return false
|
||||||
|
@ -98,39 +96,34 @@ func (svc *Service) matchPattern(uri string) bool {
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Validate implements the validator interface
|
||||||
func (svc *Service) validate(datatypes ...validator.Type) error {
|
func (svc *Service) validate(datatypes ...validator.Type) error {
|
||||||
// check method
|
err := svc.checkMethod()
|
||||||
err := svc.isMethodAvailable()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'method': %w", err)
|
return fmt.Errorf("field 'method': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check pattern
|
|
||||||
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
|
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
|
||||||
err = svc.isPatternValid()
|
err = svc.checkPattern()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'path': %w", err)
|
return fmt.Errorf("field 'path': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check description
|
|
||||||
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
|
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
|
||||||
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
|
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check input parameters
|
err = svc.checkInput(datatypes)
|
||||||
err = svc.validateInput(datatypes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'in': %w", err)
|
return fmt.Errorf("field 'in': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if a brace capture remains undefined
|
// fail when a brace capture remains undefined
|
||||||
for _, capture := range svc.Captures {
|
for _, capture := range svc.Captures {
|
||||||
if capture.Ref == nil {
|
if capture.Ref == nil {
|
||||||
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
|
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check output
|
err = svc.checkOutput(datatypes)
|
||||||
err = svc.validateOutput(datatypes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'out': %w", err)
|
return fmt.Errorf("field 'out': %w", err)
|
||||||
}
|
}
|
||||||
|
@ -138,7 +131,7 @@ func (svc *Service) validate(datatypes ...validator.Type) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) isMethodAvailable() error {
|
func (svc *Service) checkMethod() error {
|
||||||
for _, available := range availableHTTPMethods {
|
for _, available := range availableHTTPMethods {
|
||||||
if svc.Method == available {
|
if svc.Method == available {
|
||||||
return nil
|
return nil
|
||||||
|
@ -147,7 +140,14 @@ func (svc *Service) isMethodAvailable() error {
|
||||||
return ErrUnknownMethod
|
return ErrUnknownMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) isPatternValid() error {
|
// checkPattern checks for the validity of the pattern definition (i.e. the uri)
|
||||||
|
//
|
||||||
|
// Note that the uri can contain capture params e.g. `/a/{b}/c/{d}`, in this
|
||||||
|
// example, input parameters with names `{b}` and `{d}` are expected.
|
||||||
|
//
|
||||||
|
// This methods sets up the service state with adding capture params that are
|
||||||
|
// expected; checkInputs() will be able to check params agains pattern captures.
|
||||||
|
func (svc *Service) checkPattern() error {
|
||||||
length := len(svc.Pattern)
|
length := len(svc.Pattern)
|
||||||
|
|
||||||
// empty pattern
|
// empty pattern
|
||||||
|
@ -170,7 +170,7 @@ func (svc *Service) isPatternValid() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if brace capture
|
// if brace capture
|
||||||
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
if matches := captureRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
braceName := matches[0][1]
|
braceName := matches[0][1]
|
||||||
|
|
||||||
// append
|
// append
|
||||||
|
@ -189,147 +189,183 @@ func (svc *Service) isPatternValid() error {
|
||||||
if strings.ContainsAny(part, "{}") {
|
if strings.ContainsAny(part, "{}") {
|
||||||
return ErrInvalidPatternBraceCapture
|
return ErrInvalidPatternBraceCapture
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) validateInput(types []validator.Type) error {
|
func (svc *Service) checkInput(types []validator.Type) error {
|
||||||
|
// no parameter
|
||||||
// ignore no parameter
|
|
||||||
if svc.Input == nil || len(svc.Input) < 1 {
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
svc.Input = make(map[string]*Parameter, 0)
|
svc.Input = map[string]*Parameter{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each parameter
|
// for each parameter
|
||||||
for paramName, param := range svc.Input {
|
for name, p := range svc.Input {
|
||||||
if len(paramName) < 1 {
|
if len(name) < 1 {
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if brace capture does not exists in pattern
|
// parse parameters: capture (uri), query or form and update the service
|
||||||
var iscapture, isquery bool
|
// attributes accordingly
|
||||||
if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
ptype, err := svc.parseParam(name, p)
|
||||||
braceName := matches[0][1]
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, capture := range svc.Captures {
|
|
||||||
if capture.Name == braceName {
|
|
||||||
capture.Ref = param
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture)
|
|
||||||
}
|
|
||||||
iscapture = true
|
|
||||||
|
|
||||||
} else if matches := queryRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
|
||||||
|
|
||||||
queryName := matches[0][1]
|
|
||||||
|
|
||||||
// init map
|
|
||||||
if svc.Query == nil {
|
|
||||||
svc.Query = make(map[string]*Parameter)
|
|
||||||
}
|
|
||||||
svc.Query[queryName] = param
|
|
||||||
isquery = true
|
|
||||||
} else {
|
|
||||||
if svc.Form == nil {
|
|
||||||
svc.Form = make(map[string]*Parameter)
|
|
||||||
}
|
|
||||||
svc.Form[paramName] = param
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail if capture or query without rename
|
|
||||||
if len(param.Rename) < 1 && (iscapture || isquery) {
|
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrMandatoryRename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use param name if no rename
|
|
||||||
if len(param.Rename) < 1 {
|
|
||||||
param.Rename = paramName
|
|
||||||
}
|
|
||||||
|
|
||||||
err := param.validate(types...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename mandatory for capture and query
|
||||||
|
if len(p.Rename) < 1 && (ptype == captureParam || ptype == queryParam) {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMandatoryRename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to name when Rename is not provided
|
||||||
|
if len(p.Rename) < 1 {
|
||||||
|
p.Rename = name
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.validate(types...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// capture parameter cannot be optional
|
// capture parameter cannot be optional
|
||||||
if iscapture && param.Optional {
|
if p.Optional && ptype == captureParam {
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
|
return fmt.Errorf("%s: %w", name, ErrIllegalOptionalURIParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail on name/rename conflict
|
err = nameConflicts(name, p, svc.Input)
|
||||||
for paramName2, param2 := range svc.Input {
|
if err != nil {
|
||||||
// ignore self
|
return err
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) validateOutput(types []validator.Type) error {
|
func (svc *Service) checkOutput(types []validator.Type) error {
|
||||||
|
// no parameter
|
||||||
// ignore no parameter
|
|
||||||
if svc.Output == nil || len(svc.Output) < 1 {
|
if svc.Output == nil || len(svc.Output) < 1 {
|
||||||
svc.Output = make(map[string]*Parameter, 0)
|
svc.Output = make(map[string]*Parameter, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each parameter
|
for name, p := range svc.Output {
|
||||||
for paramName, param := range svc.Output {
|
if len(name) < 1 {
|
||||||
if len(paramName) < 1 {
|
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// use param name if no rename
|
// fallback to name when Rename is not provided
|
||||||
if len(param.Rename) < 1 {
|
if len(p.Rename) < 1 {
|
||||||
param.Rename = paramName
|
p.Rename = name
|
||||||
}
|
}
|
||||||
|
|
||||||
err := param.validate(types...)
|
err := p.validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if param.Optional {
|
if p.Optional {
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption)
|
return fmt.Errorf("%s: %w", name, ErrOptionalOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail on name/rename conflict
|
err = nameConflicts(name, p, svc.Output)
|
||||||
for paramName2, param2 := range svc.Output {
|
if err != nil {
|
||||||
// ignore self
|
return err
|
||||||
if paramName == paramName2 {
|
}
|
||||||
continue
|
}
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
// 3.2.1. Same rename field
|
|
||||||
// 3.2.2. Not-renamed field matches a renamed field
|
type paramType int
|
||||||
// 3.2.3. Renamed field matches name
|
|
||||||
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
|
const (
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
|
captureParam paramType = iota
|
||||||
}
|
queryParam
|
||||||
|
formParam
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseParam determines which param type it is from its name:
|
||||||
|
// - `{paramName}` is an capture; it captures a segment of the uri defined in
|
||||||
|
// the pattern definition, e.g. `/some/path/with/{paramName}/somewhere`
|
||||||
|
// - `GET@paramName` is an uri query that is received from the http query format
|
||||||
|
// in the uri, e.g. `http://domain.com/uri?paramName=paramValue¶m2=value2`
|
||||||
|
// - any other name that contains valid characters is considered a Form
|
||||||
|
// parameter; it is extracted from the http request's body as: json, multipart
|
||||||
|
// or using the x-www-form-urlencoded format.
|
||||||
|
//
|
||||||
|
// Special notes:
|
||||||
|
// - capture params MUST be found in the pattern definition.
|
||||||
|
// - capture params MUST NOT be optional as they are in the pattern anyways.
|
||||||
|
// - capture and query params MUST be renamed because the `{param}` or
|
||||||
|
// `GET@param` name formats cannot be translated to a valid go exported name.
|
||||||
|
// c.f. the `dynfunc` package that creates a handler func() signature from
|
||||||
|
// the service definitions (i.e. input and output parameters).
|
||||||
|
func (svc *Service) parseParam(name string, p *Parameter) (paramType, error) {
|
||||||
|
var (
|
||||||
|
captureMatches = captureRegex.FindAllStringSubmatch(name, -1)
|
||||||
|
isCapture = len(captureMatches) > 0 && len(captureMatches[0]) > 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameter is a capture (uri/{param})
|
||||||
|
if isCapture {
|
||||||
|
captureName := captureMatches[0][1]
|
||||||
|
|
||||||
|
// fail if brace capture does not exists in pattern
|
||||||
|
found := false
|
||||||
|
for _, capture := range svc.Captures {
|
||||||
|
if capture.Name == captureName {
|
||||||
|
capture.Ref = p
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return captureParam, fmt.Errorf("%s: %w", name, ErrUnspecifiedBraceCapture)
|
||||||
|
}
|
||||||
|
return captureParam, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
queryMatches = queryRegex.FindAllStringSubmatch(name, -1)
|
||||||
|
isQuery = len(queryMatches) > 0 && len(queryMatches[0]) > 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameter is a query (uri?param)
|
||||||
|
if isQuery {
|
||||||
|
queryName := queryMatches[0][1]
|
||||||
|
|
||||||
|
// init map
|
||||||
|
if svc.Query == nil {
|
||||||
|
svc.Query = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Query[queryName] = p
|
||||||
|
|
||||||
|
return queryParam, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter is a form param
|
||||||
|
if svc.Form == nil {
|
||||||
|
svc.Form = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Form[name] = p
|
||||||
|
return formParam, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nameConflicts returns whether ar given parameter has its name or Rename field
|
||||||
|
// in conflict with an existing parameter
|
||||||
|
func nameConflicts(name string, param *Parameter, others map[string]*Parameter) error {
|
||||||
|
for otherName, other := range others {
|
||||||
|
// ignore self
|
||||||
|
if otherName == name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. same rename field
|
||||||
|
// 2. original name matches a renamed field
|
||||||
|
// 3. renamed field matches an original name
|
||||||
|
if param.Rename == other.Rename || name == other.Rename || otherName == param.Rename {
|
||||||
|
return fmt.Errorf("%s: %w", otherName, ErrParamNameConflict)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue