Compare commits

..

No commits in common. "32aff3e07fa597fb2e50efdf94749cf407f9d5ca" and "511070196b2ffbf14868190d5f0c3e1dc12f71ac" have entirely different histories.

8 changed files with 175 additions and 368 deletions

View File

@ -3,12 +3,8 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
"git.xdrm.io/go/aicra/config/datatype/builtin"
) )
func TestLegalServiceName(t *testing.T) { func TestLegalServiceName(t *testing.T) {
@ -223,22 +219,22 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
"path": "/", "path": "/",
"info": "valid-description", "info": "valid-description",
"in": { "in": {
"original": { "info": "valid-desc", "type": "any", "name": "" } "original": { "info": "valid-desc", "type": "valid-type", "name": "" }
} }
} }
]`) ]`)
srv, err := Parse(reader, builtin.AnyDataType{}) srv, err := Parse(reader)
if err != nil { if err != nil {
t.Errorf("unexpected error: '%s'", err) t.Errorf("unexpected error: '%s'", err)
t.FailNow() t.FailNow()
} }
if len(srv.services) < 1 { if len(srv) < 1 {
t.Errorf("expected a service") t.Errorf("expected a service")
t.FailNow() t.FailNow()
} }
for _, param := range srv.services[0].Input { for _, param := range (srv)[0].Input {
if param.Rename != "original" { if param.Rename != "original" {
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename) t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
t.FailNow() t.FailNow()
@ -253,24 +249,24 @@ func TestOptionalParam(t *testing.T) {
"path": "/", "path": "/",
"info": "valid-description", "info": "valid-description",
"in": { "in": {
"optional": { "info": "optional-type", "type": "?bool" }, "optional": { "info": "valid-desc", "type": "?optional-type" },
"required": { "info": "required-type", "type": "bool" }, "required": { "info": "valid-desc", "type": "required-type" },
"required2": { "info": "required", "type": "any" }, "required2": { "info": "valid-desc", "type": "a" },
"optional2": { "info": "optional", "type": "?any" } "optional2": { "info": "valid-desc", "type": "?a" }
} }
} }
]`) ]`)
srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{}) srv, err := Parse(reader)
if err != nil { if err != nil {
t.Errorf("unexpected error: '%s'", err) t.Errorf("unexpected error: '%s'", err)
t.FailNow() t.FailNow()
} }
if len(srv.services) < 1 { if len(srv) < 1 {
t.Errorf("expected a service") t.Errorf("expected a service")
t.FailNow() t.FailNow()
} }
for pName, param := range srv.services[0].Input { for pName, param := range (srv)[0].Input {
if pName == "optional" || pName == "optional2" { if pName == "optional" || pName == "optional2" {
if !param.Optional { if !param.Optional {
@ -393,7 +389,7 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "any" } "param1": { "info": "valid", "type": "a" }
} }
} }
]`, ]`,
@ -406,7 +402,7 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "?any" } "param1": { "info": "valid", "type": "?valid" }
} }
} }
]`, ]`,
@ -420,8 +416,8 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "any" }, "param1": { "info": "valid", "type": "valid" },
"param2": { "info": "valid", "type": "any", "name": "param1" } "param2": { "info": "valid", "type": "valid", "name": "param1" }
} }
} }
]`, ]`,
@ -435,8 +431,8 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "any", "name": "param2" }, "param1": { "info": "valid", "type": "valid", "name": "param2" },
"param2": { "info": "valid", "type": "any" } "param2": { "info": "valid", "type": "valid" }
} }
} }
]`, ]`,
@ -450,8 +446,8 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "any", "name": "conflict" }, "param1": { "info": "valid", "type": "valid", "name": "conflict" },
"param2": { "info": "valid", "type": "any", "name": "conflict" } "param2": { "info": "valid", "type": "valid", "name": "conflict" }
} }
} }
]`, ]`,
@ -466,8 +462,8 @@ func TestParseParameters(t *testing.T) {
"path": "/", "path": "/",
"info": "info", "info": "info",
"in": { "in": {
"param1": { "info": "valid", "type": "any", "name": "freename" }, "param1": { "info": "valid", "type": "valid", "name": "freename" },
"param2": { "info": "valid", "type": "any", "name": "freename2" } "param2": { "info": "valid", "type": "valid", "name": "freename2" }
} }
} }
]`, ]`,
@ -478,7 +474,7 @@ func TestParseParameters(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
_, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{}) _, err := Parse(strings.NewReader(test.Raw))
if err == nil && test.Error != nil { if err == nil && test.Error != nil {
t.Errorf("expected an error: '%s'", test.Error.Error()) t.Errorf("expected an error: '%s'", test.Error.Error())
@ -501,141 +497,136 @@ func TestParseParameters(t *testing.T) {
} }
// todo: rewrite with new api format // todo: rewrite with new api format
func TestMatchSimple(t *testing.T) { // func TestMatchSimple(t *testing.T) {
tests := []struct { // tests := []struct {
Config string // Raw string
URL string // Path []string
Match bool // BrowseDepth int
}{ // ValidDepth bool
{ // false positive -1 // }{
`[ { // { // false positive -1
"method": "GET", // `{
"path": "/a", // "/" : {
"info": "info", // "parent": {
"in": {} // "/": {
} ]`, // "subdir": {}
"/", // }
false, // }
}, // }
{ // false positive +1 // }`,
`[ { // []string{"parent", "subdir"},
"method": "GET", // 1,
"path": "/", // false,
"info": "info", // },
"in": {} // { // false positive +1
} ]`, // `{
"/a", // "/" : {
false, // "parent": {
}, // "/": {
{ // "subdir": {}
`[ { // }
"method": "GET", // }
"path": "/a", // }
"info": "info", // }`,
"in": {} // []string{"parent", "subdir"},
} ]`, // 3,
"/a", // false,
true, // },
},
{
`[ {
"method": "GET",
"path": "/a",
"info": "info",
"in": {}
} ]`,
"/a/",
true,
},
{
`[ {
"method": "GET",
"path": "/a/{id}",
"info": "info",
"in": {
"{id}": {
"info": "info",
"type": "bool"
}
}
} ]`,
"/a/12/",
false,
},
{
`[ {
"method": "GET",
"path": "/a/{id}",
"info": "info",
"in": {
"{id}": {
"info": "info",
"type": "int"
}
}
} ]`,
"/a/12/",
true,
},
{
`[ {
"method": "GET",
"path": "/a/{valid}",
"info": "info",
"in": {
"{id}": {
"info": "info",
"type": "bool"
}
}
} ]`,
"/a/12/",
false,
},
{
`[ {
"method": "GET",
"path": "/a/{valid}",
"info": "info",
"in": {
"{id}": {
"info": "info",
"type": "bool"
}
}
} ]`,
"/a/true/",
true,
},
}
for i, test := range tests { // {
// `{
// "/" : {
// "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,
// },
// }
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { // for i, test := range tests {
srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{})
if err != nil { // t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
t.Errorf("unexpected error: '%s'", err) // srv, err := Parse(strings.NewReader(test.Raw))
t.FailNow()
}
if len(srv.services) != 1 { // if err != nil {
t.Errorf("expected to have 1 service, got %d", len(srv.services)) // t.Errorf("unexpected error: '%s'", err)
t.FailNow() // t.FailNow()
} // }
req := httptest.NewRequest(http.MethodGet, test.URL, nil) // _, 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()
// }
match := srv.services[0].Match(req) // }
if test.Match && !match { // })
t.Errorf("expected '%s' to match", test.URL) // }
t.FailNow()
}
if !test.Match && match {
t.Errorf("expected '%s' NOT to match", test.URL)
t.FailNow()
}
})
}
} // }

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)
// DataType builds a DataType from the type definition (from the // Builder builds a DataType 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 DataType // definition does not match this DataType
type DataType interface { type Builder interface {
Build(typeDefinition string) Validator Build(typeDefinition string) Validator
} }

View File

@ -38,9 +38,6 @@ const ErrMissingDescription = Error("missing description")
// ErrMissingParamDesc - a parameter is missing its description // ErrMissingParamDesc - a parameter is missing its description
const ErrMissingParamDesc = Error("missing parameter description") const ErrMissingParamDesc = Error("missing parameter description")
// ErrUnknownDataType - a parameter has an unknown datatype name
const ErrUnknownDataType = Error("unknown data type")
// ErrIllegalParamName - a parameter has an illegal name // ErrIllegalParamName - a parameter has an illegal name
const ErrIllegalParamName = Error("parameter name must not begin/end with '_'") const ErrIllegalParamName = Error("parameter name must not begin/end with '_'")

View File

@ -1,15 +0,0 @@
package config
import "strings"
// splits an URL without empty sets
func splitURL(url string) []string {
trimmed := strings.Trim(url, " /\t\r\n")
split := strings.Split(trimmed, "/")
// remove empty set when empty url
if len(split) == 1 && len(split[0]) == 0 {
return []string{}
}
return split
}

View File

@ -1,7 +1,5 @@
package config package config
import "git.xdrm.io/go/aicra/config/datatype"
func (param *Parameter) checkAndFormat() error { func (param *Parameter) checkAndFormat() error {
// missing description // missing description
@ -22,14 +20,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.DataType) bool {
for _, dtype := range types {
param.Validator = dtype.Build(param.Type)
if param.Validator != nil {
return true
}
}
return false
}

View File

@ -4,26 +4,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"git.xdrm.io/go/aicra/config/datatype"
) )
// 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 return false
if req.Method != svc.Method {
return false
}
// check path
if !svc.matchPattern(req.RequestURI) {
return false
}
// check and extract input
// todo: check if input match
return true
} }
func (svc *Service) checkMethod() error { func (svc *Service) checkMethod() error {
@ -83,7 +68,7 @@ func (svc *Service) checkPattern() error {
return nil return nil
} }
func (svc *Service) checkAndFormatInput(types []datatype.DataType) error { func (svc *Service) checkAndFormatInput() error {
// ignore no parameter // ignore no parameter
if svc.Input == nil || len(svc.Input) < 1 { if svc.Input == nil || len(svc.Input) < 1 {
@ -109,10 +94,6 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
return fmt.Errorf("%s: %w", paramName, err) return fmt.Errorf("%s: %w", paramName, err)
} }
if !param.assignDataType(types) {
return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType)
}
// check for name/rename conflict // check for name/rename conflict
for paramName2, param2 := range svc.Input { for paramName2, param2 := range svc.Input {
// ignore self // ignore self
@ -133,48 +114,3 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) 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

@ -6,123 +6,38 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"git.xdrm.io/go/aicra/config/datatype"
) )
// Parse builds a server configuration from a json reader and checks for most format errors. // Parse builds a server configuration from a json reader and checks for most format errors.
// you can provide additional DataTypes as variadic arguments func Parse(r io.Reader) (Services, error) {
func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) { services := make(Services, 0)
server := &Server{
types: make([]datatype.DataType, 0),
services: make([]*Service, 0),
}
// add data types
for _, dtype := range dtypes {
server.types = append(server.types, dtype)
}
// parse JSON err := json.NewDecoder(r).Decode(&services)
if err := json.NewDecoder(r).Decode(&server.services); err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", ErrRead, err) return nil, fmt.Errorf("%s: %w", ErrRead, err)
} }
// check services err = services.checkAndFormat()
if err := server.checkAndFormat(); err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", ErrFormat, err) return nil, fmt.Errorf("%s: %w", ErrFormat, err)
} }
// check collisions if services.collide() {
if err := server.collide(); err != nil { return nil, fmt.Errorf("%s: %w", ErrFormat, ErrPatternCollision)
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
} }
return server, nil return services, nil
} }
// collide returns if there is collision between services // collide returns if there is collision between services
func (server *Server) collide() error { func (svc Services) collide() bool {
length := len(server.services) // todo: implement pattern collision using types to check if braces can be equal to fixed uri parts
return false
// for each service combination
for a := 0; a < length; a++ {
for b := a + 1; b < length; b++ {
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) {
continue
}
// 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 {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
// no capture -> check equal
if !aIsCapture && !bIsCapture {
if aPart == bPart {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
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 {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
// fail if not valid
if _, valid := input.Validator(aPart); !valid {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
// B captures A -> check type (A is B ?)
} else {
input, exists := bService.Input[bPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
// fail if not valid
if _, valid := input.Validator(bPart); !valid {
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
}
}
}
}
}
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 (svc Services) Find(r *http.Request) *Service {
for _, service := range server.services { for _, service := range svc {
if service.Match(r) { if service.Match(r) {
return service return service
} }
@ -132,8 +47,8 @@ func (server Server) Find(r *http.Request) *Service {
} }
// checkAndFormat checks for errors and missing fields and sets default values for optional fields. // checkAndFormat checks for errors and missing fields and sets default values for optional fields.
func (server Server) checkAndFormat() error { func (svc Services) checkAndFormat() error {
for _, service := range server.services { for _, service := range svc {
// check method // check method
err := service.checkMethod() err := service.checkMethod()
@ -154,7 +69,7 @@ func (server Server) checkAndFormat() error {
} }
// check input parameters // check input parameters
err = service.checkAndFormatInput(server.types) err = service.checkAndFormatInput()
if err != nil { if err != nil {
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err) return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
} }

View File

@ -13,11 +13,10 @@ 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"`
// Optional is set to true when the type is prefixed with '?' Optional bool
Optional bool
// Validator is inferred from @Type // validator is set from the @Type
Validator datatype.Validator validator datatype.Validator
} }
// Service represents a service definition (from api.json) // Service represents a service definition (from api.json)
@ -26,13 +25,10 @@ type Service struct {
Pattern string `json:"path"` Pattern string `json:"path"`
Scope [][]string `json:"scope"` Scope [][]string `json:"scope"`
Description string `json:"info"` Description string `json:"info"`
Download *bool `json:"download"`
Input map[string]*Parameter `json:"in"` Input map[string]*Parameter `json:"in"`
// Download *bool `json:"download"`
// Output map[string]*Parameter `json:"out"` // Output map[string]*Parameter `json:"out"`
} }
// Server represents a full server configuration // Services contains every service that represents a server configuration
type Server struct { type Services []*Service
types []datatype.DataType
services []*Service
}