Compare commits
No commits in common. "0.4.0" and "36991ea9ef774e4ac5dce168cd2ac38ecb241127" have entirely different histories.
0.4.0
...
36991ea9ef
10
README.md
10
README.md
|
@ -89,16 +89,16 @@ import (
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra"
|
"github.com/xdrm-io/aicra"
|
||||||
"github.com/xdrm-io/aicra/api"
|
"github.com/xdrm-io/aicra/api"
|
||||||
"github.com/xdrm-io/aicra/validator/builtin"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
builder := &aicra.Builder{}
|
builder := &aicra.Builder{}
|
||||||
|
|
||||||
// add custom type validators
|
// register data validators
|
||||||
builder.Validate(validator.BoolDataType{})
|
builder.AddType(builtin.BoolDataType{})
|
||||||
builder.Validate(validator.UintDataType{})
|
builder.AddType(builtin.UintDataType{})
|
||||||
builder.Validate(validator.StringDataType{})
|
builder.AddType(builtin.StringDataType{})
|
||||||
|
|
||||||
// load your configuration
|
// load your configuration
|
||||||
config, err := os.Open("api.json")
|
config, err := os.Open("api.json")
|
||||||
|
|
|
@ -17,12 +17,6 @@ func TestCombination(t *testing.T) {
|
||||||
Active: []string{},
|
Active: []string{},
|
||||||
Granted: true,
|
Granted: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "empty requirements none given",
|
|
||||||
Required: [][]string{{}},
|
|
||||||
Active: []string{},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "no requirement 1 given",
|
Name: "no requirement 1 given",
|
||||||
Required: [][]string{},
|
Required: [][]string{},
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
package api_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/ctx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContextGetRequest(t *testing.T) {
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "/random", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot create http request: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Request, req)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetRequest(c)
|
|
||||||
if fetched != req {
|
|
||||||
t.Fatalf("fetched http request %v ; expected %v", fetched, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestContextGetNilRequest(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetRequest(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched http request %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetResponseWriter(t *testing.T) {
|
|
||||||
res := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Response, res)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetResponseWriter(c)
|
|
||||||
if fetched != res {
|
|
||||||
t.Fatalf("fetched http response writer %v ; expected %v", fetched, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetNilResponseWriter(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetResponseWriter(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched http response writer %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetAuth(t *testing.T) {
|
|
||||||
auth := &api.Auth{}
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Auth, auth)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetAuth(c)
|
|
||||||
if fetched != auth {
|
|
||||||
t.Fatalf("fetched api auth %v ; expected %v", fetched, auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetNilAuth(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetAuth(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched api auth %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseData defines format for response parameters to return
|
||||||
|
type ResponseData map[string]interface{}
|
||||||
|
|
||||||
|
// Response represents an API response to be sent
|
||||||
|
type Response struct {
|
||||||
|
Data ResponseData
|
||||||
|
Status int
|
||||||
|
Headers http.Header
|
||||||
|
err Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse creates an empty response.
|
||||||
|
func EmptyResponse() *Response {
|
||||||
|
return &Response{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: make(ResponseData),
|
||||||
|
err: ErrFailure,
|
||||||
|
Headers: make(http.Header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithError sets the error
|
||||||
|
func (res *Response) WithError(err Err) *Response {
|
||||||
|
res.err = err
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response) Error() string {
|
||||||
|
return res.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData adds/overrides a new response field
|
||||||
|
func (res *Response) SetData(name string, value interface{}) {
|
||||||
|
res.Data[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the 'json.Marshaler' interface and is used
|
||||||
|
// to generate the JSON representation of the response
|
||||||
|
func (res *Response) MarshalJSON() ([]byte, error) {
|
||||||
|
fmt := make(map[string]interface{})
|
||||||
|
for k, v := range res.Data {
|
||||||
|
fmt[k] = v
|
||||||
|
}
|
||||||
|
fmt["error"] = res.err
|
||||||
|
return json.Marshal(fmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
w.WriteHeader(res.err.Status)
|
||||||
|
encoded, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Write(encoded)
|
||||||
|
return nil
|
||||||
|
}
|
24
builder.go
24
builder.go
|
@ -5,9 +5,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
"github.com/xdrm-io/aicra/internal/config"
|
||||||
"github.com/xdrm-io/aicra/internal/dynfunc"
|
"github.com/xdrm-io/aicra/internal/dynfunc"
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builder for an aicra server
|
// Builder for an aicra server
|
||||||
|
@ -20,7 +20,7 @@ type Builder struct {
|
||||||
middlewares []func(http.Handler) http.Handler
|
middlewares []func(http.Handler) http.Handler
|
||||||
// custom middlewares only wrapping the service handler of a request
|
// custom middlewares only wrapping the service handler of a request
|
||||||
// they will benefit from the request's context that contains service-specific
|
// they will benefit from the request's context that contains service-specific
|
||||||
// information (e.g. required permissions from the configuration)
|
// information (e.g. required permisisons from the configuration)
|
||||||
ctxMiddlewares []func(http.Handler) http.Handler
|
ctxMiddlewares []func(http.Handler) http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,18 +31,18 @@ type apiHandler struct {
|
||||||
dyn *dynfunc.Handler
|
dyn *dynfunc.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate adds an available Type to validate in the api definition
|
// AddType adds an available datatype to the api definition
|
||||||
func (b *Builder) Validate(t validator.Type) error {
|
func (b *Builder) AddType(t datatype.T) error {
|
||||||
if b.conf == nil {
|
if b.conf == nil {
|
||||||
b.conf = &config.Server{}
|
b.conf = &config.Server{}
|
||||||
}
|
}
|
||||||
if b.conf.Services != nil {
|
if b.conf.Services != nil {
|
||||||
return errLateType
|
return errLateType
|
||||||
}
|
}
|
||||||
if b.conf.Validators == nil {
|
if b.conf.Types == nil {
|
||||||
b.conf.Validators = make([]validator.Type, 0)
|
b.conf.Types = make([]datatype.T, 0)
|
||||||
}
|
}
|
||||||
b.conf.Validators = append(b.conf.Validators, t)
|
b.conf.Types = append(b.conf.Types, t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,9 @@ func (b *Builder) Validate(t validator.Type) error {
|
||||||
// the service associated with the request has not been found at this stage.
|
// the service associated with the request has not been found at this stage.
|
||||||
// This stage is perfect for logging or generic request management.
|
// This stage is perfect for logging or generic request management.
|
||||||
func (b *Builder) With(mw func(http.Handler) http.Handler) {
|
func (b *Builder) With(mw func(http.Handler) http.Handler) {
|
||||||
|
if b.conf == nil {
|
||||||
|
b.conf = &config.Server{}
|
||||||
|
}
|
||||||
if b.middlewares == nil {
|
if b.middlewares == nil {
|
||||||
b.middlewares = make([]func(http.Handler) http.Handler, 0)
|
b.middlewares = make([]func(http.Handler) http.Handler, 0)
|
||||||
}
|
}
|
||||||
|
@ -66,6 +69,9 @@ func (b *Builder) With(mw func(http.Handler) http.Handler) {
|
||||||
// data that can be access with api.GetRequest(), api.GetResponseWriter(),
|
// data that can be access with api.GetRequest(), api.GetResponseWriter(),
|
||||||
// api.GetAuth(), etc methods.
|
// api.GetAuth(), etc methods.
|
||||||
func (b *Builder) WithContext(mw func(http.Handler) http.Handler) {
|
func (b *Builder) WithContext(mw func(http.Handler) http.Handler) {
|
||||||
|
if b.conf == nil {
|
||||||
|
b.conf = &config.Server{}
|
||||||
|
}
|
||||||
if b.ctxMiddlewares == nil {
|
if b.ctxMiddlewares == nil {
|
||||||
b.ctxMiddlewares = make([]func(http.Handler) http.Handler, 0)
|
b.ctxMiddlewares = make([]func(http.Handler) http.Handler, 0)
|
||||||
}
|
}
|
||||||
|
@ -79,14 +85,14 @@ func (b *Builder) Setup(r io.Reader) error {
|
||||||
b.conf = &config.Server{}
|
b.conf = &config.Server{}
|
||||||
}
|
}
|
||||||
if b.conf.Services != nil {
|
if b.conf.Services != nil {
|
||||||
return errAlreadySetup
|
panic(errAlreadySetup)
|
||||||
}
|
}
|
||||||
return b.conf.Parse(r)
|
return b.conf.Parse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind a dynamic handler to a REST service (method and pattern)
|
// Bind a dynamic handler to a REST service (method and pattern)
|
||||||
func (b *Builder) Bind(method, path string, fn interface{}) error {
|
func (b *Builder) Bind(method, path string, fn interface{}) error {
|
||||||
if b.conf == nil || b.conf.Services == nil {
|
if b.conf.Services == nil {
|
||||||
return errNotSetup
|
return errNotSetup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
194
builder_test.go
194
builder_test.go
|
@ -8,37 +8,34 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
"github.com/xdrm-io/aicra/api"
|
||||||
"github.com/xdrm-io/aicra/internal/dynfunc"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func addBuiltinTypes(b *Builder) error {
|
func addBuiltinTypes(b *Builder) error {
|
||||||
if err := b.Validate(validator.AnyType{}); err != nil {
|
if err := b.AddType(builtin.AnyDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.BoolType{}); err != nil {
|
if err := b.AddType(builtin.BoolDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.FloatType{}); err != nil {
|
if err := b.AddType(builtin.FloatDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.IntType{}); err != nil {
|
if err := b.AddType(builtin.IntDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.StringType{}); err != nil {
|
if err := b.AddType(builtin.StringDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.UintType{}); err != nil {
|
if err := b.AddType(builtin.UintDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddType(t *testing.T) {
|
func TestAddType(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
builder := &Builder{}
|
||||||
err := builder.Validate(validator.BoolType{})
|
err := builder.AddType(builtin.BoolDataType{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -46,186 +43,13 @@ func TestAddType(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
err = builder.Validate(validator.FloatType{})
|
err = builder.AddType(builtin.FloatDataType{})
|
||||||
if err != errLateType {
|
if err != errLateType {
|
||||||
t.Fatalf("expected <%v> got <%v>", errLateType, err)
|
t.Fatalf("expected <%v> got <%v>", errLateType, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetupNoType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestSetupTwice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
// double Setup() must fail
|
|
||||||
err = builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != errAlreadySetup {
|
|
||||||
t.Fatalf("expected error %v, got %v", errAlreadySetup, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindBeforeSetup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
// binding before Setup() must fail
|
|
||||||
err := builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
if err != errNotSetup {
|
|
||||||
t.Fatalf("expected error %v, got %v", errNotSetup, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindUnknownService(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
if !errors.Is(err, errUnknownService) {
|
|
||||||
t.Fatalf("expected error %v, got %v", errUnknownService, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBindInvalidHandler(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected an error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !errors.Is(err, dynfunc.ErrMissingHandlerContextArgument) {
|
|
||||||
t.Fatalf("expected a dynfunc.Err got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBindGet(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = builder.Get("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Post("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Put("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Delete("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnhandledService(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = builder.Get("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = builder.Build()
|
|
||||||
if !errors.Is(err, errMissingHandler) {
|
|
||||||
t.Fatalf("expected a %v error, got %v", errMissingHandler, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBind(t *testing.T) {
|
func TestBind(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tcases := []struct {
|
tcases := []struct {
|
||||||
Name string
|
Name string
|
||||||
Config string
|
Config string
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnyDataType is what its name tells
|
||||||
|
type AnyDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (AnyDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(interface{}(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (AnyDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "any" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAny_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.AnyType{}
|
|
||||||
expected = reflect.TypeOf(interface{}(nil))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAny_AvailableTypes(t *testing.T) {
|
func TestAny_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.AnyType{}
|
dt := builtin.AnyDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,7 +26,7 @@ func TestAny_AvailableTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
|
@ -60,7 +47,7 @@ func TestAny_AlwaysTrue(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "any"
|
const typeName = "any"
|
||||||
|
|
||||||
validator := validator.AnyType{}.Validator(typeName)
|
validator := builtin.AnyDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,24 +1,23 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoolType makes the "bool" type available in the aicra configuration
|
// BoolDataType is what its name tells
|
||||||
// It considers valid:
|
type BoolDataType struct{}
|
||||||
// - booleans
|
|
||||||
// - strings containing "true" or "false"
|
|
||||||
// - []byte containing "true" or "false"
|
|
||||||
type BoolType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `bool` type
|
// Type returns the type of data
|
||||||
func (BoolType) GoType() reflect.Type {
|
func (BoolDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(true)
|
return reflect.TypeOf(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for bool values
|
// Build returns the validator
|
||||||
func (BoolType) Validator(typename string, avail ...Type) ValidateFunc {
|
func (BoolDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if typename != "bool" {
|
// nothing if type not handled
|
||||||
|
if typeName != "bool" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBool_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.BoolType{}
|
|
||||||
expected = reflect.TypeOf(true)
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBool_AvailableTypes(t *testing.T) {
|
func TestBool_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.BoolType{}
|
dt := builtin.BoolDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,7 +26,7 @@ func TestBool_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
|
@ -62,7 +49,7 @@ func TestBool_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "bool"
|
const typeName = "bool"
|
||||||
|
|
||||||
validator := validator.BoolType{}.Validator(typeName)
|
validator := builtin.BoolDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,27 +1,24 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FloatType makes the "float" (or "float64") type available in the aicra configuration
|
// FloatDataType is what its name tells
|
||||||
// It considers valid:
|
type FloatDataType struct{}
|
||||||
// - float64
|
|
||||||
// - int (since it does not overflow)
|
|
||||||
// - uint (since it does not overflow)
|
|
||||||
// - strings containing json-compatible floats
|
|
||||||
// - []byte containing json-compatible floats
|
|
||||||
type FloatType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `float64` type
|
// Type returns the type of data
|
||||||
func (FloatType) GoType() reflect.Type {
|
func (FloatDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(float64(0))
|
return reflect.TypeOf(float64(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for float64 values
|
// Build returns the validator
|
||||||
func (FloatType) Validator(typename string, avail ...Type) ValidateFunc {
|
func (FloatDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if typename != "float64" && typename != "float" {
|
// nothing if type not handled
|
||||||
|
if typeName != "float64" && typeName != "float" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return func(value interface{}) (interface{}, bool) {
|
return func(value interface{}) (interface{}, bool) {
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFloat64_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.FloatType{}
|
|
||||||
expected = reflect.TypeOf(float64(0.0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFloat64_AvailableTypes(t *testing.T) {
|
func TestFloat64_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.FloatType{}
|
dt := builtin.FloatDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -46,7 +33,7 @@ func TestFloat64_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
|
@ -69,7 +56,7 @@ func TestFloat64_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "float"
|
const typeName = "float"
|
||||||
|
|
||||||
validator := validator.FloatType{}.Validator(typeName)
|
validator := builtin.FloatDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,29 +1,25 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntType makes the "int" type available in the aicra configuration
|
// IntDataType is what its name tells
|
||||||
// It considers valid:
|
type IntDataType struct{}
|
||||||
// - int
|
|
||||||
// - float64 (since it does not overflow)
|
|
||||||
// - uint (since it does not overflow)
|
|
||||||
// - strings containing json-compatible integers
|
|
||||||
// - []byte containing json-compatible integers
|
|
||||||
type IntType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `int` type
|
// Type returns the type of data
|
||||||
func (IntType) GoType() reflect.Type {
|
func (IntDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(int(0))
|
return reflect.TypeOf(int(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for int values
|
// Build returns the validator
|
||||||
func (IntType) Validator(typename string, avail ...Type) ValidateFunc {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInt_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.IntType{}
|
|
||||||
expected = reflect.TypeOf(int(0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInt_AvailableTypes(t *testing.T) {
|
func TestInt_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.IntType{}
|
dt := builtin.IntDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,7 +27,7 @@ func TestInt_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
|
@ -63,7 +50,7 @@ func TestInt_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "int"
|
const typeName = "int"
|
||||||
|
|
||||||
validator := validator.IntType{}.Validator(typeName)
|
validator := builtin.IntDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -84,7 +71,7 @@ func TestInt_Values(t *testing.T) {
|
||||||
{uint(math.MaxInt64 + 1), false},
|
{uint(math.MaxInt64 + 1), false},
|
||||||
|
|
||||||
{float64(math.MinInt64), true},
|
{float64(math.MinInt64), true},
|
||||||
// we cannot just subtract 1 because of how precision works
|
// we cannot just substract 1 because of how precision works
|
||||||
{float64(math.MinInt64 - 1024 - 1), false},
|
{float64(math.MinInt64 - 1024 - 1), false},
|
||||||
|
|
||||||
// WARNING : this is due to how floats are compared
|
// WARNING : this is due to how floats are compared
|
|
@ -1,37 +1,32 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
||||||
fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
var variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
||||||
variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// StringType makes the types beloz available in the aicra configuration:
|
// StringDataType is what its name tells
|
||||||
// - "string" considers any string valid
|
type StringDataType struct{}
|
||||||
// - "string(n)" considers any string with an exact size of `n` valid
|
|
||||||
// - "string(a,b)" considers any string with a size between `a` and `b` valid
|
|
||||||
// > for the last one, `a` and `b` are included in the valid sizes
|
|
||||||
type StringType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `string` type
|
// Type returns the type of data
|
||||||
func (StringType) GoType() reflect.Type {
|
func (StringDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(string(""))
|
return reflect.TypeOf(string(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for strings with any/fixed/bound sizes
|
// Build returns the validator.
|
||||||
func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
// availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`.
|
||||||
var (
|
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)
|
||||||
)
|
|
||||||
|
|
||||||
// ignore unknown typename
|
// nothing if type not handled
|
||||||
if !simple && fixedLengthMatches == nil && variableLengthMatches == nil {
|
if !simple && fixedLengthMatches == nil && variableLengthMatches == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -45,7 +40,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
if fixedLengthMatches != nil {
|
if fixedLengthMatches != nil {
|
||||||
exLen, ok := s.getFixedLength(fixedLengthMatches)
|
exLen, ok := s.getFixedLength(fixedLengthMatches)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
mustFail = true
|
||||||
}
|
}
|
||||||
min = exLen
|
min = exLen
|
||||||
max = exLen
|
max = exLen
|
||||||
|
@ -54,7 +49,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
} else if variableLengthMatches != nil {
|
} else if variableLengthMatches != nil {
|
||||||
exMin, exMax, ok := s.getVariableLength(variableLengthMatches)
|
exMin, exMax, ok := s.getVariableLength(variableLengthMatches)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
mustFail = true
|
||||||
}
|
}
|
||||||
min = exMin
|
min = exMin
|
||||||
max = exMax
|
max = exMax
|
||||||
|
@ -89,7 +84,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFixedLength returns the fixed length from regex matches and a success state.
|
// getFixedLength returns the fixed length from regex matches and a success state.
|
||||||
func (StringType) getFixedLength(regexMatches []string) (int, bool) {
|
func (StringDataType) getFixedLength(regexMatches []string) (int, bool) {
|
||||||
// incoherence error
|
// incoherence error
|
||||||
if regexMatches == nil || len(regexMatches) < 2 {
|
if regexMatches == nil || len(regexMatches) < 2 {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
@ -105,7 +100,7 @@ func (StringType) getFixedLength(regexMatches []string) (int, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVariableLength returns the length min and max from regex matches and a success state.
|
// getVariableLength returns the length min and max from regex matches and a success state.
|
||||||
func (StringType) getVariableLength(regexMatches []string) (int, int, bool) {
|
func (StringDataType) getVariableLength(regexMatches []string) (int, int, bool) {
|
||||||
// incoherence error
|
// incoherence error
|
||||||
if regexMatches == nil || len(regexMatches) < 3 {
|
if regexMatches == nil || len(regexMatches) < 3 {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestString_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.StringType{}
|
|
||||||
expected = reflect.TypeOf(string("abc"))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString_AvailableTypes(t *testing.T) {
|
func TestString_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.StringType{}
|
dt := builtin.StringDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -66,7 +53,7 @@ func TestString_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
|
@ -88,7 +75,7 @@ func TestString_AnyLength(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "string"
|
const typeName = "string"
|
||||||
|
|
||||||
validator := validator.StringType{}.Validator(typeName)
|
validator := builtin.StringDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -146,7 +133,7 @@ func TestString_FixedLength(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
validator := validator.StringType{}.Validator(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -207,7 +194,7 @@ func TestString_VariableLength(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
validator := validator.StringType{}.Validator(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,28 +1,25 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UintType makes the "uint" type available in the aicra configuration
|
// UintDataType is what its name tells
|
||||||
// It considers valid:
|
type UintDataType struct{}
|
||||||
// - uint
|
|
||||||
// - int (since it does not overflow)
|
|
||||||
// - float64 (since it does not overflow)
|
|
||||||
// - strings containing json-compatible integers
|
|
||||||
// - []byte containing json-compatible integers
|
|
||||||
type UintType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `uint` type
|
// Type returns the type of data
|
||||||
func (UintType) GoType() reflect.Type {
|
func (UintDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(uint(0))
|
return reflect.TypeOf(uint(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for uint values
|
// Build returns the validator
|
||||||
func (UintType) Validator(other string, avail ...Type) ValidateFunc {
|
func (UintDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if other != "uint" {
|
// nothing if type not handled
|
||||||
|
if typeName != "uint" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUint_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.UintType{}
|
|
||||||
expected = reflect.TypeOf(uint(0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUint_AvailableTypes(t *testing.T) {
|
func TestUint_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.UintType{}
|
dt := builtin.UintDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,7 +27,7 @@ func TestUint_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
|
@ -63,7 +50,7 @@ func TestUint_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "uint"
|
const typeName = "uint"
|
||||||
|
|
||||||
validator := validator.UintType{}.Validator(typeName)
|
validator := builtin.UintDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -0,0 +1,23 @@
|
||||||
|
package datatype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator returns whether a given value fulfills the datatype
|
||||||
|
// and casts the value into a common go type.
|
||||||
|
//
|
||||||
|
// for example, if a validator checks for upper case strings,
|
||||||
|
// whether the value is a []byte, a string or a []rune, if the
|
||||||
|
// value matches the validator's checks, it will be cast it into
|
||||||
|
// a common go type, say, string.
|
||||||
|
type Validator func(value interface{}) (cast interface{}, valid bool)
|
||||||
|
|
||||||
|
// T represents a datatype. The Build function returns a Validator if
|
||||||
|
// it manages types with the name `typeDefinition` (from the configuration field "type"); else it or returns NIL if the type
|
||||||
|
// definition does not match this datatype; the registry is passed to allow recursive datatypes (e.g. slices, structs, etc)
|
||||||
|
// The datatype's validator (when input is valid) must return a cast's go type matching the `Type() reflect.Type`
|
||||||
|
type T interface {
|
||||||
|
Type() reflect.Type
|
||||||
|
Build(typeDefinition string, registry ...T) Validator
|
||||||
|
}
|
53
handler.go
53
handler.go
|
@ -2,7 +2,6 @@ package aicra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,25 +27,21 @@ func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
|
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
|
||||||
func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
|
func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
|
||||||
// match service from config
|
// 1ind a matching service from config
|
||||||
var service = s.conf.Find(r)
|
var service = s.conf.Find(r)
|
||||||
if service == nil {
|
if service == nil {
|
||||||
newResponse().WithError(api.ErrUnknownService).ServeHTTP(w, r)
|
handleError(api.ErrUnknownService, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract request data
|
// extract request data
|
||||||
var input, err = extractInput(service, *r)
|
var input, err = extractInput(service, *r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, reqdata.ErrInvalidType) {
|
handleError(api.ErrMissingParam, w, r)
|
||||||
newResponse().WithError(api.ErrInvalidParam).ServeHTTP(w, r)
|
|
||||||
} else {
|
|
||||||
newResponse().WithError(api.ErrMissingParam).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// match handler
|
// find a matching handler
|
||||||
var handler *apiHandler
|
var handler *apiHandler
|
||||||
for _, h := range s.handlers {
|
for _, h := range s.handlers {
|
||||||
if h.Method == service.Method && h.Path == service.Pattern {
|
if h.Method == service.Method && h.Path == service.Pattern {
|
||||||
|
@ -54,13 +49,13 @@ func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// no handler found
|
// fail on no matching handler
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
newResponse().WithError(api.ErrUncallableService).ServeHTTP(w, r)
|
handleError(api.ErrUncallableService, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// add info into context
|
// build context with builtin data
|
||||||
c := r.Context()
|
c := r.Context()
|
||||||
c = context.WithValue(c, ctx.Request, r)
|
c = context.WithValue(c, ctx.Request, r)
|
||||||
c = context.WithValue(c, ctx.Response, w)
|
c = context.WithValue(c, ctx.Response, w)
|
||||||
|
@ -68,55 +63,63 @@ func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// create http handler
|
// create http handler
|
||||||
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// should not happen
|
|
||||||
auth := api.GetAuth(r.Context())
|
auth := api.GetAuth(r.Context())
|
||||||
if auth == nil {
|
if auth == nil {
|
||||||
newResponse().WithError(api.ErrPermission).ServeHTTP(w, r)
|
handleError(api.ErrPermission, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject non granted requests
|
// reject non granted requests
|
||||||
if !auth.Granted() {
|
if !auth.Granted() {
|
||||||
newResponse().WithError(api.ErrPermission).ServeHTTP(w, r)
|
handleError(api.ErrPermission, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the service handler
|
// use context defined in the request
|
||||||
s.handle(r.Context(), input, handler, service, w, r)
|
s.handle(r.Context(), input, handler, service, w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// run contextual middlewares
|
// run middlewares the handler
|
||||||
for _, mw := range s.ctxMiddlewares {
|
for _, mw := range s.ctxMiddlewares {
|
||||||
h = mw(h)
|
h = mw(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve using the pre-filled context
|
// serve using the context with values
|
||||||
h.ServeHTTP(w, r.WithContext(c))
|
h.ServeHTTP(w, r.WithContext(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the service request with the associated handler func and respond using
|
|
||||||
// the handler func output
|
|
||||||
func (s *Handler) handle(c context.Context, input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
|
func (s *Handler) handle(c context.Context, input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
|
||||||
// pass execution to the handler function
|
// pass execution to the handler
|
||||||
var outData, outErr = handler.dyn.Handle(c, input.Data)
|
var outData, outErr = handler.dyn.Handle(c, input.Data)
|
||||||
|
|
||||||
// build response from output arguments
|
// build response from returned arguments
|
||||||
var res = newResponse().WithError(outErr)
|
var res = api.EmptyResponse().WithError(outErr)
|
||||||
for key, value := range outData {
|
for key, value := range outData {
|
||||||
|
|
||||||
// find original name from 'rename' field
|
// find original name from 'rename' field
|
||||||
for name, param := range service.Output {
|
for name, param := range service.Output {
|
||||||
if param.Rename == key {
|
if param.Rename == key {
|
||||||
res.WithValue(name, value)
|
res.SetData(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write response and close request
|
// 7. apply headers
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
for key, values := range res.Headers {
|
||||||
|
for _, value := range values {
|
||||||
|
w.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleError(err api.Err, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var response = api.EmptyResponse().WithError(err)
|
||||||
|
response.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func extractInput(service *config.Service, req http.Request) (*reqdata.T, error) {
|
func extractInput(service *config.Service, req http.Request) (*reqdata.T, error) {
|
||||||
var dataset = reqdata.New(service)
|
var dataset = reqdata.New(service)
|
||||||
|
|
||||||
|
|
559
handler_test.go
559
handler_test.go
|
@ -12,40 +12,34 @@ import (
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra"
|
"github.com/xdrm-io/aicra"
|
||||||
"github.com/xdrm-io/aicra/api"
|
"github.com/xdrm-io/aicra/api"
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printEscaped(raw string) string {
|
func addBuiltinTypes(b *aicra.Builder) error {
|
||||||
raw = strings.ReplaceAll(raw, "\n", "\\n")
|
if err := b.AddType(builtin.AnyDataType{}); err != nil {
|
||||||
raw = strings.ReplaceAll(raw, "\r", "\\r")
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func addDefaultTypes(b *aicra.Builder) error {
|
|
||||||
if err := b.Validate(validator.AnyType{}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.BoolType{}); err != nil {
|
if err := b.AddType(builtin.BoolDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.FloatType{}); err != nil {
|
if err := b.AddType(builtin.FloatDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.IntType{}); err != nil {
|
if err := b.AddType(builtin.IntDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.StringType{}); err != nil {
|
if err := b.AddType(builtin.StringDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.Validate(validator.UintType{}); err != nil {
|
if err := b.AddType(builtin.UintDataType{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_With(t *testing.T) {
|
func TestWith(t *testing.T) {
|
||||||
builder := &aicra.Builder{}
|
builder := &aicra.Builder{}
|
||||||
if err := addDefaultTypes(builder); err != nil {
|
if err := addBuiltinTypes(builder); err != nil {
|
||||||
t.Fatalf("unexpected error <%v>", err)
|
t.Fatalf("unexpected error <%v>", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +51,13 @@ func TestHandler_With(t *testing.T) {
|
||||||
|
|
||||||
middleware := func(next http.Handler) http.Handler {
|
middleware := func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
newr := r
|
||||||
|
|
||||||
// first time -> store 1
|
// first time -> store 1
|
||||||
value := r.Context().Value(key)
|
value := r.Context().Value(key)
|
||||||
if value == nil {
|
if value == nil {
|
||||||
r = r.WithContext(context.WithValue(r.Context(), key, int(1)))
|
newr = r.WithContext(context.WithValue(r.Context(), key, int(1)))
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, newr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,8 +67,8 @@ func TestHandler_With(t *testing.T) {
|
||||||
t.Fatalf("value is not an int")
|
t.Fatalf("value is not an int")
|
||||||
}
|
}
|
||||||
cast++
|
cast++
|
||||||
r = r.WithContext(context.WithValue(r.Context(), key, cast))
|
newr = r.WithContext(context.WithValue(r.Context(), key, cast))
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, newr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +123,7 @@ func TestHandler_With(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_WithAuth(t *testing.T) {
|
func TestWithAuth(t *testing.T) {
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -212,7 +208,7 @@ func TestHandler_WithAuth(t *testing.T) {
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
builder := &aicra.Builder{}
|
builder := &aicra.Builder{}
|
||||||
if err := addDefaultTypes(builder); err != nil {
|
if err := addBuiltinTypes(builder); err != nil {
|
||||||
t.Fatalf("unexpected error <%v>", err)
|
t.Fatalf("unexpected error <%v>", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +276,7 @@ func TestHandler_WithAuth(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_PermissionError(t *testing.T) {
|
func TestPermissionError(t *testing.T) {
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -305,7 +301,7 @@ func TestHandler_PermissionError(t *testing.T) {
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
builder := &aicra.Builder{}
|
builder := &aicra.Builder{}
|
||||||
if err := addDefaultTypes(builder); err != nil {
|
if err := addBuiltinTypes(builder); err != nil {
|
||||||
t.Fatalf("unexpected error <%v>", err)
|
t.Fatalf("unexpected error <%v>", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +367,7 @@ func TestHandler_PermissionError(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_DynamicScope(t *testing.T) {
|
func TestDynamicScope(t *testing.T) {
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
manifest string
|
manifest string
|
||||||
|
@ -562,7 +558,7 @@ func TestHandler_DynamicScope(t *testing.T) {
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
builder := &aicra.Builder{}
|
builder := &aicra.Builder{}
|
||||||
if err := addDefaultTypes(builder); err != nil {
|
if err := addBuiltinTypes(builder); err != nil {
|
||||||
t.Fatalf("unexpected error <%v>", err)
|
t.Fatalf("unexpected error <%v>", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,514 +621,3 @@ func TestHandler_DynamicScope(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_ServiceErrors(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
manifest string
|
|
||||||
// handler
|
|
||||||
hmethod, huri string
|
|
||||||
hfn interface{}
|
|
||||||
// request
|
|
||||||
method, url string
|
|
||||||
contentType string
|
|
||||||
body string
|
|
||||||
permissions []string
|
|
||||||
err api.Err
|
|
||||||
}{
|
|
||||||
// service match
|
|
||||||
{
|
|
||||||
name: "unknown service method",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrUnknownService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown service path",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/invalid",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrUnknownService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid empty service",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
|
|
||||||
// invalid uri param -> unknown service
|
|
||||||
{
|
|
||||||
name: "invalid uri param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a/{id}/b",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"{id}": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/a/{id}/b",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/a/invalid/b",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrUnknownService,
|
|
||||||
},
|
|
||||||
|
|
||||||
// query param
|
|
||||||
{
|
|
||||||
name: "missing query param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"GET@id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrMissingParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid query param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"GET@id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/a",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/a?id=abc",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid query multi param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"GET@id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/a",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/a?id=123&id=456",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid query param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"GET@id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodGet,
|
|
||||||
huri: "/a",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: "/a?id=123",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
|
|
||||||
// json param
|
|
||||||
{
|
|
||||||
name: "missing json param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/json",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrMissingParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid json param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/json",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `{ "id": "invalid type" }`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid json param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/json",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `{ "id": 123 }`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
|
|
||||||
// urlencoded param
|
|
||||||
{
|
|
||||||
name: "missing urlencoded param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrMissingParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid urlencoded param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `id=abc`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid urlencoded param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `id=123`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
|
|
||||||
// formdata param
|
|
||||||
{
|
|
||||||
name: "missing multipart param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "multipart/form-data; boundary=xxx",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: ``,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrMissingParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid multipart param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "multipart/form-data; boundary=xxx",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `--xxx
|
|
||||||
Content-Disposition: form-data; name="id"
|
|
||||||
|
|
||||||
abc
|
|
||||||
--xxx--`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid multipart param",
|
|
||||||
manifest: `[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"scope": [],
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "ID" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
hmethod: http.MethodPost,
|
|
||||||
huri: "/",
|
|
||||||
hfn: func(context.Context, struct{ ID int }) api.Err {
|
|
||||||
return api.ErrSuccess
|
|
||||||
},
|
|
||||||
contentType: "multipart/form-data; boundary=xxx",
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: "/",
|
|
||||||
body: `--xxx
|
|
||||||
Content-Disposition: form-data; name="id"
|
|
||||||
|
|
||||||
123
|
|
||||||
--xxx--`,
|
|
||||||
permissions: []string{},
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
builder := &aicra.Builder{}
|
|
||||||
if err := addDefaultTypes(builder); err != nil {
|
|
||||||
t.Fatalf("unexpected error <%v>", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := builder.Setup(strings.NewReader(tc.manifest))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("setup: unexpected error <%v>", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := builder.Bind(tc.hmethod, tc.huri, tc.hfn); err != nil {
|
|
||||||
t.Fatalf("bind: unexpected error <%v>", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler, err := builder.Build()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("build: unexpected error <%v>", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
response = httptest.NewRecorder()
|
|
||||||
body = strings.NewReader(tc.body)
|
|
||||||
request = httptest.NewRequest(tc.method, tc.url, body)
|
|
||||||
)
|
|
||||||
if len(tc.contentType) > 0 {
|
|
||||||
request.Header.Add("Content-Type", tc.contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test request
|
|
||||||
handler.ServeHTTP(response, request)
|
|
||||||
if response.Body == nil {
|
|
||||||
t.Fatalf("response has no body")
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonErr, err := json.Marshal(tc.err)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot marshal expected error: %v", err)
|
|
||||||
}
|
|
||||||
jsonExpected := fmt.Sprintf(`{"error":%s}`, jsonErr)
|
|
||||||
if response.Body.String() != jsonExpected {
|
|
||||||
t.Fatalf("invalid response:\n- actual: %s\n- expect: %s\n", printEscaped(response.Body.String()), printEscaped(jsonExpected))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,48 +7,48 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server definition
|
// Server definition
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Validators []validator.Type
|
Types []datatype.T
|
||||||
Services []*Service
|
Services []*Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a configuration 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 (s *Server) Parse(r io.Reader) error {
|
func (srv *Server) Parse(r io.Reader) error {
|
||||||
err := json.NewDecoder(r).Decode(&s.Services)
|
err := json.NewDecoder(r).Decode(&srv.Services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", ErrRead, err)
|
return fmt.Errorf("%s: %w", errRead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.validate()
|
err = srv.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", ErrFormat, err)
|
return fmt.Errorf("%s: %w", errFormat, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate implements the validator interface
|
// validate implements the validator interface
|
||||||
func (s Server) validate(datatypes ...validator.Type) error {
|
func (server Server) validate(datatypes ...datatype.T) error {
|
||||||
for _, service := range s.Services {
|
for _, service := range server.Services {
|
||||||
err := service.validate(s.Validators...)
|
err := service.validate(server.Types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
|
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a service matching an incoming HTTP request
|
// Find a service matching an incoming HTTP request
|
||||||
func (s Server) Find(r *http.Request) *Service {
|
func (server Server) Find(r *http.Request) *Service {
|
||||||
for _, service := range s.Services {
|
for _, service := range server.Services {
|
||||||
if matches := service.Match(r); matches {
|
if matches := service.Match(r); matches {
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
@ -62,14 +62,14 @@ func (s Server) Find(r *http.Request) *Service {
|
||||||
// - example 1: `/user/{id}` and `/user/articles` will not collide as {id} is an int and "articles" is not
|
// - 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 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
|
// - example 3: `/user/{name}` and `/user/{id}` will collide as {name} and {id} cannot be checked against their potential values
|
||||||
func (s *Server) collide() error {
|
func (server *Server) collide() error {
|
||||||
length := len(s.Services)
|
length := len(server.Services)
|
||||||
|
|
||||||
// for each service combination
|
// for each service combination
|
||||||
for a := 0; a < length; a++ {
|
for a := 0; a < length; a++ {
|
||||||
for b := a + 1; b < length; b++ {
|
for b := a + 1; b < length; b++ {
|
||||||
aService := s.Services[a]
|
aService := server.Services[a]
|
||||||
bService := s.Services[b]
|
bService := server.Services[b]
|
||||||
|
|
||||||
if aService.Method != bService.Method {
|
if aService.Method != bService.Method {
|
||||||
continue
|
continue
|
||||||
|
@ -105,14 +105,14 @@ func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter
|
||||||
|
|
||||||
// both captures -> as we cannot check, consider a collision
|
// both captures -> as we cannot check, consider a collision
|
||||||
if aIsCapture && bIsCapture {
|
if aIsCapture && bIsCapture {
|
||||||
errors = append(errors, fmt.Errorf("%w (path %s and %s)", ErrPatternCollision, aPart, bPart))
|
errors = append(errors, fmt.Errorf("%w (path %s and %s)", errPatternCollision, aPart, bPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// no capture -> check strict equality
|
// no capture -> check strict equality
|
||||||
if !aIsCapture && !bIsCapture {
|
if !aIsCapture && !bIsCapture {
|
||||||
if aPart == bPart {
|
if aPart == bPart {
|
||||||
errors = append(errors, fmt.Errorf("%w (same path '%s')", ErrPatternCollision, aPart))
|
errors = append(errors, fmt.Errorf("%w (same path '%s')", errPatternCollision, aPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,13 +123,13 @@ func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter
|
||||||
|
|
||||||
// fail if no type or no validator
|
// fail if no type or no validator
|
||||||
if !exists || input.Validator == nil {
|
if !exists || input.Validator == nil {
|
||||||
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", ErrPatternCollision, aPart))
|
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, aPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if not valid
|
// fail if not valid
|
||||||
if _, valid := input.Validator(bPart); valid {
|
if _, valid := input.Validator(bPart); valid {
|
||||||
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", ErrPatternCollision, aPart, bPart))
|
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, aPart, bPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,13 +139,13 @@ func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter
|
||||||
|
|
||||||
// fail if no type or no validator
|
// fail if no type or no validator
|
||||||
if !exists || input.Validator == nil {
|
if !exists || input.Validator == nil {
|
||||||
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", ErrPatternCollision, bPart))
|
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, bPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if not valid
|
// fail if not valid
|
||||||
if _, valid := input.Validator(aPart); valid {
|
if _, valid := input.Validator(aPart); valid {
|
||||||
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", ErrPatternCollision, bPart, aPart))
|
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, bPart, aPart))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLegalServiceName(t *testing.T) {
|
func TestLegalServiceName(t *testing.T) {
|
||||||
|
@ -21,15 +21,15 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
// empty
|
// empty
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "" } ]`,
|
||||||
ErrInvalidPattern,
|
errInvalidPattern,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`,
|
||||||
ErrInvalidPattern,
|
errInvalidPattern,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`,
|
||||||
ErrInvalidPattern,
|
errInvalidPattern,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/" } ]`,
|
||||||
|
@ -45,35 +45,35 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
||||||
ErrUndefinedBraceCapture,
|
errUndefinedBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
||||||
ErrUndefinedBraceCapture,
|
errUndefinedBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
errInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +143,8 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) {
|
if !test.ValidMethod && !errors.Is(err, errUnknownMethod) {
|
||||||
t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err)
|
t.Errorf("expected error <%s> got <%s>", errUnknownMethod, err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -184,7 +184,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
`[ { "method": "GET", "path": "/" }]`,
|
`[ { "method": "GET", "path": "/" }]`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{ // missing descriptiontype
|
{ // missing description
|
||||||
`[ { "method": "GET", "path": "/subservice" }]`,
|
`[ { "method": "GET", "path": "/subservice" }]`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
@ -217,8 +217,8 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) {
|
if !test.ValidDescription && !errors.Is(err, errMissingDescription) {
|
||||||
t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err)
|
t.Errorf("expected error <%s> got <%s>", errMissingDescription, err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -239,7 +239,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
err := srv.Parse(r)
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -275,8 +275,8 @@ func TestOptionalParam(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
err := srv.Parse(r)
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -321,7 +321,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
errMissingParamDesc,
|
||||||
},
|
},
|
||||||
{ // invalid param name suffix
|
{ // invalid param name suffix
|
||||||
`[
|
`[
|
||||||
|
@ -334,7 +334,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
errMissingParamDesc,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ // missing param description
|
{ // missing param description
|
||||||
|
@ -348,7 +348,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
errMissingParamDesc,
|
||||||
},
|
},
|
||||||
{ // empty param description
|
{ // empty param description
|
||||||
`[
|
`[
|
||||||
|
@ -361,7 +361,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
errMissingParamDesc,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ // missing param type
|
{ // missing param type
|
||||||
|
@ -375,7 +375,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamType,
|
errMissingParamType,
|
||||||
},
|
},
|
||||||
{ // empty param type
|
{ // empty param type
|
||||||
`[
|
`[
|
||||||
|
@ -388,7 +388,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamType,
|
errMissingParamType,
|
||||||
},
|
},
|
||||||
{ // invalid type (optional mark only)
|
{ // invalid type (optional mark only)
|
||||||
`[
|
`[
|
||||||
|
@ -402,7 +402,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
|
|
||||||
ErrMissingParamType,
|
errMissingParamType,
|
||||||
},
|
},
|
||||||
{ // valid description + valid type
|
{ // valid description + valid type
|
||||||
`[
|
`[
|
||||||
|
@ -444,7 +444,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
// 2 possible errors as map order is not deterministic
|
// 2 possible errors as map order is not deterministic
|
||||||
ErrParamNameConflict,
|
errParamNameConflict,
|
||||||
},
|
},
|
||||||
{ // rename conflict with name
|
{ // rename conflict with name
|
||||||
`[
|
`[
|
||||||
|
@ -459,7 +459,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
// 2 possible errors as map order is not deterministic
|
// 2 possible errors as map order is not deterministic
|
||||||
ErrParamNameConflict,
|
errParamNameConflict,
|
||||||
},
|
},
|
||||||
{ // rename conflict with rename
|
{ // rename conflict with rename
|
||||||
`[
|
`[
|
||||||
|
@ -474,7 +474,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
// 2 possible errors as map order is not deterministic
|
// 2 possible errors as map order is not deterministic
|
||||||
ErrParamNameConflict,
|
errParamNameConflict,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ // both renamed with no conflict
|
{ // both renamed with no conflict
|
||||||
|
@ -503,7 +503,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMandatoryRename,
|
errMandatoryRename,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -516,7 +516,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMandatoryRename,
|
errMandatoryRename,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -556,7 +556,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrIllegalOptionalURIParam,
|
errIllegalOptionalURIParam,
|
||||||
},
|
},
|
||||||
{ // URI parameter not specified
|
{ // URI parameter not specified
|
||||||
`[
|
`[
|
||||||
|
@ -569,7 +569,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrUnspecifiedBraceCapture,
|
errUnspecifiedBraceCapture,
|
||||||
},
|
},
|
||||||
{ // URI parameter not defined
|
{ // URI parameter not defined
|
||||||
`[
|
`[
|
||||||
|
@ -580,7 +580,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
"in": { }
|
"in": { }
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrUndefinedBraceCapture,
|
errUndefinedBraceCapture,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,7 +588,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
err := srv.Parse(strings.NewReader(test.Raw))
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
|
@ -637,7 +637,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
"info": "info", "in": {}
|
"info": "info", "in": {}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -672,7 +672,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -698,7 +698,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -711,7 +711,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -750,7 +750,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -789,7 +789,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -804,7 +804,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrPatternCollision,
|
errPatternCollision,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[
|
`[
|
||||||
|
@ -827,8 +827,8 @@ func TestServiceCollision(t *testing.T) {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.StringType{})
|
srv.Types = append(srv.Types, builtin.StringDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.UintType{})
|
srv.Types = append(srv.Types, builtin.UintDataType{})
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
|
@ -997,9 +997,9 @@ func TestMatchSimple(t *testing.T) {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.IntType{})
|
srv.Types = append(srv.Types, builtin.IntDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1081,9 +1081,9 @@ func TestFindPriority(t *testing.T) {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
srv := &Server{}
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.IntType{})
|
srv.Types = append(srv.Types, builtin.IntDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,61 +1,59 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Err allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type Err string
|
type cerr string
|
||||||
|
|
||||||
func (err Err) Error() string {
|
func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// errRead - read error
|
||||||
// ErrRead - read error
|
const errRead = cerr("cannot read config")
|
||||||
ErrRead = Err("cannot read config")
|
|
||||||
|
|
||||||
// ErrUnknownMethod - unknown http method
|
// errUnknownMethod - unknown http method
|
||||||
ErrUnknownMethod = Err("unknown HTTP method")
|
const errUnknownMethod = cerr("unknown HTTP method")
|
||||||
|
|
||||||
// ErrFormat - invalid format
|
// errFormat - invalid format
|
||||||
ErrFormat = Err("invalid config format")
|
const errFormat = cerr("invalid config format")
|
||||||
|
|
||||||
// ErrPatternCollision - collision between 2 services' patterns
|
// errPatternCollision - collision between 2 services' patterns
|
||||||
ErrPatternCollision = Err("pattern collision")
|
const errPatternCollision = cerr("pattern collision")
|
||||||
|
|
||||||
// ErrInvalidPattern - malformed service pattern
|
// errInvalidPattern - malformed service pattern
|
||||||
ErrInvalidPattern = Err("malformed service path: must begin with a '/' and not end with")
|
const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with")
|
||||||
|
|
||||||
// ErrInvalidPatternBraceCapture - invalid brace capture
|
// errInvalidPatternBraceCapture - invalid brace capture
|
||||||
ErrInvalidPatternBraceCapture = Err("invalid uri parameter")
|
const errInvalidPatternBraceCapture = cerr("invalid uri parameter")
|
||||||
|
|
||||||
// ErrUnspecifiedBraceCapture - missing path brace capture
|
// errUnspecifiedBraceCapture - missing path brace capture
|
||||||
ErrUnspecifiedBraceCapture = Err("missing uri parameter")
|
const errUnspecifiedBraceCapture = cerr("missing uri parameter")
|
||||||
|
|
||||||
// ErrUndefinedBraceCapture - missing capturing brace definition
|
// errUndefinedBraceCapture - missing capturing brace definition
|
||||||
ErrUndefinedBraceCapture = Err("missing uri parameter definition")
|
const errUndefinedBraceCapture = cerr("missing uri parameter definition")
|
||||||
|
|
||||||
// ErrMandatoryRename - capture/query parameters must be renamed
|
// errMandatoryRename - capture/query parameters must be renamed
|
||||||
ErrMandatoryRename = Err("uri and query parameters must be renamed")
|
const errMandatoryRename = cerr("uri and query parameters must be renamed")
|
||||||
|
|
||||||
// ErrMissingDescription - a service is missing its description
|
// errMissingDescription - a service is missing its description
|
||||||
ErrMissingDescription = Err("missing description")
|
const errMissingDescription = cerr("missing description")
|
||||||
|
|
||||||
// ErrIllegalOptionalURIParam - uri parameter cannot optional
|
// errIllegalOptionalURIParam - uri parameter cannot optional
|
||||||
ErrIllegalOptionalURIParam = Err("uri parameter cannot be optional")
|
const errIllegalOptionalURIParam = cerr("uri parameter cannot be optional")
|
||||||
|
|
||||||
// ErrOptionalOption - cannot have optional output
|
// errOptionalOption - cannot have optional output
|
||||||
ErrOptionalOption = Err("output cannot be optional")
|
const errOptionalOption = cerr("output cannot be optional")
|
||||||
|
|
||||||
// ErrMissingParamDesc - missing parameter description
|
// errMissingParamDesc - missing parameter description
|
||||||
ErrMissingParamDesc = Err("missing parameter description")
|
const errMissingParamDesc = cerr("missing parameter description")
|
||||||
|
|
||||||
// ErrUnknownParamType - unknown parameter type
|
// errUnknownDataType - unknown parameter datatype
|
||||||
ErrUnknownParamType = Err("unknown parameter datatype")
|
const errUnknownDataType = cerr("unknown parameter datatype")
|
||||||
|
|
||||||
// ErrIllegalParamName - illegal parameter name
|
// errIllegalParamName - illegal parameter name
|
||||||
ErrIllegalParamName = Err("illegal parameter name")
|
const errIllegalParamName = cerr("illegal parameter name")
|
||||||
|
|
||||||
// ErrMissingParamType - missing parameter type
|
// errMissingParamType - missing parameter type
|
||||||
ErrMissingParamType = Err("missing parameter type")
|
const errMissingParamType = cerr("missing parameter type")
|
||||||
|
|
||||||
// ErrParamNameConflict - name/rename conflict
|
// errParamNameConflict - name/rename conflict
|
||||||
ErrParamNameConflict = Err("parameter name conflict")
|
const errParamNameConflict = cerr("parameter name conflict")
|
||||||
)
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parameter represents a parameter definition (from api.json)
|
// Parameter represents a parameter definition (from api.json)
|
||||||
|
@ -12,19 +12,19 @@ type Parameter struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Rename string `json:"name,omitempty"`
|
Rename string `json:"name,omitempty"`
|
||||||
Optional bool
|
Optional bool
|
||||||
// GoType is the type the Validator will cast into
|
// ExtractType is the type the Validator will cast into
|
||||||
GoType reflect.Type
|
ExtractType reflect.Type
|
||||||
// Validator is inferred from the "type" property
|
// Validator is inferred from the "type" property
|
||||||
Validator validator.ValidateFunc
|
Validator datatype.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (param *Parameter) validate(datatypes ...validator.Type) error {
|
func (param *Parameter) validate(datatypes ...datatype.T) error {
|
||||||
if len(param.Description) < 1 {
|
if len(param.Description) < 1 {
|
||||||
return ErrMissingParamDesc
|
return errMissingParamDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(param.Type) < 1 || param.Type == "?" {
|
if len(param.Type) < 1 || param.Type == "?" {
|
||||||
return ErrMissingParamType
|
return errMissingParamType
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional type
|
// optional type
|
||||||
|
@ -35,14 +35,14 @@ func (param *Parameter) validate(datatypes ...validator.Type) error {
|
||||||
|
|
||||||
// find validator
|
// find validator
|
||||||
for _, dtype := range datatypes {
|
for _, dtype := range datatypes {
|
||||||
param.Validator = dtype.Validator(param.Type, datatypes...)
|
param.Validator = dtype.Build(param.Type, datatypes...)
|
||||||
param.GoType = dtype.GoType()
|
param.ExtractType = dtype.Type()
|
||||||
if param.Validator != nil {
|
if param.Validator != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if param.Validator == nil {
|
if param.Validator == nil {
|
||||||
return ErrUnknownParamType
|
return errUnknownDataType
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,12 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"github.com/xdrm-io/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
captureRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service definition
|
// Service definition
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -45,25 +43,19 @@ 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 {
|
||||||
var (
|
if req.Method != svc.Method {
|
||||||
uri = req.RequestURI
|
return false
|
||||||
queryIndex = strings.IndexByte(uri, '?')
|
|
||||||
)
|
|
||||||
|
|
||||||
// remove query part for matching the pattern
|
|
||||||
if queryIndex > -1 {
|
|
||||||
uri = uri[:queryIndex]
|
|
||||||
}
|
}
|
||||||
|
if !svc.matchPattern(req.RequestURI) {
|
||||||
return req.Method == svc.Method && svc.matchPattern(uri)
|
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 {
|
||||||
var (
|
uriparts := SplitURL(uri)
|
||||||
uriparts = SplitURL(uri)
|
parts := SplitURL(svc.Pattern)
|
||||||
parts = SplitURL(svc.Pattern)
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(uriparts) != len(parts) {
|
if len(uriparts) != len(parts) {
|
||||||
return false
|
return false
|
||||||
|
@ -105,35 +97,40 @@ 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 ...datatype.T) error {
|
||||||
err := svc.checkMethod()
|
// check method
|
||||||
|
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.checkPattern()
|
err = svc.isPatternValid()
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.checkInput(datatypes)
|
// check input parameters
|
||||||
|
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 when a brace capture remains undefined
|
// fail if 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.checkOutput(datatypes)
|
// check output
|
||||||
|
err = svc.validateOutput(datatypes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'out': %w", err)
|
return fmt.Errorf("field 'out': %w", err)
|
||||||
}
|
}
|
||||||
|
@ -141,34 +138,27 @@ func (svc *Service) validate(datatypes ...validator.Type) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkMethod() error {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ErrUnknownMethod
|
return errUnknownMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPattern checks for the validity of the pattern definition (i.e. the uri)
|
func (svc *Service) isPatternValid() error {
|
||||||
//
|
|
||||||
// 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
|
||||||
if length < 1 {
|
if length < 1 {
|
||||||
return ErrInvalidPattern
|
return errInvalidPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
if length > 1 {
|
if length > 1 {
|
||||||
// pattern not starting with '/' or ending with '/'
|
// pattern not starting with '/' or ending with '/'
|
||||||
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' {
|
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' {
|
||||||
return ErrInvalidPattern
|
return errInvalidPattern
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,11 +166,11 @@ func (svc *Service) checkPattern() error {
|
||||||
parts := SplitURL(svc.Pattern)
|
parts := SplitURL(svc.Pattern)
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if len(part) < 1 {
|
if len(part) < 1 {
|
||||||
return ErrInvalidPattern
|
return errInvalidPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
// if brace capture
|
// if brace capture
|
||||||
if matches := captureRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
braceName := matches[0][1]
|
braceName := matches[0][1]
|
||||||
|
|
||||||
// append
|
// append
|
||||||
|
@ -197,185 +187,149 @@ func (svc *Service) checkPattern() error {
|
||||||
|
|
||||||
// fail on invalid format
|
// fail on invalid format
|
||||||
if strings.ContainsAny(part, "{}") {
|
if strings.ContainsAny(part, "{}") {
|
||||||
return ErrInvalidPatternBraceCapture
|
return errInvalidPatternBraceCapture
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkInput(types []validator.Type) error {
|
func (svc *Service) validateInput(types []datatype.T) error {
|
||||||
// no parameter
|
|
||||||
|
// ignore no parameter
|
||||||
if svc.Input == nil || len(svc.Input) < 1 {
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
svc.Input = map[string]*Parameter{}
|
svc.Input = make(map[string]*Parameter, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each parameter
|
// for each parameter
|
||||||
for name, p := range svc.Input {
|
for paramName, param := range svc.Input {
|
||||||
if len(name) < 1 {
|
if len(paramName) < 1 {
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
return fmt.Errorf("%s: %w", paramName, errIllegalParamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse parameters: capture (uri), query or form and update the service
|
|
||||||
// attributes accordingly
|
|
||||||
ptype, err := svc.parseParam(name, p)
|
|
||||||
if err != nil {
|
|
||||||
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
|
|
||||||
if p.Optional && ptype == captureParam {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalOptionalURIParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nameConflicts(name, p, svc.Input)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *Service) checkOutput(types []validator.Type) error {
|
|
||||||
// no parameter
|
|
||||||
if svc.Output == nil || len(svc.Output) < 1 {
|
|
||||||
svc.Output = make(map[string]*Parameter, 0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, p := range svc.Output {
|
|
||||||
if len(name) < 1 {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Optional {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrOptionalOption)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nameConflicts(name, p, svc.Output)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type paramType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
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
|
// fail if brace capture does not exists in pattern
|
||||||
|
var iscapture, isquery bool
|
||||||
|
if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
braceName := matches[0][1]
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, capture := range svc.Captures {
|
for _, capture := range svc.Captures {
|
||||||
if capture.Name == captureName {
|
if capture.Name == braceName {
|
||||||
capture.Ref = p
|
capture.Ref = param
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return captureParam, fmt.Errorf("%s: %w", name, ErrUnspecifiedBraceCapture)
|
return fmt.Errorf("%s: %w", paramName, errUnspecifiedBraceCapture)
|
||||||
}
|
|
||||||
return captureParam, nil
|
|
||||||
}
|
}
|
||||||
|
iscapture = true
|
||||||
|
|
||||||
var (
|
} else if matches := queryRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
queryMatches = queryRegex.FindAllStringSubmatch(name, -1)
|
|
||||||
isQuery = len(queryMatches) > 0 && len(queryMatches[0]) > 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parameter is a query (uri?param)
|
queryName := matches[0][1]
|
||||||
if isQuery {
|
|
||||||
queryName := queryMatches[0][1]
|
|
||||||
|
|
||||||
// init map
|
// init map
|
||||||
if svc.Query == nil {
|
if svc.Query == nil {
|
||||||
svc.Query = make(map[string]*Parameter)
|
svc.Query = make(map[string]*Parameter)
|
||||||
}
|
}
|
||||||
svc.Query[queryName] = p
|
svc.Query[queryName] = param
|
||||||
|
isquery = true
|
||||||
return queryParam, nil
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter is a form param
|
|
||||||
if svc.Form == nil {
|
if svc.Form == nil {
|
||||||
svc.Form = make(map[string]*Parameter)
|
svc.Form = make(map[string]*Parameter)
|
||||||
}
|
}
|
||||||
svc.Form[name] = p
|
svc.Form[paramName] = param
|
||||||
return formParam, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nameConflicts returns whether ar given parameter has its name or Rename field
|
// fail if capture or query without rename
|
||||||
// in conflict with an existing parameter
|
if len(param.Rename) < 1 && (iscapture || isquery) {
|
||||||
func nameConflicts(name string, param *Parameter, others map[string]*Parameter) error {
|
return fmt.Errorf("%s: %w", paramName, errMandatoryRename)
|
||||||
for otherName, other := range others {
|
}
|
||||||
|
|
||||||
|
// use param name if no rename
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
param.Rename = paramName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := param.validate(types...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture parameter cannot be optional
|
||||||
|
if iscapture && param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, errIllegalOptionalURIParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on name/rename conflict
|
||||||
|
for paramName2, param2 := range svc.Input {
|
||||||
// ignore self
|
// ignore self
|
||||||
if otherName == name {
|
if paramName == paramName2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. same rename field
|
// 3.2.1. Same rename field
|
||||||
// 2. original name matches a renamed field
|
// 3.2.2. Not-renamed field matches a renamed field
|
||||||
// 3. renamed field matches an original name
|
// 3.2.3. Renamed field matches name
|
||||||
if param.Rename == other.Rename || name == other.Rename || otherName == param.Rename {
|
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
|
||||||
return fmt.Errorf("%s: %w", otherName, ErrParamNameConflict)
|
return fmt.Errorf("%s: %w", paramName, errParamNameConflict)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) validateOutput(types []datatype.T) error {
|
||||||
|
|
||||||
|
// ignore no parameter
|
||||||
|
if svc.Output == nil || len(svc.Output) < 1 {
|
||||||
|
svc.Output = make(map[string]*Parameter, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each parameter
|
||||||
|
for paramName, param := range svc.Output {
|
||||||
|
if len(paramName) < 1 {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, errIllegalParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use param name if no rename
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
param.Rename = paramName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := param.validate(types...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, errOptionalOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on name/rename conflict
|
||||||
|
for paramName2, param2 := range svc.Output {
|
||||||
|
// ignore self
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,50 @@
|
||||||
package dynfunc
|
package dynfunc
|
||||||
|
|
||||||
// Err allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type Err string
|
type cerr string
|
||||||
|
|
||||||
func (err Err) Error() string {
|
func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// errHandlerNotFunc - handler is not a func
|
||||||
// ErrHandlerNotFunc - handler is not a func
|
const errHandlerNotFunc = cerr("handler must be a func")
|
||||||
ErrHandlerNotFunc = Err("handler must be a func")
|
|
||||||
|
|
||||||
// ErrNoServiceForHandler - no service matching this handler
|
// errNoServiceForHandler - no service matching this handler
|
||||||
ErrNoServiceForHandler = Err("no service found for this handler")
|
const errNoServiceForHandler = cerr("no service found for this handler")
|
||||||
|
|
||||||
// errMissingHandlerArgumentParam - missing params arguments for handler
|
// errMissingHandlerArgumentParam - missing params arguments for handler
|
||||||
ErrMissingHandlerContextArgument = Err("missing handler first argument of type context.Context")
|
const errMissingHandlerContextArgument = cerr("missing handler first argument of type context.Context")
|
||||||
|
|
||||||
// ErrInvalidHandlerContextArgument - missing handler output error
|
// errMissingHandlerInputArgument - missing params arguments for handler
|
||||||
ErrInvalidHandlerContextArgument = Err("first input argument should be of type context.Context")
|
const errMissingHandlerInputArgument = cerr("missing handler argument: input struct")
|
||||||
|
|
||||||
// ErrMissingHandlerInputArgument - missing params arguments for handler
|
// errUnexpectedInput - input argument is not expected
|
||||||
ErrMissingHandlerInputArgument = Err("missing handler argument: input struct")
|
const errUnexpectedInput = cerr("unexpected input struct")
|
||||||
|
|
||||||
// ErrUnexpectedInput - input argument is not expected
|
// errMissingHandlerOutputArgument - missing output for handler
|
||||||
ErrUnexpectedInput = Err("unexpected input struct")
|
const errMissingHandlerOutputArgument = cerr("missing handler first output argument: output struct")
|
||||||
|
|
||||||
// ErrMissingHandlerOutputArgument - missing output for handler
|
// errMissingHandlerOutputError - missing error output for handler
|
||||||
ErrMissingHandlerOutputArgument = Err("missing handler first output argument: output struct")
|
const errMissingHandlerOutputError = cerr("missing handler last output argument of type api.Err")
|
||||||
|
|
||||||
// ErrMissingHandlerErrorArgument - missing error output for handler
|
// errMissingRequestArgument - missing request argument for handler
|
||||||
ErrMissingHandlerErrorArgument = Err("missing handler last output argument of type api.Err")
|
const errMissingRequestArgument = cerr("handler first argument must be of type api.Request")
|
||||||
|
|
||||||
// ErrInvalidHandlerErrorArgument - missing handler output error
|
// errMissingParamArgument - missing parameters argument for handler
|
||||||
ErrInvalidHandlerErrorArgument = Err("last output must be of type api.Err")
|
const errMissingParamArgument = cerr("handler second argument must be a struct")
|
||||||
|
|
||||||
// ErrMissingParamArgument - missing parameters argument for handler
|
// errUnexportedName - argument is unexported in struct
|
||||||
ErrMissingParamArgument = Err("handler second argument must be a struct")
|
const errUnexportedName = cerr("unexported name")
|
||||||
|
|
||||||
// ErrUnexportedName - argument is unexported in struct
|
// errWrongOutputArgumentType - wrong type for output first argument
|
||||||
ErrUnexportedName = Err("unexported name")
|
const errWrongOutputArgumentType = cerr("handler first output argument must be a *struct")
|
||||||
|
|
||||||
// ErrWrongOutputArgumentType - wrong type for output first argument
|
// errMissingConfigArgument - missing an input/output argument in handler struct
|
||||||
ErrWrongOutputArgumentType = Err("handler first output argument must be a *struct")
|
const errMissingConfigArgument = cerr("missing an argument from the configuration")
|
||||||
|
|
||||||
// ErrMissingConfigArgument - missing an input/output argument in handler struct
|
// errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
|
||||||
ErrMissingConfigArgument = Err("missing an argument from the configuration")
|
const errWrongParamTypeFromConfig = cerr("invalid struct field type")
|
||||||
|
|
||||||
// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
|
// errMissingHandlerErrorArgument - missing handler output error
|
||||||
ErrWrongParamTypeFromConfig = Err("invalid struct field type")
|
const errMissingHandlerErrorArgument = cerr("last output must be of type api.Err")
|
||||||
)
|
|
||||||
|
|
|
@ -20,14 +20,14 @@ type Handler struct {
|
||||||
|
|
||||||
// Build a handler from a dynamic function and checks its signature against a
|
// Build a handler from a dynamic function and checks its signature against a
|
||||||
// service configuration
|
// service configuration
|
||||||
//
|
//e
|
||||||
// `fn` must have as a signature : `func(context.Context, in) (*out, api.Err)`
|
// `fn` must have as a signature : `func(*api.Context, in) (*out, api.Err)`
|
||||||
// - `in` is a struct{} containing a field for each service input (with valid reflect.Type)
|
// - `in` is a struct{} containing a field for each service input (with valid reflect.Type)
|
||||||
// - `out` is a struct{} containing a field for each service output (with valid reflect.Type)
|
// - `out` is a struct{} containing a field for each service output (with valid reflect.Type)
|
||||||
//
|
//
|
||||||
// Special cases:
|
// Special cases:
|
||||||
// - it there is no input, `in` MUST be omitted
|
// - it there is no input, `in` MUST be omitted
|
||||||
// - it there is no output, `out` CAN be omitted
|
// - it there is no output, `out` MUST be omitted
|
||||||
func Build(fn interface{}, service config.Service) (*Handler, error) {
|
func Build(fn interface{}, service config.Service) (*Handler, error) {
|
||||||
var (
|
var (
|
||||||
h = &Handler{
|
h = &Handler{
|
||||||
|
@ -38,7 +38,7 @@ func Build(fn interface{}, service config.Service) (*Handler, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if fnType.Kind() != reflect.Func {
|
if fnType.Kind() != reflect.Func {
|
||||||
return nil, ErrHandlerNotFunc
|
return nil, errHandlerNotFunc
|
||||||
}
|
}
|
||||||
if err := h.signature.ValidateInput(fnType); err != nil {
|
if err := h.signature.ValidateInput(fnType); err != nil {
|
||||||
return nil, fmt.Errorf("input: %w", err)
|
return nil, fmt.Errorf("input: %w", err)
|
||||||
|
|
|
@ -31,17 +31,17 @@ func BuildSignature(service config.Service) *Signature {
|
||||||
}
|
}
|
||||||
// make a pointer if optional
|
// make a pointer if optional
|
||||||
if param.Optional {
|
if param.Optional {
|
||||||
s.Input[param.Rename] = reflect.PtrTo(param.GoType)
|
s.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.Input[param.Rename] = param.GoType
|
s.Input[param.Rename] = param.ExtractType
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, param := range service.Output {
|
for _, param := range service.Output {
|
||||||
if len(param.Rename) < 1 {
|
if len(param.Rename) < 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.Output[param.Rename] = param.GoType
|
s.Output[param.Rename] = param.ExtractType
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
@ -53,47 +53,47 @@ func (s *Signature) ValidateInput(handlerType reflect.Type) error {
|
||||||
|
|
||||||
// missing or invalid first arg: context.Context
|
// missing or invalid first arg: context.Context
|
||||||
if handlerType.NumIn() < 1 {
|
if handlerType.NumIn() < 1 {
|
||||||
return ErrMissingHandlerContextArgument
|
return errMissingHandlerContextArgument
|
||||||
}
|
}
|
||||||
firstArgType := handlerType.In(0)
|
firstArgType := handlerType.In(0)
|
||||||
|
|
||||||
if !firstArgType.Implements(ctxType) {
|
if !firstArgType.Implements(ctxType) {
|
||||||
return ErrInvalidHandlerContextArgument
|
return fmt.Errorf("fock")
|
||||||
}
|
}
|
||||||
|
|
||||||
// no input required
|
// no input required
|
||||||
if len(s.Input) == 0 {
|
if len(s.Input) == 0 {
|
||||||
// input struct provided
|
// input struct provided
|
||||||
if handlerType.NumIn() > 1 {
|
if handlerType.NumIn() > 1 {
|
||||||
return ErrUnexpectedInput
|
return errUnexpectedInput
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// too much arguments
|
// too much arguments
|
||||||
if handlerType.NumIn() != 2 {
|
if handlerType.NumIn() > 2 {
|
||||||
return ErrMissingHandlerInputArgument
|
return errMissingHandlerInputArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// arg must be a struct
|
// arg must be a struct
|
||||||
inStruct := handlerType.In(1)
|
inStruct := handlerType.In(1)
|
||||||
if inStruct.Kind() != reflect.Struct {
|
if inStruct.Kind() != reflect.Struct {
|
||||||
return ErrMissingParamArgument
|
return errMissingParamArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for invalid param
|
// check for invalid param
|
||||||
for name, ptype := range s.Input {
|
for name, ptype := range s.Input {
|
||||||
if name[0] == strings.ToLower(name)[0] {
|
if name[0] == strings.ToLower(name)[0] {
|
||||||
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
return fmt.Errorf("%s: %w", name, errUnexportedName)
|
||||||
}
|
}
|
||||||
|
|
||||||
field, exists := inStruct.FieldByName(name)
|
field, exists := inStruct.FieldByName(name)
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingConfigArgument)
|
return fmt.Errorf("%s: %w", name, errMissingConfigArgument)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ptype.AssignableTo(field.Type) {
|
if !ptype.AssignableTo(field.Type) {
|
||||||
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,44 +105,44 @@ func (s Signature) ValidateOutput(handlerType reflect.Type) error {
|
||||||
errType := reflect.TypeOf(api.ErrUnknown)
|
errType := reflect.TypeOf(api.ErrUnknown)
|
||||||
|
|
||||||
if handlerType.NumOut() < 1 {
|
if handlerType.NumOut() < 1 {
|
||||||
return ErrMissingHandlerErrorArgument
|
return errMissingHandlerErrorArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// last output must be api.Err
|
// last output must be api.Err
|
||||||
lastArgType := handlerType.Out(handlerType.NumOut() - 1)
|
lastArgType := handlerType.Out(handlerType.NumOut() - 1)
|
||||||
if !lastArgType.AssignableTo(errType) {
|
if !lastArgType.AssignableTo(errType) {
|
||||||
return ErrInvalidHandlerErrorArgument
|
return errMissingHandlerErrorArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// no output required -> ok
|
// no output -> ok
|
||||||
if len(s.Output) == 0 {
|
if len(s.Output) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if handlerType.NumOut() < 2 {
|
if handlerType.NumOut() < 2 {
|
||||||
return ErrMissingHandlerOutputArgument
|
return errMissingHandlerOutputArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if first output is not a pointer to struct
|
// fail if first output is not a pointer to struct
|
||||||
outStructPtr := handlerType.Out(0)
|
outStructPtr := handlerType.Out(0)
|
||||||
if outStructPtr.Kind() != reflect.Ptr {
|
if outStructPtr.Kind() != reflect.Ptr {
|
||||||
return ErrWrongOutputArgumentType
|
return errWrongOutputArgumentType
|
||||||
}
|
}
|
||||||
|
|
||||||
outStruct := outStructPtr.Elem()
|
outStruct := outStructPtr.Elem()
|
||||||
if outStruct.Kind() != reflect.Struct {
|
if outStruct.Kind() != reflect.Struct {
|
||||||
return ErrWrongOutputArgumentType
|
return errWrongOutputArgumentType
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail on invalid output
|
// fail on invalid output
|
||||||
for name, ptype := range s.Output {
|
for name, ptype := range s.Output {
|
||||||
if name[0] == strings.ToLower(name)[0] {
|
if name[0] == strings.ToLower(name)[0] {
|
||||||
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
return fmt.Errorf("%s: %w", name, errUnexportedName)
|
||||||
}
|
}
|
||||||
|
|
||||||
field, exists := outStruct.FieldByName(name)
|
field, exists := outStruct.FieldByName(name)
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingConfigArgument)
|
return fmt.Errorf("%s: %w", name, errMissingConfigArgument)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore types evalutating to nil
|
// ignore types evalutating to nil
|
||||||
|
@ -151,7 +151,7 @@ func (s Signature) ValidateOutput(handlerType reflect.Type) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !field.Type.ConvertibleTo(ptype) {
|
if !field.Type.ConvertibleTo(ptype) {
|
||||||
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,562 +8,382 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
"github.com/xdrm-io/aicra/api"
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInputValidation(t *testing.T) {
|
func TestInputCheck(t *testing.T) {
|
||||||
tt := []struct {
|
tcases := []struct {
|
||||||
name string
|
Name string
|
||||||
input map[string]reflect.Type
|
Input map[string]reflect.Type
|
||||||
fn interface{}
|
Fn interface{}
|
||||||
err error
|
FnCtx interface{}
|
||||||
|
Err error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing context",
|
Name: "no input 0 given",
|
||||||
input: map[string]reflect.Type{},
|
Input: map[string]reflect.Type{},
|
||||||
fn: func() {},
|
Fn: func(context.Context) {},
|
||||||
err: ErrMissingHandlerContextArgument,
|
FnCtx: func(context.Context) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid context",
|
Name: "no input 1 given",
|
||||||
input: map[string]reflect.Type{},
|
Input: map[string]reflect.Type{},
|
||||||
fn: func(int) {},
|
Fn: func(context.Context, int) {},
|
||||||
err: ErrInvalidHandlerContextArgument,
|
FnCtx: func(context.Context, int) {},
|
||||||
|
Err: errUnexpectedInput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no input 0 given",
|
Name: "no input 2 given",
|
||||||
input: map[string]reflect.Type{},
|
Input: map[string]reflect.Type{},
|
||||||
fn: func(context.Context) {},
|
Fn: func(context.Context, int, string) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, int, string) {},
|
||||||
|
Err: errUnexpectedInput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no input 1 given",
|
Name: "1 input 0 given",
|
||||||
input: map[string]reflect.Type{},
|
Input: map[string]reflect.Type{
|
||||||
fn: func(context.Context, int) {},
|
|
||||||
err: ErrUnexpectedInput,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no input 2 given",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func(context.Context, int, string) {},
|
|
||||||
err: ErrUnexpectedInput,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input 0 given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context) {},
|
Fn: func(context.Context) {},
|
||||||
err: ErrMissingHandlerInputArgument,
|
FnCtx: func(context.Context) {},
|
||||||
|
Err: errMissingHandlerInputArgument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input non-struct given",
|
Name: "1 input non-struct given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, int) {},
|
Fn: func(context.Context, int) {},
|
||||||
err: ErrMissingParamArgument,
|
FnCtx: func(context.Context, int) {},
|
||||||
|
Err: errMissingParamArgument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unexported input",
|
Name: "unexported input",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"test1": reflect.TypeOf(int(0)),
|
"test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{}) {},
|
Fn: func(context.Context, struct{}) {},
|
||||||
err: ErrUnexportedName,
|
FnCtx: func(context.Context, struct{}) {},
|
||||||
|
Err: errUnexportedName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input empty struct given",
|
Name: "1 input empty struct given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{}) {},
|
Fn: func(context.Context, struct{}) {},
|
||||||
err: ErrMissingConfigArgument,
|
FnCtx: func(context.Context, struct{}) {},
|
||||||
|
Err: errMissingConfigArgument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input invalid given",
|
Name: "1 input invalid given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
Fn: func(context.Context, struct{ Test1 string }) {},
|
||||||
err: ErrWrongParamTypeFromConfig,
|
FnCtx: func(context.Context, struct{ Test1 string }) {},
|
||||||
|
Err: errWrongParamTypeFromConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input valid given",
|
Name: "1 input valid given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 int }) {},
|
Fn: func(context.Context, struct{ Test1 int }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 int }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input ptr empty struct given",
|
Name: "1 input ptr empty struct given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
"Test1": reflect.TypeOf(new(int)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{}) {},
|
Fn: func(context.Context, struct{}) {},
|
||||||
err: ErrMissingConfigArgument,
|
FnCtx: func(context.Context, struct{}) {},
|
||||||
|
Err: errMissingConfigArgument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input ptr invalid given",
|
Name: "1 input ptr invalid given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
"Test1": reflect.TypeOf(new(int)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
Fn: func(context.Context, struct{ Test1 string }) {},
|
||||||
err: ErrWrongParamTypeFromConfig,
|
FnCtx: func(context.Context, struct{ Test1 string }) {},
|
||||||
|
Err: errWrongParamTypeFromConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input ptr invalid ptr type given",
|
Name: "1 input ptr invalid ptr type given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
"Test1": reflect.TypeOf(new(int)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *string }) {},
|
Fn: func(context.Context, struct{ Test1 *string }) {},
|
||||||
err: ErrWrongParamTypeFromConfig,
|
FnCtx: func(context.Context, struct{ Test1 *string }) {},
|
||||||
|
Err: errWrongParamTypeFromConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 input ptr valid given",
|
Name: "1 input ptr valid given",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
"Test1": reflect.TypeOf(new(int)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *int }) {},
|
Fn: func(context.Context, struct{ Test1 *int }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *int }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid string",
|
Name: "1 valid string",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(string("")),
|
"Test1": reflect.TypeOf(string("")),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
Fn: func(context.Context, struct{ Test1 string }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 string }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid uint",
|
Name: "1 valid uint",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(uint(0)),
|
"Test1": reflect.TypeOf(uint(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 uint }) {},
|
Fn: func(context.Context, struct{ Test1 uint }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 uint }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid float64",
|
Name: "1 valid float64",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(float64(0)),
|
"Test1": reflect.TypeOf(float64(0)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 float64 }) {},
|
Fn: func(context.Context, struct{ Test1 float64 }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 float64 }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid []byte",
|
Name: "1 valid []byte",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf([]byte("")),
|
"Test1": reflect.TypeOf([]byte("")),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 []byte }) {},
|
Fn: func(context.Context, struct{ Test1 []byte }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 []byte }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid []rune",
|
Name: "1 valid []rune",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf([]rune("")),
|
"Test1": reflect.TypeOf([]rune("")),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 []rune }) {},
|
Fn: func(context.Context, struct{ Test1 []rune }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 []rune }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid *string",
|
Name: "1 valid *string",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(string)),
|
"Test1": reflect.TypeOf(new(string)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *string }) {},
|
Fn: func(context.Context, struct{ Test1 *string }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *string }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid *uint",
|
Name: "1 valid *uint",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(uint)),
|
"Test1": reflect.TypeOf(new(uint)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *uint }) {},
|
Fn: func(context.Context, struct{ Test1 *uint }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *uint }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid *float64",
|
Name: "1 valid *float64",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new(float64)),
|
"Test1": reflect.TypeOf(new(float64)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *float64 }) {},
|
Fn: func(context.Context, struct{ Test1 *float64 }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *float64 }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid *[]byte",
|
Name: "1 valid *[]byte",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new([]byte)),
|
"Test1": reflect.TypeOf(new([]byte)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *[]byte }) {},
|
Fn: func(context.Context, struct{ Test1 *[]byte }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *[]byte }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "1 valid *[]rune",
|
Name: "1 valid *[]rune",
|
||||||
input: map[string]reflect.Type{
|
Input: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(new([]rune)),
|
"Test1": reflect.TypeOf(new([]rune)),
|
||||||
},
|
},
|
||||||
fn: func(context.Context, struct{ Test1 *[]rune }) {},
|
Fn: func(context.Context, struct{ Test1 *[]rune }) {},
|
||||||
err: nil,
|
FnCtx: func(context.Context, struct{ Test1 *[]rune }) {},
|
||||||
|
Err: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tcase := range tcases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tcase.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
// mock spec
|
// mock spec
|
||||||
s := Signature{
|
s := Signature{
|
||||||
Input: tc.input,
|
Input: tcase.Input,
|
||||||
Output: nil,
|
Output: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.ValidateInput(reflect.TypeOf(tc.fn))
|
err := s.ValidateInput(reflect.TypeOf(tcase.FnCtx))
|
||||||
if err == nil && tc.err != nil {
|
if err == nil && tcase.Err != nil {
|
||||||
t.Fatalf("expected an error: '%s'", tc.err.Error())
|
t.Errorf("expected an error: '%s'", tcase.Err.Error())
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
if err != nil && tc.err == nil {
|
if err != nil && tcase.Err == nil {
|
||||||
t.Fatalf("unexpected error: '%s'", err.Error())
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && tc.err != nil {
|
if err != nil && tcase.Err != nil {
|
||||||
if !errors.Is(err, tc.err) {
|
if !errors.Is(err, tcase.Err) {
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
t.Errorf("expected the error <%s> got <%s>", tcase.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutputValidation(t *testing.T) {
|
func TestOutputCheck(t *testing.T) {
|
||||||
tt := []struct {
|
tcases := []struct {
|
||||||
name string
|
Output map[string]reflect.Type
|
||||||
output map[string]reflect.Type
|
Fn interface{}
|
||||||
fn interface{}
|
Err error
|
||||||
err error
|
|
||||||
}{
|
}{
|
||||||
|
// no input -> missing api.Err
|
||||||
{
|
{
|
||||||
name: "no output missing err",
|
Output: map[string]reflect.Type{},
|
||||||
output: map[string]reflect.Type{},
|
Fn: func(context.Context) {},
|
||||||
fn: func() {},
|
Err: errMissingHandlerOutputArgument,
|
||||||
err: ErrMissingHandlerErrorArgument,
|
|
||||||
},
|
},
|
||||||
|
// no input -> with last type not api.Err
|
||||||
{
|
{
|
||||||
name: "no output invalid err",
|
Output: map[string]reflect.Type{},
|
||||||
output: map[string]reflect.Type{},
|
Fn: func(context.Context) bool { return true },
|
||||||
fn: func() bool { return true },
|
Err: errMissingHandlerErrorArgument,
|
||||||
err: ErrInvalidHandlerErrorArgument,
|
|
||||||
},
|
},
|
||||||
|
// no input -> with api.Err
|
||||||
{
|
{
|
||||||
name: "1 output none required",
|
Output: map[string]reflect.Type{},
|
||||||
output: map[string]reflect.Type{},
|
Fn: func(context.Context) api.Err { return api.ErrSuccess },
|
||||||
fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
Err: nil,
|
||||||
err: nil,
|
|
||||||
},
|
},
|
||||||
|
// no input -> missing context.Context
|
||||||
{
|
{
|
||||||
name: "no output 1 required",
|
Output: map[string]reflect.Type{},
|
||||||
output: map[string]reflect.Type{
|
Fn: func(context.Context) api.Err { return api.ErrSuccess },
|
||||||
|
Err: errMissingHandlerContextArgument,
|
||||||
|
},
|
||||||
|
// no input -> invlaid context.Context type
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func(context.Context, int) api.Err { return api.ErrSuccess },
|
||||||
|
Err: errMissingHandlerContextArgument,
|
||||||
|
},
|
||||||
|
// func can have output if not specified
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
// missing output struct in func
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() api.Err { return api.ErrSuccess },
|
Fn: func() api.Err { return api.ErrSuccess },
|
||||||
err: ErrMissingHandlerOutputArgument,
|
Err: errWrongOutputArgumentType,
|
||||||
},
|
},
|
||||||
|
// output not a pointer
|
||||||
{
|
{
|
||||||
name: "invalid int output",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (int, api.Err) { return 0, api.ErrSuccess },
|
Fn: func() (int, api.Err) { return 0, api.ErrSuccess },
|
||||||
err: ErrWrongOutputArgumentType,
|
Err: errWrongOutputArgumentType,
|
||||||
},
|
},
|
||||||
|
// output not a pointer to struct
|
||||||
{
|
{
|
||||||
name: "invalid int ptr output",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (*int, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*int, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: ErrWrongOutputArgumentType,
|
Err: errWrongOutputArgumentType,
|
||||||
},
|
},
|
||||||
|
// unexported param name
|
||||||
{
|
{
|
||||||
name: "invalid struct output",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (struct{ Test1 int }, api.Err) { return struct{ Test1 int }{Test1: 1}, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unexported param",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"test1": reflect.TypeOf(int(0)),
|
"test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: ErrUnexportedName,
|
Err: errUnexportedName,
|
||||||
},
|
},
|
||||||
|
// output field missing
|
||||||
{
|
{
|
||||||
name: "missing output param",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: ErrMissingConfigArgument,
|
Err: errMissingConfigArgument,
|
||||||
},
|
},
|
||||||
|
// output field invalid type
|
||||||
{
|
{
|
||||||
name: "invalid output param",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: ErrWrongParamTypeFromConfig,
|
Err: errWrongParamTypeFromConfig,
|
||||||
},
|
},
|
||||||
|
// output field valid type
|
||||||
{
|
{
|
||||||
name: "valid param",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
},
|
},
|
||||||
fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: nil,
|
Err: nil,
|
||||||
},
|
},
|
||||||
|
// ignore type check on nil type
|
||||||
{
|
{
|
||||||
name: "2 valid params",
|
Output: map[string]reflect.Type{
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
"Test2": reflect.TypeOf(string("")),
|
|
||||||
},
|
|
||||||
fn: func() (*struct {
|
|
||||||
Test1 int
|
|
||||||
Test2 string
|
|
||||||
}, api.Err) {
|
|
||||||
return nil, api.ErrSuccess
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil type ignore typecheck",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": nil,
|
"Test1": nil,
|
||||||
},
|
},
|
||||||
fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
||||||
err: nil,
|
Err: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for i, tcase := range tcases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
// mock spec
|
// mock spec
|
||||||
s := Signature{
|
s := Signature{
|
||||||
Input: nil,
|
Input: nil,
|
||||||
Output: tc.output,
|
Output: tcase.Output,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.ValidateOutput(reflect.TypeOf(tc.fn))
|
err := s.ValidateOutput(reflect.TypeOf(tcase.Fn))
|
||||||
if !errors.Is(err, tc.err) {
|
if err == nil && tcase.Err != nil {
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
t.Errorf("expected an error: '%s'", tcase.Err.Error())
|
||||||
}
|
t.FailNow()
|
||||||
})
|
}
|
||||||
}
|
if err != nil && tcase.Err == nil {
|
||||||
}
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
func TestServiceValidation(t *testing.T) {
|
}
|
||||||
|
|
||||||
tt := []struct {
|
if err != nil && tcase.Err != nil {
|
||||||
name string
|
if !errors.Is(err, tcase.Err) {
|
||||||
in []*config.Parameter
|
t.Errorf("expected the error <%s> got <%s>", tcase.Err, err)
|
||||||
out []*config.Parameter
|
t.FailNow()
|
||||||
fn interface{}
|
}
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "missing context",
|
|
||||||
fn: func() {},
|
|
||||||
err: ErrMissingHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid context",
|
|
||||||
fn: func(int) {},
|
|
||||||
err: ErrInvalidHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing error",
|
|
||||||
fn: func(context.Context) {},
|
|
||||||
err: ErrMissingHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid error",
|
|
||||||
fn: func(context.Context) int { return 1 },
|
|
||||||
err: ErrInvalidHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no in no out",
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unamed in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "", // should be ignored
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrMissingHandlerInputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional in not ptr",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid optional in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "unamed out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "", // should be ignored
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing out struct",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrMissingHandlerOutputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid out struct type",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (int, api.Err) { return 0, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrMissingConfigArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional out not ptr",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{ Test1 *int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
service := config.Service{
|
|
||||||
Input: make(map[string]*config.Parameter),
|
|
||||||
Output: make(map[string]*config.Parameter),
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill service with arguments
|
|
||||||
if tc.in != nil && len(tc.in) > 0 {
|
|
||||||
for i, in := range tc.in {
|
|
||||||
service.Input[fmt.Sprintf("%d", i)] = in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tc.out != nil && len(tc.out) > 0 {
|
|
||||||
for i, out := range tc.out {
|
|
||||||
service.Output[fmt.Sprintf("%d", i)] = out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := BuildSignature(service)
|
|
||||||
|
|
||||||
err := s.ValidateInput(reflect.TypeOf(tc.fn))
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.ValidateOutput(reflect.TypeOf(tc.fn))
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// no error encountered but expected 1
|
|
||||||
if tc.err != nil {
|
|
||||||
t.Fatalf("expected an error <%v>", tc.err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,24 +66,17 @@ func (i *T) GetQuery(req http.Request) error {
|
||||||
query := req.URL.Query()
|
query := req.URL.Query()
|
||||||
|
|
||||||
for name, param := range i.service.Query {
|
for name, param := range i.service.Query {
|
||||||
values, exist := query[name]
|
value, exist := query[name]
|
||||||
|
|
||||||
if !exist {
|
if !exist && !param.Optional {
|
||||||
if !param.Optional {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed interface{}
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
// consider element instead of slice or elements when only 1
|
|
||||||
if len(values) == 1 {
|
|
||||||
parsed = parseParameter(values[0])
|
|
||||||
} else { // consider slice
|
|
||||||
parsed = parseParameter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, valid := param.Validator(parsed)
|
cast, valid := param.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
@ -106,33 +99,18 @@ func (i *T) GetForm(req http.Request) error {
|
||||||
ct := req.Header.Get("Content-Type")
|
ct := req.Header.Get("Content-Type")
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(ct, "application/json"):
|
case strings.HasPrefix(ct, "application/json"):
|
||||||
err := i.parseJSON(req)
|
return i.parseJSON(req)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
|
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
|
||||||
err := i.parseUrlencoded(req)
|
return i.parseUrlencoded(req)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasPrefix(ct, "multipart/form-data; boundary="):
|
case strings.HasPrefix(ct, "multipart/form-data; boundary="):
|
||||||
err := i.parseMultipart(req)
|
return i.parseMultipart(req)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail on at least 1 mandatory form param when there is no body
|
default:
|
||||||
for name, param := range i.service.Form {
|
|
||||||
_, exists := i.Data[param.Rename]
|
|
||||||
if !exists && !param.Optional {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseJSON parses JSON from the request body inside 'Form'
|
// parseJSON parses JSON from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
|
@ -151,6 +129,10 @@ func (i *T) parseJSON(req http.Request) error {
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
value, exist := parsed[name]
|
value, exist := parsed[name]
|
||||||
|
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -173,21 +155,17 @@ func (i *T) parseUrlencoded(req http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
values, exist := req.PostForm[name]
|
value, exist := req.PostForm[name]
|
||||||
|
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed interface{}
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
// consider element instead of slice or elements when only 1
|
|
||||||
if len(values) == 1 {
|
|
||||||
parsed = parseParameter(values[0])
|
|
||||||
} else { // consider slice
|
|
||||||
parsed = parseParameter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, valid := param.Validator(parsed)
|
cast, valid := param.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
@ -207,7 +185,7 @@ func (i *T) parseMultipart(req http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mpr.Parse()
|
err = mpr.Parse()
|
||||||
|
@ -218,6 +196,10 @@ func (i *T) parseMultipart(req http.Request) error {
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
component, exist := mpr.Data[name]
|
component, exist := mpr.Data[name]
|
||||||
|
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,11 +135,13 @@ func TestStoreWithUri(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(store.Data) != len(service.Input) {
|
if len(store.Data) != len(service.Input) {
|
||||||
|
@ -181,14 +183,14 @@ func TestExtractQuery(t *testing.T) {
|
||||||
Query: "a",
|
Query: "a",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a"},
|
ServiceParam: []string{"a"},
|
||||||
Query: "a&b",
|
Query: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "missing"},
|
ServiceParam: []string{"a", "missing"},
|
||||||
|
@ -202,40 +204,40 @@ func TestExtractQuery(t *testing.T) {
|
||||||
Query: "a&b",
|
Query: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {""}},
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a"},
|
ServiceParam: []string{"a"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=",
|
Query: "a=",
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "b"},
|
ServiceParam: []string{"a", "b"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=&b=x",
|
Query: "a=&b=x",
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {"x"}},
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "c"},
|
ServiceParam: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=b&c=d",
|
Query: "a=b&c=d",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b"}, {"d"}},
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "c"},
|
ServiceParam: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=b&c=d&a=x",
|
Query: "a=b&c=d&a=x",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b", "x"}, {"d"}},
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("request[%d]", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
|
||||||
store := New(getServiceWithQuery(test.ServiceParam...))
|
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||||
|
|
||||||
|
@ -244,16 +246,19 @@ func TestExtractQuery(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -261,7 +266,8 @@ func TestExtractQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
for pi, pName := range test.ParamNames {
|
||||||
|
@ -270,35 +276,29 @@ func TestExtractQuery(t *testing.T) {
|
||||||
t.Run(pName, func(t *testing.T) {
|
t.Run(pName, func(t *testing.T) {
|
||||||
param, isset := store.Data[pName]
|
param, isset := store.Data[pName]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("param does not exist")
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// single value, should return a single element
|
|
||||||
if len(values) == 1 {
|
|
||||||
cast, canCast := param.(string)
|
|
||||||
if !canCast {
|
|
||||||
t.Fatalf("should return a string (got '%v')", cast)
|
|
||||||
}
|
|
||||||
if values[0] != cast {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiple values, should return a slice
|
|
||||||
cast, canCast := param.([]interface{})
|
cast, canCast := param.([]interface{})
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Fatalf("should return a []string (got '%v')", cast)
|
t.Errorf("should return a []string (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
if len(cast) != len(values) {
|
||||||
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for vi, value := range values {
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
if value != cast[vi] {
|
if value != cast[vi] {
|
||||||
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -326,7 +326,9 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||||
store := New(nil)
|
store := New(nil)
|
||||||
err := store.GetForm(*req)
|
err := store.GetForm(*req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestExtractFormUrlEncoded(t *testing.T) {
|
func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
|
@ -357,14 +359,14 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
URLEncoded: "a",
|
URLEncoded: "a",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a"},
|
ServiceParams: []string{"a"},
|
||||||
URLEncoded: "a&b",
|
URLEncoded: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "missing"},
|
ServiceParams: []string{"a", "missing"},
|
||||||
|
@ -378,35 +380,35 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
URLEncoded: "a&b",
|
URLEncoded: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {""}},
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a"},
|
ServiceParams: []string{"a"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=",
|
URLEncoded: "a=",
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "b"},
|
ServiceParams: []string{"a", "b"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=&b=x",
|
URLEncoded: "a=&b=x",
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {"x"}},
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "c"},
|
ServiceParams: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=b&c=d",
|
URLEncoded: "a=b&c=d",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b"}, {"d"}},
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "c"},
|
ServiceParams: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=b&c=d&a=x",
|
URLEncoded: "a=b&c=d&a=x",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b", "x"}, {"d"}},
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,16 +424,19 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -439,7 +444,8 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for pi, key := range test.ParamNames {
|
for pi, key := range test.ParamNames {
|
||||||
|
@ -448,35 +454,29 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
t.Run(key, func(t *testing.T) {
|
t.Run(key, func(t *testing.T) {
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("param does not exist")
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// single value, should return a single element
|
|
||||||
if len(values) == 1 {
|
|
||||||
cast, canCast := param.(string)
|
|
||||||
if !canCast {
|
|
||||||
t.Fatalf("should return a string (got '%v')", cast)
|
|
||||||
}
|
|
||||||
if values[0] != cast {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiple values, should return a slice
|
|
||||||
cast, canCast := param.([]interface{})
|
cast, canCast := param.([]interface{})
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Fatalf("should return a []string (got '%v')", cast)
|
t.Errorf("should return a []interface{} (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
if len(cast) != len(values) {
|
||||||
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for vi, value := range values {
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
if value != cast[vi] {
|
if value != cast[vi] {
|
||||||
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -567,16 +567,19 @@ func TestJsonParameters(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -584,7 +587,8 @@ func TestJsonParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
for pi, pName := range test.ParamNames {
|
||||||
|
@ -595,7 +599,8 @@ func TestJsonParameters(t *testing.T) {
|
||||||
|
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("store should contain element with key '%s'", key)
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,11 +610,13 @@ func TestJsonParameters(t *testing.T) {
|
||||||
paramValueType := reflect.TypeOf(param)
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
if valueType != paramValueType {
|
if valueType != paramValueType {
|
||||||
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if paramValue != value {
|
if paramValue != value {
|
||||||
t.Fatalf("should return %v (got '%v')", value, paramValue)
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -717,16 +724,19 @@ x
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -734,7 +744,8 @@ x
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for pi, key := range test.ParamNames {
|
for pi, key := range test.ParamNames {
|
||||||
|
@ -744,7 +755,8 @@ x
|
||||||
|
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("store should contain element with key '%s'", key)
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -754,11 +766,13 @@ x
|
||||||
paramValueType := reflect.TypeOf(param)
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
if valueType != paramValueType {
|
if valueType != paramValueType {
|
||||||
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if paramValue != value {
|
if paramValue != value {
|
||||||
t.Fatalf("should return %v (got '%v')", value, paramValue)
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
60
response.go
60
response.go
|
@ -1,60 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
// response for an service call
|
|
||||||
type response struct {
|
|
||||||
Data map[string]interface{}
|
|
||||||
Status int
|
|
||||||
err api.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// newResponse creates an empty response.
|
|
||||||
func newResponse() *response {
|
|
||||||
return &response{
|
|
||||||
Status: http.StatusOK,
|
|
||||||
Data: make(map[string]interface{}),
|
|
||||||
err: api.ErrFailure,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithError sets the response error
|
|
||||||
func (r *response) WithError(err api.Err) *response {
|
|
||||||
r.err = err
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithValue sets a response value
|
|
||||||
func (r *response) WithValue(name string, value interface{}) *response {
|
|
||||||
r.Data[name] = value
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON generates the JSON representation of the response
|
|
||||||
//
|
|
||||||
// implements json.Marshaler
|
|
||||||
func (r *response) MarshalJSON() ([]byte, error) {
|
|
||||||
fmt := make(map[string]interface{})
|
|
||||||
for k, v := range r.Data {
|
|
||||||
fmt[k] = v
|
|
||||||
}
|
|
||||||
fmt["error"] = r.err
|
|
||||||
return json.Marshal(fmt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP writes the response representation back to the http.ResponseWriter
|
|
||||||
//
|
|
||||||
// implements http.Handler
|
|
||||||
func (res *response) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
w.WriteHeader(res.err.Status)
|
|
||||||
encoded, err := json.Marshal(res)
|
|
||||||
if err == nil {
|
|
||||||
w.Write(encoded)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func printEscaped(raw string) string {
|
|
||||||
raw = strings.ReplaceAll(raw, "\n", "\\n")
|
|
||||||
raw = strings.ReplaceAll(raw, "\r", "\\r")
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResponseJSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
err api.Err
|
|
||||||
data map[string]interface{}
|
|
||||||
json string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty success response",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty failure response",
|
|
||||||
err: api.ErrFailure,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":1,"reason":"it failed"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty unknown error response",
|
|
||||||
err: api.ErrUnknown,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":-1,"reason":"unknown error"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data before err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"a": 12},
|
|
||||||
json: `{"a":12,"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data right before err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"e": 12},
|
|
||||||
json: `{"e":12,"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data right after err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"f": 12},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"},"f":12}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data after err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"z": 12},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"},"z":12}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data around err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"d": "before", "f": "after"},
|
|
||||||
json: `{"d":"before","error":{"code":0,"reason":"all right"},"f":"after"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
res := newResponse().WithError(tc.err)
|
|
||||||
for k, v := range tc.data {
|
|
||||||
res.WithValue(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot marshal to json: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(raw) != tc.json {
|
|
||||||
t.Fatalf("mismatching json:\nexpect: %v\nactual: %v", printEscaped(tc.json), printEscaped(string(raw)))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package validator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AnyType makes the "any" type available in the aicra configuration
|
|
||||||
// It considers valid any value
|
|
||||||
type AnyType struct{}
|
|
||||||
|
|
||||||
// GoType returns the interface{} type
|
|
||||||
func (AnyType) GoType() reflect.Type {
|
|
||||||
return reflect.TypeOf(interface{}(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator that considers any value valid
|
|
||||||
func (AnyType) Validator(typename string, avail ...Type) ValidateFunc {
|
|
||||||
if typename != "any" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) (interface{}, bool) {
|
|
||||||
return value, true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package validator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateFunc returns whether a given value fulfills the datatype and casts
|
|
||||||
// the value into a go type.
|
|
||||||
//
|
|
||||||
// for example, if a validator checks for upper case strings, whether the value
|
|
||||||
// is a []byte, a string or a []rune, if the value matches is all upper-case, it
|
|
||||||
// will be cast into a go type, say, string.
|
|
||||||
type ValidateFunc func(value interface{}) (cast interface{}, valid bool)
|
|
||||||
|
|
||||||
// Type defines an available in/out parameter "type" for the aicra configuration
|
|
||||||
//
|
|
||||||
// A Type maps to a go type in order to generate the handler signature from the
|
|
||||||
// aicra configuration
|
|
||||||
//
|
|
||||||
// A Type returns a custom validator when the typename matches
|
|
||||||
type Type interface {
|
|
||||||
// Validator function when the typename matches. It must return nil when the
|
|
||||||
// typename does not match
|
|
||||||
//
|
|
||||||
// The `typename` argument has to match types used in your aicra configuration
|
|
||||||
// in parameter definitions ("in", "out") and in the "type" json field.
|
|
||||||
//
|
|
||||||
// basic example:
|
|
||||||
// - `IntType.Validator("string")`` should return nil
|
|
||||||
// - `IntType.Validator("int")`` should return its ValidateFunc
|
|
||||||
//
|
|
||||||
// The `typename` is not returned by a simple method i.e. `TypeName() string`
|
|
||||||
// because it allows for validation relative to the typename, for instance:
|
|
||||||
// - `VarcharType.Validator("varchar")` valides any string
|
|
||||||
// - `VarcharType.Validator("varchar(2)")` validates any string of 2
|
|
||||||
// characters
|
|
||||||
// - `VarcharType.Validator("varchar(1,3)")` validates any string
|
|
||||||
// with a length between 1 and 3
|
|
||||||
//
|
|
||||||
// The `avail` argument represents all other available Types. It allows a
|
|
||||||
// Type to use other available Types internally.
|
|
||||||
//
|
|
||||||
// recursive example: slices
|
|
||||||
// - `SliceType.Validator("[]int", avail...)` validates a slice containing
|
|
||||||
// values that are valide to the `IntType`
|
|
||||||
// - `SliceType.Validator("[]varchar", avail...)` validates a slice containing
|
|
||||||
// values that are valid to the `VarcharType`
|
|
||||||
//
|
|
||||||
// and so on.. this works for maps, structs, etc
|
|
||||||
Validator(typename string, avail ...Type) ValidateFunc
|
|
||||||
|
|
||||||
// GoType must return the go type associated with the output type of ValidateFunc.
|
|
||||||
// It is used to define handlers' signature from the configuration file.
|
|
||||||
GoType() reflect.Type
|
|
||||||
}
|
|
Loading…
Reference in New Issue