Add validator interface to unify and for readability #11

Merged
xdrm-brackets merged 6 commits from refactor/config-validator into 0.3.0 2020-03-28 11:33:35 +00:00
12 changed files with 133 additions and 123 deletions

View File

@ -6,7 +6,7 @@ import "git.xdrm.io/go/aicra/datatype"
type AnyDataType struct{} type AnyDataType struct{}
// Build returns the validator // Build returns the validator
func (AnyDataType) Build(typeName string) datatype.Validator { func (AnyDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
// nothing if type not handled // nothing if type not handled
if typeName != "any" { if typeName != "any" {
return nil return nil

View File

@ -6,7 +6,7 @@ import "git.xdrm.io/go/aicra/datatype"
type BoolDataType struct{} type BoolDataType struct{}
// Build returns the validator // Build returns the validator
func (BoolDataType) Build(typeName string) datatype.Validator { func (BoolDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
// nothing if type not handled // nothing if type not handled
if typeName != "bool" { if typeName != "bool" {
return nil return nil

View File

@ -10,7 +10,7 @@ import (
type FloatDataType struct{} type FloatDataType struct{}
// Build returns the validator // Build returns the validator
func (FloatDataType) Build(typeName string) datatype.Validator { func (FloatDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
// nothing if type not handled // nothing if type not handled
if typeName != "float64" && typeName != "float" { if typeName != "float64" && typeName != "float" {
return nil return nil

View File

@ -11,7 +11,7 @@ import (
type IntDataType struct{} type IntDataType struct{}
// Build returns the validator // Build returns the validator
func (IntDataType) Build(typeName string) datatype.Validator { func (IntDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
// nothing if type not handled // nothing if type not handled
if typeName != "int" { if typeName != "int" {
return nil return nil

View File

@ -15,7 +15,7 @@ type StringDataType struct{}
// Build returns the validator. // Build returns the validator.
// availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`. // availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`.
func (s StringDataType) Build(typeName string) datatype.Validator { func (s StringDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
simple := typeName == "string" simple := typeName == "string"
fixedLengthMatches := fixedLengthRegex.FindStringSubmatch(typeName) fixedLengthMatches := fixedLengthRegex.FindStringSubmatch(typeName)
variableLengthMatches := variableLengthRegex.FindStringSubmatch(typeName) variableLengthMatches := variableLengthRegex.FindStringSubmatch(typeName)

View File

@ -11,7 +11,7 @@ import (
type UintDataType struct{} type UintDataType struct{}
// Build returns the validator // Build returns the validator
func (UintDataType) Build(typeName string) datatype.Validator { func (UintDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
// nothing if type not handled // nothing if type not handled
if typeName != "uint" { if typeName != "uint" {
return nil return nil

View File

@ -4,9 +4,9 @@ package datatype
// and casts the value into a compatible type // and casts the value into a compatible type
type Validator func(value interface{}) (cast interface{}, valid bool) type Validator func(value interface{}) (cast interface{}, valid bool)
// T builds a T from the type definition (from the // T builds a T from the type definition (from the configuration field "type") and returns NIL if the type
// configuration field "type") and returns NIL if the type // definition does not match this T ; the registry is passed for recursive datatypes (e.g. slices, structs, etc)
// definition does not match this T // to be able to access other datatypes
type T interface { type T interface {
Build(typeDefinition string) Validator Build(typeDefinition string, registry ...T) Validator
} }

View File

@ -2,8 +2,8 @@ package config
import "git.xdrm.io/go/aicra/datatype" import "git.xdrm.io/go/aicra/datatype"
func (param *Parameter) checkAndFormat() error { // Validate implements the validator interface
func (param *Parameter) Validate(datatypes ...datatype.T) error {
// missing description // missing description
if len(param.Description) < 1 { if len(param.Description) < 1 {
return ErrMissingParamDesc return ErrMissingParamDesc
@ -14,7 +14,7 @@ func (param *Parameter) checkAndFormat() error {
return ErrMissingParamType return ErrMissingParamType
} }
// set optional + type // optional type transform
if param.Type[0] == '?' { if param.Type[0] == '?' {
param.Optional = true param.Optional = true
param.Type = param.Type[1:] param.Type = param.Type[1:]
@ -22,14 +22,3 @@ func (param *Parameter) checkAndFormat() error {
return nil return nil
} }
// assigns the first matching data type from the type definition
func (param *Parameter) assignDataType(types []datatype.T) bool {
for _, dtype := range types {
param.Validator = dtype.Build(param.Type)
if param.Validator != nil {
return true
}
}
return false
}

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"git.xdrm.io/go/aicra/datatype" "git.xdrm.io/go/aicra/datatype"
) )
@ -17,29 +16,40 @@ func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
Types: make([]datatype.T, 0), Types: make([]datatype.T, 0),
Services: make([]*Service, 0), Services: make([]*Service, 0),
} }
// add data types // add data types
for _, dtype := range dtypes { for _, dtype := range dtypes {
server.Types = append(server.Types, dtype) server.Types = append(server.Types, dtype)
} }
// parse JSON
if err := json.NewDecoder(r).Decode(&server.Services); err != nil { if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
return nil, fmt.Errorf("%s: %w", ErrRead, err) return nil, fmt.Errorf("%s: %w", ErrRead, err)
} }
// check services if err := server.Validate(); err != nil {
if err := server.checkAndFormat(); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
}
// check collisions
if err := server.collide(); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFormat, err) return nil, fmt.Errorf("%s: %w", ErrFormat, err)
} }
return server, nil return server, nil
} }
// Validate implements the validator interface
func (server Server) Validate(datatypes ...datatype.T) error {
for _, service := range server.Services {
err := service.Validate(server.Types...)
if err != nil {
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
}
}
// check for collisions
if err := server.collide(); err != nil {
return fmt.Errorf("%s: %w", ErrFormat, err)
}
return nil
}
// Find a service matching an incoming HTTP request // Find a service matching an incoming HTTP request
func (server Server) Find(r *http.Request) *Service { func (server Server) Find(r *http.Request) *Service {
for _, service := range server.Services { for _, service := range server.Services {
@ -157,42 +167,3 @@ func (server *Server) collide() error {
return nil return nil
} }
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
func (server Server) checkAndFormat() error {
for _, service := range server.Services {
// 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(server.Types)
if err != nil {
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
}
// fail if a brace capture remains undefined
for _, capture := range service.Captures {
if capture.Ref == nil {
return fmt.Errorf("%s '%s' [in]: %s: %w", service.Method, service.Pattern, capture.Name, ErrUndefinedBraceCapture)
}
}
}
return nil
}

View File

@ -30,7 +30,88 @@ func (svc *Service) Match(req *http.Request) bool {
return true return true
} }
func (svc *Service) checkMethod() error { // checks if an uri matches the service's pattern
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 {
return true
}
// check part by part
for i, part := range parts {
uripart := uriparts[i]
isCapture := len(part) > 0 && part[0] == '{'
// if no capture -> check equality
if !isCapture {
if part != uripart {
return false
}
continue
}
param, exists := svc.Input[part]
// fail if no validator
if !exists || param.Validator == nil {
return false
}
// fail if not type-valid
if _, valid := param.Validator(uripart); !valid {
return false
}
}
return true
}
// Validate implements the validator interface
func (svc *Service) Validate(datatypes ...datatype.T) error {
// check method
err := svc.isMethodAvailable()
if err != nil {
return fmt.Errorf("field 'method': %w", err)
}
// check pattern
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
err = svc.isPatternValid()
if err != nil {
return fmt.Errorf("field 'path': %w", err)
}
// check description
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
}
// check input parameters
err = svc.validateInput(datatypes)
if err != nil {
return fmt.Errorf("field 'in': %w", err)
}
// fail if a brace capture remains undefined
for _, capture := range svc.Captures {
if capture.Ref == nil {
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
}
}
return nil
}
func (svc *Service) isMethodAvailable() error {
for _, available := range availableHTTPMethods { for _, available := range availableHTTPMethods {
if svc.Method == available { if svc.Method == available {
return nil return nil
@ -39,7 +120,7 @@ func (svc *Service) checkMethod() error {
return ErrUnknownMethod return ErrUnknownMethod
} }
func (svc *Service) checkPattern() error { func (svc *Service) isPatternValid() error {
length := len(svc.Pattern) length := len(svc.Pattern)
// empty pattern // empty pattern
@ -87,7 +168,7 @@ func (svc *Service) checkPattern() error {
return nil return nil
} }
func (svc *Service) checkAndFormatInput(types []datatype.T) error { func (svc *Service) validateInput(types []datatype.T) error {
// ignore no parameter // ignore no parameter
if svc.Input == nil || len(svc.Input) < 1 { if svc.Input == nil || len(svc.Input) < 1 {
@ -141,7 +222,7 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) error {
param.Rename = paramName param.Rename = paramName
} }
err := param.checkAndFormat() err := param.Validate()
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", paramName, err) return fmt.Errorf("%s: %w", paramName, err)
} }
@ -151,7 +232,16 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) error {
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam) return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
} }
if !param.assignDataType(types) { // assign the datatype
datatypeFound := false
for _, dtype := range types {
param.Validator = dtype.Build(param.Type, types...)
if param.Validator != nil {
datatypeFound = true
break
}
}
if !datatypeFound {
return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType) return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType)
} }
@ -175,48 +265,3 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) error {
return nil return nil
} }
// checks if an uri matches the service's pattern
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 {
return true
}
// check part by part
for i, part := range parts {
uripart := uriparts[i]
isCapture := len(part) > 0 && part[0] == '{'
// if no capture -> check equality
if !isCapture {
if part != uripart {
return false
}
continue
}
param, exists := svc.Input[part]
// fail if no validator
if !exists || param.Validator == nil {
return false
}
// fail if not type-valid
if _, valid := param.Validator(uripart); !valid {
return false
}
}
return true
}

View File

@ -8,6 +8,11 @@ import (
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} 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 // Server represents a full server configuration
type Server struct { type Server struct {
Types []datatype.T Types []datatype.T

View File

@ -64,7 +64,7 @@ func (s Server) ToHTTPServer() (*httpServer, error) {
for _, service := range s.config.Services { for _, service := range s.config.Services {
found := false found := false
for _, handler := range s.handlers { for _, handler := range s.handlers {
if handler.GetPath() == service.Pattern { if handler.GetMethod() == service.Method && handler.GetPath() == service.Pattern {
found = true found = true
break break
} }