Compare commits

..

No commits in common. "cc2599565959d011729077f1d4c2e7174b9b3127" and "418631e09dbe0ba69c1d245f2b5c43b7a7bd3a29" have entirely different histories.

14 changed files with 342 additions and 523 deletions

View File

@ -107,17 +107,12 @@ func main() {
log.Fatalf("invalid config: %s", err) log.Fatalf("invalid config: %s", err)
} }
// add http middlewares (logger)
builder.With(func(next http.Handler) http.Handler{ /* ... */ })
// add contextual middlewares (authentication)
builder.WithContext(func(next http.Handler) http.Handler{ /* ... */ })
// bind handlers // bind handlers
err = builder.Bind(http.MethodGet, "/user/{id}", getUserById) err = builder.Bind(http.MethodGet, "/user/{id}", getUserById)
if err != nil { if err != nil {
log.Fatalf("cannog bind GET /user/{id}: %s", err) log.Fatalf("cannog bind GET /user/{id}: %s", err)
} }
// ...
// build your services // build your services
handler, err := builder.Build() handler, err := builder.Build()
@ -266,7 +261,7 @@ type res struct{
Output2 bool Output2 bool
} }
func myHandler(ctx context.Context, r req) (*res, api.Err) { func myHandler(r req) (*res, api.Err) {
err := doSomething() err := doSomething()
if err != nil { if err != nil {
return nil, api.ErrFailure return nil, api.ErrFailure

13
api/adapter.go Normal file
View File

@ -0,0 +1,13 @@
package api
import "net/http"
// Adapter to encapsulate incoming requests
type Adapter func(http.HandlerFunc) http.HandlerFunc
// AuthHandlerFunc is http.HandlerFunc with additional Authorization information
type AuthHandlerFunc func(Auth, http.ResponseWriter, *http.Request)
// AuthAdapter to encapsulate incoming request with access to api.Auth
// to manage permissions
type AuthAdapter func(AuthHandlerFunc) AuthHandlerFunc

View File

@ -21,7 +21,7 @@ type Auth struct {
// Granted returns whether the authorization is granted // Granted returns whether the authorization is granted
// i.e. Auth.Active fulfills Auth.Required // i.e. Auth.Active fulfills Auth.Required
func (a *Auth) Granted() bool { func (a Auth) Granted() bool {
var nothingRequired = true var nothingRequired = true
// first dimension: OR ; at least one is valid // first dimension: OR ; at least one is valid
@ -43,7 +43,7 @@ func (a *Auth) Granted() bool {
} }
// returns whether Auth.Active fulfills (contains) all @required roles // returns whether Auth.Active fulfills (contains) all @required roles
func (a *Auth) fulfills(required []string) bool { func (a Auth) fulfills(required []string) bool {
for _, requiredRole := range required { for _, requiredRole := range required {
var found = false var found = false
for _, activeRole := range a.Active { for _, activeRole := range a.Active {

View File

@ -1,44 +1,17 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"git.xdrm.io/go/aicra/internal/ctx"
) )
// GetRequest extracts the current request from a context.Context // Ctx contains additional information for handlers
func GetRequest(c context.Context) *http.Request { //
var ( // usually input/output arguments built by aicra are sufficient
raw = c.Value(ctx.Request) // but the Ctx lets you manage your request from scratch if required
cast, ok = raw.(*http.Request) //
) // If required, set api.Ctx as the first argument of your handler; if you
if !ok { // don't need it, only use standard input arguments and it will be ignored
return nil type Ctx struct {
} Res http.ResponseWriter
return cast Req *http.Request
}
// GetResponseWriter extracts the response writer from a context.Context
func GetResponseWriter(c context.Context) http.ResponseWriter {
var (
raw = c.Value(ctx.Response)
cast, ok = raw.(http.ResponseWriter)
)
if !ok {
return nil
}
return cast
}
// GetAuth returns the api.Auth associated with this request from a context.Context
func GetAuth(c context.Context) *Auth {
var (
raw = c.Value(ctx.Auth)
cast, ok = raw.(*Auth)
)
if !ok {
return nil
}
return cast
} }

View File

@ -5,6 +5,7 @@ import (
"io" "io"
"net/http" "net/http"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype" "git.xdrm.io/go/aicra/datatype"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/dynfunc" "git.xdrm.io/go/aicra/internal/dynfunc"
@ -12,16 +13,10 @@ import (
// Builder for an aicra server // Builder for an aicra server
type Builder struct { type Builder struct {
// the server configuration defining available services conf *config.Server
conf *config.Server handlers []*apiHandler
// user-defined handlers bound to services from the configuration adapters []api.Adapter
handlers []*apiHandler authAdapters []api.AuthAdapter
// http middlewares wrapping the entire http connection (e.g. logger)
middlewares []func(http.Handler) http.Handler
// custom middlewares only wrapping the service handler of a request
// they will benefit from the request's context that contains service-specific
// information (e.g. required permisisons from the configuration)
ctxMiddlewares []func(http.Handler) http.Handler
} }
// represents an api handler (method-pattern combination) // represents an api handler (method-pattern combination)
@ -46,36 +41,26 @@ func (b *Builder) AddType(t datatype.T) error {
return nil return nil
} }
// With adds an http middleware on top of the http connection // With adds an http adapter (middleware)
// func (b *Builder) With(adapter api.Adapter) {
// Authentication management can only be done with the WithContext() methods as
// the service associated with the request has not been found at this stage.
// This stage is perfect for logging or generic request management.
func (b *Builder) With(mw func(http.Handler) http.Handler) {
if b.conf == nil { if b.conf == nil {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
if b.middlewares == nil { if b.adapters == nil {
b.middlewares = make([]func(http.Handler) http.Handler, 0) b.adapters = make([]api.Adapter, 0)
} }
b.middlewares = append(b.middlewares, mw) b.adapters = append(b.adapters, adapter)
} }
// WithContext adds an http middleware with the fully loaded context // WithAuth adds an http adapter with auth capabilities (middleware)
// func (b *Builder) WithAuth(adapter api.AuthAdapter) {
// Logging or generic request management should be done with the With() method as
// it wraps the full http connection. Middlewares added through this method only
// wrap the user-defined service handler. The context.Context is filled with useful
// data that can be access with api.GetRequest(), api.GetResponseWriter(),
// api.GetAuth(), etc methods.
func (b *Builder) WithContext(mw func(http.Handler) http.Handler) {
if b.conf == nil { if b.conf == nil {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
if b.ctxMiddlewares == nil { if b.authAdapters == nil {
b.ctxMiddlewares = make([]func(http.Handler) http.Handler, 0) b.authAdapters = make([]api.AuthAdapter, 0)
} }
b.ctxMiddlewares = append(b.ctxMiddlewares, mw) b.authAdapters = append(b.authAdapters, adapter)
} }
// Setup the builder with its api definition file // Setup the builder with its api definition file

View File

@ -1,7 +1,6 @@
package aicra package aicra
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"strings" "strings"
@ -73,7 +72,7 @@ func TestBind(t *testing.T) {
Config: "[]", Config: "[]",
HandlerMethod: "", HandlerMethod: "",
HandlerPath: "", HandlerPath: "",
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: nil, BuildErr: nil,
}, },
@ -109,7 +108,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodPost, HandlerMethod: http.MethodPost,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: errMissingHandler, BuildErr: errMissingHandler,
}, },
@ -127,7 +126,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/paths", HandlerPath: "/paths",
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: errMissingHandler, BuildErr: errMissingHandler,
}, },
@ -145,7 +144,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -165,7 +164,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context, struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -185,7 +184,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context, struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -205,7 +204,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context, struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -225,7 +224,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(context.Context, struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },

View File

@ -1,14 +1,12 @@
package aicra package aicra
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/ctx"
"git.xdrm.io/go/aicra/internal/reqdata" "git.xdrm.io/go/aicra/internal/reqdata"
) )
@ -17,31 +15,30 @@ type Handler Builder
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters) // ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h http.Handler = http.HandlerFunc(s.resolve) var h = http.HandlerFunc(s.resolve)
for _, mw := range s.middlewares { for _, adapter := range s.adapters {
h = mw(h) h = adapter(h)
} }
h.ServeHTTP(w, r) h(w, r)
} }
// 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) {
// 1ind a matching service from config // 1. find a matching service from config
var service = s.conf.Find(r) var service = s.conf.Find(r)
if service == nil { if service == nil {
handleError(api.ErrUnknownService, w, r) handleError(api.ErrUnknownService, w, r)
return return
} }
// extract request data // 2. extract request data
var input, err = extractInput(service, *r) var input, err = extractInput(service, *r)
if err != nil { if err != nil {
handleError(api.ErrMissingParam, w, r) handleError(api.ErrMissingParam, w, r)
return return
} }
// find a matching handler // 3. 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 {
@ -49,50 +46,59 @@ func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
} }
} }
// fail on no matching handler // 4. fail on no matching handler
if handler == nil { if handler == nil {
handleError(api.ErrUncallableService, w, r) handleError(api.ErrUncallableService, w, r)
return return
} }
// build context with builtin data // replace format '[a]' in scope where 'a' is an existing input's name
c := r.Context() scope := make([][]string, len(service.Scope))
c = context.WithValue(c, ctx.Request, r) for a, list := range service.Scope {
c = context.WithValue(c, ctx.Response, w) scope[a] = make([]string, len(list))
c = context.WithValue(c, ctx.Auth, buildAuth(service.Scope, input.Data)) for b, perm := range list {
scope[a][b] = perm
// create http handler for name, value := range input.Data {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var (
auth := api.GetAuth(r.Context()) token = fmt.Sprintf("[%s]", name)
if auth == nil { replacement = ""
handleError(api.ErrPermission, w, r) )
return if value != nil {
replacement = fmt.Sprintf("[%v]", value)
}
scope[a][b] = strings.ReplaceAll(scope[a][b], token, replacement)
}
} }
// reject non granted requests
if !auth.Granted() {
handleError(api.ErrPermission, w, r)
return
}
// use context defined in the request
s.handle(r.Context(), input, handler, service, w, r)
})
// run middlewares the handler
for _, mw := range s.ctxMiddlewares {
h = mw(h)
} }
// serve using the context with values var auth = api.Auth{
h.ServeHTTP(w, r.WithContext(c)) Required: scope,
Active: []string{},
}
// 5. run auth-aware middlewares
var h = api.AuthHandlerFunc(func(a api.Auth, w http.ResponseWriter, r *http.Request) {
if !a.Granted() {
handleError(api.ErrPermission, w, r)
return
}
s.handle(input, handler, service, w, r)
})
for _, adapter := range s.authAdapters {
h = adapter(h)
}
h(auth, w, r)
} }
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(input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
// pass execution to the handler // 5. pass execution to the handler
var outData, outErr = handler.dyn.Handle(c, input.Data) ctx := api.Ctx{Res: w, Req: r}
var outData, outErr = handler.dyn.Handle(ctx, input.Data)
// build response from returned arguments // 6. build res from returned data
var res = api.EmptyResponse().WithError(outErr) var res = api.EmptyResponse().WithError(outErr)
for key, value := range outData { for key, value := range outData {
@ -143,35 +149,3 @@ func extractInput(service *config.Service, req http.Request) (*reqdata.T, error)
return dataset, nil return dataset, nil
} }
// buildAuth builds the api.Auth struct from the service scope configuration
//
// it replaces format '[a]' in scope where 'a' is an existing input argument's
// name with its value
func buildAuth(scope [][]string, in map[string]interface{}) *api.Auth {
updated := make([][]string, len(scope))
// replace '[arg_name]' with the 'arg_name' value if it is a known variable
// name
for a, list := range scope {
updated[a] = make([]string, len(list))
for b, perm := range list {
updated[a][b] = perm
for name, value := range in {
var (
token = fmt.Sprintf("[%s]", name)
replacement = ""
)
if value != nil {
replacement = fmt.Sprintf("[%v]", value)
}
updated[a][b] = strings.ReplaceAll(updated[a][b], token, replacement)
}
}
}
return &api.Auth{
Required: updated,
Active: []string{},
}
}

View File

@ -3,7 +3,6 @@ package aicra_test
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -49,15 +48,15 @@ func TestWith(t *testing.T) {
type ckey int type ckey int
const key ckey = 0 const key ckey = 0
middleware := func(next http.Handler) http.Handler { middleware := func(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
newr := r 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 {
newr = r.WithContext(context.WithValue(r.Context(), key, int(1))) newr = r.WithContext(context.WithValue(r.Context(), key, int(1)))
next.ServeHTTP(w, newr) next(w, newr)
return return
} }
@ -68,8 +67,8 @@ func TestWith(t *testing.T) {
} }
cast++ cast++
newr = r.WithContext(context.WithValue(r.Context(), key, cast)) newr = r.WithContext(context.WithValue(r.Context(), key, cast))
next.ServeHTTP(w, newr) next(w, newr)
}) }
} }
// add middleware @n times // add middleware @n times
@ -83,9 +82,9 @@ func TestWith(t *testing.T) {
t.Fatalf("setup: unexpected error <%v>", err) t.Fatalf("setup: unexpected error <%v>", err)
} }
pathHandler := func(ctx context.Context) (*struct{}, api.Err) { pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) {
// write value from middlewares into response // write value from middlewares into response
value := ctx.Value(key) value := ctx.Req.Context().Value(key)
if value == nil { if value == nil {
t.Fatalf("nothing found in context") t.Fatalf("nothing found in context")
} }
@ -94,7 +93,7 @@ func TestWith(t *testing.T) {
t.Fatalf("cannot cast context data to int") t.Fatalf("cannot cast context data to int")
} }
// write to response // write to response
api.GetResponseWriter(ctx).Write([]byte(fmt.Sprintf("#%d#", cast))) ctx.Res.Write([]byte(fmt.Sprintf("#%d#", cast)))
return nil, api.ErrSuccess return nil, api.ErrSuccess
} }
@ -213,13 +212,8 @@ func TestWithAuth(t *testing.T) {
} }
// tester middleware (last executed) // tester middleware (last executed)
builder.WithContext(func(next http.Handler) http.Handler { builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
a := api.GetAuth(r.Context())
if a == nil {
t.Fatalf("cannot access api.Auth form request context")
}
if a.Granted() == tc.granted { if a.Granted() == tc.granted {
return return
} }
@ -228,20 +222,14 @@ func TestWithAuth(t *testing.T) {
} else { } else {
t.Fatalf("expected granted auth") t.Fatalf("expected granted auth")
} }
next.ServeHTTP(w, r) }
})
}) })
builder.WithContext(func(next http.Handler) http.Handler { builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
a := api.GetAuth(r.Context())
if a == nil {
t.Fatalf("cannot access api.Auth form request context")
}
a.Active = tc.permissions a.Active = tc.permissions
next.ServeHTTP(w, r) next(a, w, r)
}) }
}) })
err := builder.Setup(strings.NewReader(tc.manifest)) err := builder.Setup(strings.NewReader(tc.manifest))
@ -249,7 +237,7 @@ func TestWithAuth(t *testing.T) {
t.Fatalf("setup: unexpected error <%v>", err) t.Fatalf("setup: unexpected error <%v>", err)
} }
pathHandler := func(ctx context.Context) (*struct{}, api.Err) { pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) {
return nil, api.ErrNotImplemented return nil, api.ErrNotImplemented
} }
@ -276,97 +264,6 @@ func TestWithAuth(t *testing.T) {
} }
func TestPermissionError(t *testing.T) {
tt := []struct {
name string
manifest string
permissions []string
granted bool
}{
{
name: "permission fulfilled",
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`,
permissions: []string{"A"},
granted: true,
},
{
name: "missing permission",
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`,
permissions: []string{},
granted: false,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
builder := &aicra.Builder{}
if err := addBuiltinTypes(builder); err != nil {
t.Fatalf("unexpected error <%v>", err)
}
// add active permissions
builder.WithContext(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a := api.GetAuth(r.Context())
if a == nil {
t.Fatalf("cannot access api.Auth form request context")
}
a.Active = tc.permissions
next.ServeHTTP(w, r)
})
})
err := builder.Setup(strings.NewReader(tc.manifest))
if err != nil {
t.Fatalf("setup: unexpected error <%v>", err)
}
pathHandler := func(ctx context.Context) (*struct{}, api.Err) {
return nil, api.ErrNotImplemented
}
if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil {
t.Fatalf("bind: unexpected error <%v>", err)
}
handler, err := builder.Build()
if err != nil {
t.Fatalf("build: unexpected error <%v>", err)
}
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{})
// test request
handler.ServeHTTP(response, request)
if response.Body == nil {
t.Fatalf("response has no body")
}
type jsonResponse struct {
Err api.Err `json:"error"`
}
var res jsonResponse
err = json.Unmarshal(response.Body.Bytes(), &res)
if err != nil {
t.Fatalf("cannot unmarshal response: %s", err)
}
expectedError := api.ErrNotImplemented
if !tc.granted {
expectedError = api.ErrPermission
}
if res.Err.Code != expectedError.Code {
t.Fatalf("expected error code %d got %d", expectedError.Code, res.Err.Code)
}
})
}
}
func TestDynamicScope(t *testing.T) { func TestDynamicScope(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
@ -393,7 +290,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/123", url: "/path/123",
body: ``, body: ``,
permissions: []string{"user[123]"}, permissions: []string{"user[123]"},
@ -414,7 +311,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/666", url: "/path/666",
body: ``, body: ``,
permissions: []string{"user[123]"}, permissions: []string{"user[123]"},
@ -435,7 +332,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(context.Context, struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/123", url: "/path/123",
body: ``, body: ``,
permissions: []string{"prefix.user[123].suffix"}, permissions: []string{"prefix.user[123].suffix"},
@ -457,7 +354,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}", path: "/prefix/{pid}/user/{uid}",
handler: func(context.Context, struct { handler: func(struct {
Prefix uint Prefix uint
User uint User uint
}) (*struct{}, api.Err) { }) (*struct{}, api.Err) {
@ -484,7 +381,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}", path: "/prefix/{pid}/user/{uid}",
handler: func(context.Context, struct { handler: func(struct {
Prefix uint Prefix uint
User uint User uint
}) (*struct{}, api.Err) { }) (*struct{}, api.Err) {
@ -512,7 +409,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}/suffix/{sid}", path: "/prefix/{pid}/user/{uid}/suffix/{sid}",
handler: func(context.Context, struct { handler: func(struct {
Prefix uint Prefix uint
User uint User uint
Suffix uint Suffix uint
@ -541,7 +438,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}/suffix/{sid}", path: "/prefix/{pid}/user/{uid}/suffix/{sid}",
handler: func(context.Context, struct { handler: func(struct {
Prefix uint Prefix uint
User uint User uint
Suffix uint Suffix uint
@ -563,12 +460,8 @@ func TestDynamicScope(t *testing.T) {
} }
// tester middleware (last executed) // tester middleware (last executed)
builder.WithContext(func(next http.Handler) http.Handler { builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
a := api.GetAuth(r.Context())
if a == nil {
t.Fatalf("cannot access api.Auth form request context")
}
if a.Granted() == tc.granted { if a.Granted() == tc.granted {
return return
} }
@ -577,20 +470,15 @@ func TestDynamicScope(t *testing.T) {
} else { } else {
t.Fatalf("expected granted auth") t.Fatalf("expected granted auth")
} }
next.ServeHTTP(w, r) }
})
}) })
// update permissions // update permissions
builder.WithContext(func(next http.Handler) http.Handler { builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
a := api.GetAuth(r.Context())
if a == nil {
t.Fatalf("cannot access api.Auth form request context")
}
a.Active = tc.permissions a.Active = tc.permissions
next.ServeHTTP(w, r) next(a, w, r)
}) }
}) })
err := builder.Setup(strings.NewReader(tc.manifest)) err := builder.Setup(strings.NewReader(tc.manifest))

View File

@ -1,13 +0,0 @@
package ctx
// Key defines a custom context key type
type Key int
const (
// Request is the key for the current *http.Request
Request Key = iota
// Response is the key for the associated http.ResponseWriter
Response
// Auth is the key for the request's authentication information
Auth
)

View File

@ -14,19 +14,16 @@ const errHandlerNotFunc = cerr("handler must be a func")
const errNoServiceForHandler = cerr("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
const errMissingHandlerContextArgument = cerr("missing handler first argument of type context.Context") const errMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
// errMissingHandlerInputArgument - missing params arguments for handler
const errMissingHandlerInputArgument = cerr("missing handler argument: input struct")
// errUnexpectedInput - input argument is not expected // errUnexpectedInput - input argument is not expected
const errUnexpectedInput = cerr("unexpected input struct") const errUnexpectedInput = cerr("unexpected input struct")
// errMissingHandlerOutputArgument - missing output for handler // errMissingHandlerOutput - missing output for handler
const errMissingHandlerOutputArgument = cerr("missing handler first output argument: output struct") const errMissingHandlerOutput = cerr("handler must have at least 1 output")
// errMissingHandlerOutputError - missing error output for handler // errMissingHandlerOutputError - missing error output for handler
const errMissingHandlerOutputError = cerr("missing handler last output argument of type api.Err") const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Err")
// errMissingRequestArgument - missing request argument for handler // errMissingRequestArgument - missing request argument for handler
const errMissingRequestArgument = cerr("handler first argument must be of type api.Request") const errMissingRequestArgument = cerr("handler first argument must be of type api.Request")
@ -37,14 +34,17 @@ const errMissingParamArgument = cerr("handler second argument must be a struct")
// errUnexportedName - argument is unexported in struct // errUnexportedName - argument is unexported in struct
const errUnexportedName = cerr("unexported name") const errUnexportedName = cerr("unexported name")
// errWrongOutputArgumentType - wrong type for output first argument // errMissingParamOutput - missing output argument for handler
const errWrongOutputArgumentType = cerr("handler first output argument must be a *struct") const errMissingParamOutput = cerr("handler first output must be a *struct")
// errMissingConfigArgument - missing an input/output argument in handler struct // errMissingParamFromConfig - missing a parameter in handler struct
const errMissingConfigArgument = cerr("missing an argument from the configuration") const errMissingParamFromConfig = cerr("missing a parameter from configuration")
// errMissingOutputFromConfig - missing a parameter in handler struct
const errMissingOutputFromConfig = cerr("missing a parameter from configuration")
// errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct // errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
const errWrongParamTypeFromConfig = cerr("invalid struct field type") const errWrongParamTypeFromConfig = cerr("invalid struct field type")
// errMissingHandlerErrorArgument - missing handler output error // errMissingHandlerErrorOutput - missing handler output error
const errMissingHandlerErrorArgument = cerr("last output must be of type api.Err") const errMissingHandlerErrorOutput = cerr("last output must be of type api.Err")

View File

@ -1,7 +1,6 @@
package dynfunc package dynfunc
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"reflect" "reflect"
@ -10,69 +9,73 @@ import (
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
) )
// Handler represents a dynamic aicra service handler // Handler represents a dynamic api handler
type Handler struct { type Handler struct {
// signature defined from the service configuration spec *signature
signature *Signature fn interface{}
// fn provided function that will be the service's handler // whether fn uses api.Ctx as 1st argument
fn interface{} hasContext bool
// index in input arguments where the data struct must be
dataIndex int
} }
// Build a handler from a dynamic function and checks its signature against a // Build a handler from a service configuration and a dynamic function
// service configuration //
//e // @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Err)`
// `fn` must have as a signature : `func(*api.Context, in) (*out, api.Err)` // - `inputStruct` 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) // - `outputStruct` 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 // - a first optional input parameter of type `api.Ctx` can be added
// - it there is no output, `out` MUST be omitted // - it there is no input, `inputStruct` must be omitted
// - it there is no output, `outputStruct` must be omitted
func Build(fn interface{}, service config.Service) (*Handler, error) { func Build(fn interface{}, service config.Service) (*Handler, error) {
var ( h := &Handler{
h = &Handler{ spec: signatureFromService(service),
signature: BuildSignature(service), fn: fn,
fn: fn, }
}
fnType = reflect.TypeOf(fn)
)
if fnType.Kind() != reflect.Func { impl := reflect.TypeOf(fn)
if impl.Kind() != reflect.Func {
return nil, errHandlerNotFunc return nil, errHandlerNotFunc
} }
if err := h.signature.ValidateInput(fnType); err != nil {
h.hasContext = impl.NumIn() >= 1 && reflect.TypeOf(api.Ctx{}).AssignableTo(impl.In(0))
if h.hasContext {
h.dataIndex = 1
}
if err := h.spec.checkInput(impl, h.dataIndex); err != nil {
return nil, fmt.Errorf("input: %w", err) return nil, fmt.Errorf("input: %w", err)
} }
if err := h.signature.ValidateOutput(fnType); err != nil { if err := h.spec.checkOutput(impl); err != nil {
return nil, fmt.Errorf("output: %w", err) return nil, fmt.Errorf("output: %w", err)
} }
return h, nil return h, nil
} }
// Handle binds input `data` into the dynamic function and returns an output map // Handle binds input @data into the dynamic function and returns map output
func (h *Handler) Handle(ctx context.Context, data map[string]interface{}) (map[string]interface{}, api.Err) { func (h *Handler) Handle(ctx api.Ctx, data map[string]interface{}) (map[string]interface{}, api.Err) {
var ( var ert = reflect.TypeOf(api.Err{})
ert = reflect.TypeOf(api.Err{}) var fnv = reflect.ValueOf(h.fn)
fnv = reflect.ValueOf(h.fn)
callArgs = make([]reflect.Value, 0)
)
// bind context callArgs := []reflect.Value{}
callArgs = append(callArgs, reflect.ValueOf(ctx))
inputStructRequired := fnv.Type().NumIn() > 1 // bind context if used in handler
if h.hasContext {
callArgs = append(callArgs, reflect.ValueOf(ctx))
}
// bind input arguments // bind input data
if inputStructRequired { if fnv.Type().NumIn() > h.dataIndex {
// create zero value struct // create zero value struct
var ( callStructPtr := reflect.New(fnv.Type().In(0))
callStructPtr = reflect.New(fnv.Type().In(1)) callStruct := callStructPtr.Elem()
callStruct = callStructPtr.Elem()
)
// set each field // set each field
for name := range h.signature.Input { for name := range h.spec.Input {
field := callStruct.FieldByName(name) field := callStruct.FieldByName(name)
if !field.CanSet() { if !field.CanSet() {
continue continue
@ -112,12 +115,12 @@ func (h *Handler) Handle(ctx context.Context, data map[string]interface{}) (map[
callArgs = append(callArgs, callStruct) callArgs = append(callArgs, callStruct)
} }
// call the handler // call the HandlerFn
output := fnv.Call(callArgs) output := fnv.Call(callArgs)
// no output OR pointer to output struct is nil // no output OR pointer to output struct is nil
outdata := make(map[string]interface{}) outdata := make(map[string]interface{})
if len(h.signature.Output) < 1 || output[0].IsNil() { if len(h.spec.Output) < 1 || output[0].IsNil() {
var structerr = output[len(output)-1].Convert(ert) var structerr = output[len(output)-1].Convert(ert)
return outdata, api.Err{ return outdata, api.Err{
Code: int(structerr.FieldByName("Code").Int()), Code: int(structerr.FieldByName("Code").Int()),
@ -129,7 +132,7 @@ func (h *Handler) Handle(ctx context.Context, data map[string]interface{}) (map[
// extract struct from pointer // extract struct from pointer
returnStruct := output[0].Elem() returnStruct := output[0].Elem()
for name := range h.signature.Output { for name := range h.spec.Output {
field := returnStruct.FieldByName(name) field := returnStruct.FieldByName(name)
outdata[name] = field.Interface() outdata[name] = field.Interface()
} }

View File

@ -1,7 +1,6 @@
package dynfunc package dynfunc
import ( import (
"context"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
@ -9,7 +8,7 @@ import (
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
) )
type testsignature Signature type testsignature signature
// builds a mock service with provided arguments as Input and matched as Output // builds a mock service with provided arguments as Input and matched as Output
func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature { func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature {
@ -53,7 +52,7 @@ func TestInput(t *testing.T) {
{ {
Name: "none required none provided", Name: "none required none provided",
Spec: (&testsignature{}).withArgs(), Spec: (&testsignature{}).withArgs(),
Fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
HasContext: false, HasContext: false,
Input: []interface{}{}, Input: []interface{}{},
ExpectedOutput: []interface{}{}, ExpectedOutput: []interface{}{},
@ -62,7 +61,7 @@ func TestInput(t *testing.T) {
{ {
Name: "int proxy (0)", Name: "int proxy (0)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
Fn: func(ctx context.Context, in intstruct) (*intstruct, api.Err) { Fn: func(in intstruct) (*intstruct, api.Err) {
return &intstruct{P1: in.P1}, api.ErrSuccess return &intstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -73,7 +72,7 @@ func TestInput(t *testing.T) {
{ {
Name: "int proxy (11)", Name: "int proxy (11)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
Fn: func(ctx context.Context, in intstruct) (*intstruct, api.Err) { Fn: func(in intstruct) (*intstruct, api.Err) {
return &intstruct{P1: in.P1}, api.ErrSuccess return &intstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -84,7 +83,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (nil)", Name: "*int proxy (nil)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(ctx context.Context, in intptrstruct) (*intptrstruct, api.Err) { Fn: func(in intptrstruct) (*intptrstruct, api.Err) {
return &intptrstruct{P1: in.P1}, api.ErrSuccess return &intptrstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -95,7 +94,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (28)", Name: "*int proxy (28)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(ctx context.Context, in intptrstruct) (*intstruct, api.Err) { Fn: func(in intptrstruct) (*intstruct, api.Err) {
return &intstruct{P1: *in.P1}, api.ErrSuccess return &intstruct{P1: *in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -106,7 +105,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (13)", Name: "*int proxy (13)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(ctx context.Context, in intptrstruct) (*intstruct, api.Err) { Fn: func(in intptrstruct) (*intstruct, api.Err) {
return &intstruct{P1: *in.P1}, api.ErrSuccess return &intstruct{P1: *in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -120,9 +119,16 @@ func TestInput(t *testing.T) {
t.Run(tcase.Name, func(t *testing.T) { t.Run(tcase.Name, func(t *testing.T) {
t.Parallel() t.Parallel()
var dataIndex = 0
if tcase.HasContext {
dataIndex = 1
}
var handler = &Handler{ var handler = &Handler{
signature: &Signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output}, spec: &signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output},
fn: tcase.Fn, fn: tcase.Fn,
dataIndex: dataIndex,
hasContext: tcase.HasContext,
} }
// build input // build input
@ -132,7 +138,7 @@ func TestInput(t *testing.T) {
input[key] = val input[key] = val
} }
var output, err = handler.Handle(context.Background(), input) var output, err = handler.Handle(api.Ctx{}, input)
if err != tcase.ExpectedErr { if err != tcase.ExpectedErr {
t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err) t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err)
} }

View File

@ -1,7 +1,6 @@
package dynfunc package dynfunc
import ( import (
"context"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
@ -10,17 +9,15 @@ import (
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
) )
// Signature represents input/output arguments for service from the aicra configuration // signature represents input/output arguments for a dynamic function
type Signature struct { type signature struct {
// Input arguments of the service Input map[string]reflect.Type
Input map[string]reflect.Type
// Output arguments of the service
Output map[string]reflect.Type Output map[string]reflect.Type
} }
// BuildSignature builds a signature for a service configuration // builds a spec from the configuration service
func BuildSignature(service config.Service) *Signature { func signatureFromService(service config.Service) *signature {
s := &Signature{ s := &signature{
Input: make(map[string]reflect.Type), Input: make(map[string]reflect.Type),
Output: make(map[string]reflect.Type), Output: make(map[string]reflect.Type),
} }
@ -47,37 +44,31 @@ func BuildSignature(service config.Service) *Signature {
return s return s
} }
// ValidateInput validates a handler's input arguments against the service signature // checks for HandlerFn input arguments
func (s *Signature) ValidateInput(handlerType reflect.Type) error { func (s *signature) checkInput(impl reflect.Type, index int) error {
ctxType := reflect.TypeOf((*context.Context)(nil)).Elem() var requiredInput, structIndex = index, index
if len(s.Input) > 0 { // arguments struct
// missing or invalid first arg: context.Context requiredInput++
if handlerType.NumIn() < 1 {
return errMissingHandlerContextArgument
}
firstArgType := handlerType.In(0)
if !firstArgType.Implements(ctxType) {
return fmt.Errorf("fock")
} }
// no input required // missing arguments
if impl.NumIn() > requiredInput {
return errUnexpectedInput
}
// none required
if len(s.Input) == 0 { if len(s.Input) == 0 {
// input struct provided
if handlerType.NumIn() > 1 {
return errUnexpectedInput
}
return nil return nil
} }
// too much arguments // too much arguments
if handlerType.NumIn() > 2 { if impl.NumIn() != requiredInput {
return errMissingHandlerInputArgument return errMissingHandlerArgumentParam
} }
// arg must be a struct // arg must be a struct
inStruct := handlerType.In(1) structArg := impl.In(structIndex)
if inStruct.Kind() != reflect.Struct { if structArg.Kind() != reflect.Struct {
return errMissingParamArgument return errMissingParamArgument
} }
@ -87,9 +78,9 @@ func (s *Signature) ValidateInput(handlerType reflect.Type) error {
return fmt.Errorf("%s: %w", name, errUnexportedName) return fmt.Errorf("%s: %w", name, errUnexportedName)
} }
field, exists := inStruct.FieldByName(name) field, exists := structArg.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, errMissingConfigArgument) return fmt.Errorf("%s: %w", name, errMissingParamFromConfig)
} }
if !ptype.AssignableTo(field.Type) { if !ptype.AssignableTo(field.Type) {
@ -100,18 +91,16 @@ func (s *Signature) ValidateInput(handlerType reflect.Type) error {
return nil return nil
} }
// ValidateOutput validates a handler's output arguments against the service signature // checks for HandlerFn output arguments
func (s Signature) ValidateOutput(handlerType reflect.Type) error { func (s signature) checkOutput(impl reflect.Type) error {
errType := reflect.TypeOf(api.ErrUnknown) if impl.NumOut() < 1 {
return errMissingHandlerOutput
if handlerType.NumOut() < 1 {
return errMissingHandlerErrorArgument
} }
// last output must be api.Err // last output must be api.Err
lastArgType := handlerType.Out(handlerType.NumOut() - 1) errOutput := impl.Out(impl.NumOut() - 1)
if !lastArgType.AssignableTo(errType) { if !errOutput.AssignableTo(reflect.TypeOf(api.ErrUnknown)) {
return errMissingHandlerErrorArgument return errMissingHandlerErrorOutput
} }
// no output -> ok // no output -> ok
@ -119,19 +108,19 @@ func (s Signature) ValidateOutput(handlerType reflect.Type) error {
return nil return nil
} }
if handlerType.NumOut() < 2 { if impl.NumOut() != 2 {
return errMissingHandlerOutputArgument return errMissingParamOutput
} }
// fail if first output is not a pointer to struct // fail if first output is not a pointer to struct
outStructPtr := handlerType.Out(0) structOutputPtr := impl.Out(0)
if outStructPtr.Kind() != reflect.Ptr { if structOutputPtr.Kind() != reflect.Ptr {
return errWrongOutputArgumentType return errMissingParamOutput
} }
outStruct := outStructPtr.Elem() structOutput := structOutputPtr.Elem()
if outStruct.Kind() != reflect.Struct { if structOutput.Kind() != reflect.Struct {
return errWrongOutputArgumentType return errMissingParamOutput
} }
// fail on invalid output // fail on invalid output
@ -140,9 +129,9 @@ func (s Signature) ValidateOutput(handlerType reflect.Type) error {
return fmt.Errorf("%s: %w", name, errUnexportedName) return fmt.Errorf("%s: %w", name, errUnexportedName)
} }
field, exists := outStruct.FieldByName(name) field, exists := structOutput.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, errMissingConfigArgument) return fmt.Errorf("%s: %w", name, errMissingOutputFromConfig)
} }
// ignore types evalutating to nil // ignore types evalutating to nil

View File

@ -1,7 +1,6 @@
package dynfunc package dynfunc
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
@ -21,22 +20,22 @@ func TestInputCheck(t *testing.T) {
{ {
Name: "no input 0 given", Name: "no input 0 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(context.Context) {}, Fn: func() {},
FnCtx: func(context.Context) {}, FnCtx: func(api.Ctx) {},
Err: nil, Err: nil,
}, },
{ {
Name: "no input 1 given", Name: "no input 1 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(context.Context, int) {}, Fn: func(int) {},
FnCtx: func(context.Context, int) {}, FnCtx: func(api.Ctx, int) {},
Err: errUnexpectedInput, Err: errUnexpectedInput,
}, },
{ {
Name: "no input 2 given", Name: "no input 2 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(context.Context, int, string) {}, Fn: func(int, string) {},
FnCtx: func(context.Context, int, string) {}, FnCtx: func(api.Ctx, int, string) {},
Err: errUnexpectedInput, Err: errUnexpectedInput,
}, },
{ {
@ -44,17 +43,17 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(context.Context) {}, Fn: func() {},
FnCtx: func(context.Context) {}, FnCtx: func(api.Ctx) {},
Err: errMissingHandlerInputArgument, Err: errMissingHandlerArgumentParam,
}, },
{ {
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(int) {},
FnCtx: func(context.Context, int) {}, FnCtx: func(api.Ctx, int) {},
Err: errMissingParamArgument, Err: errMissingParamArgument,
}, },
{ {
@ -62,8 +61,8 @@ func TestInputCheck(t *testing.T) {
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(struct{}) {},
FnCtx: func(context.Context, struct{}) {}, FnCtx: func(api.Ctx, struct{}) {},
Err: errUnexportedName, Err: errUnexportedName,
}, },
{ {
@ -71,17 +70,17 @@ func TestInputCheck(t *testing.T) {
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(struct{}) {},
FnCtx: func(context.Context, struct{}) {}, FnCtx: func(api.Ctx, struct{}) {},
Err: errMissingConfigArgument, Err: errMissingParamFromConfig,
}, },
{ {
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(struct{ Test1 string }) {},
FnCtx: func(context.Context, struct{ Test1 string }) {}, FnCtx: func(api.Ctx, struct{ Test1 string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -89,8 +88,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 int }) {},
FnCtx: func(context.Context, struct{ Test1 int }) {}, FnCtx: func(api.Ctx, struct{ Test1 int }) {},
Err: nil, Err: nil,
}, },
{ {
@ -98,17 +97,17 @@ func TestInputCheck(t *testing.T) {
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(struct{}) {},
FnCtx: func(context.Context, struct{}) {}, FnCtx: func(api.Ctx, struct{}) {},
Err: errMissingConfigArgument, Err: errMissingParamFromConfig,
}, },
{ {
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(struct{ Test1 string }) {},
FnCtx: func(context.Context, struct{ Test1 string }) {}, FnCtx: func(api.Ctx, struct{ Test1 string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -116,8 +115,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *string }) {},
FnCtx: func(context.Context, struct{ Test1 *string }) {}, FnCtx: func(api.Ctx, struct{ Test1 *string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -125,8 +124,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *int }) {},
FnCtx: func(context.Context, struct{ Test1 *int }) {}, FnCtx: func(api.Ctx, struct{ Test1 *int }) {},
Err: nil, Err: nil,
}, },
{ {
@ -134,8 +133,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 string }) {},
FnCtx: func(context.Context, struct{ Test1 string }) {}, FnCtx: func(api.Ctx, struct{ Test1 string }) {},
Err: nil, Err: nil,
}, },
{ {
@ -143,8 +142,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 uint }) {},
FnCtx: func(context.Context, struct{ Test1 uint }) {}, FnCtx: func(api.Ctx, struct{ Test1 uint }) {},
Err: nil, Err: nil,
}, },
{ {
@ -152,8 +151,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 float64 }) {},
FnCtx: func(context.Context, struct{ Test1 float64 }) {}, FnCtx: func(api.Ctx, struct{ Test1 float64 }) {},
Err: nil, Err: nil,
}, },
{ {
@ -161,8 +160,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 []byte }) {},
FnCtx: func(context.Context, struct{ Test1 []byte }) {}, FnCtx: func(api.Ctx, struct{ Test1 []byte }) {},
Err: nil, Err: nil,
}, },
{ {
@ -170,8 +169,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 []rune }) {},
FnCtx: func(context.Context, struct{ Test1 []rune }) {}, FnCtx: func(api.Ctx, struct{ Test1 []rune }) {},
Err: nil, Err: nil,
}, },
{ {
@ -179,8 +178,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *string }) {},
FnCtx: func(context.Context, struct{ Test1 *string }) {}, FnCtx: func(api.Ctx, struct{ Test1 *string }) {},
Err: nil, Err: nil,
}, },
{ {
@ -188,8 +187,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *uint }) {},
FnCtx: func(context.Context, struct{ Test1 *uint }) {}, FnCtx: func(api.Ctx, struct{ Test1 *uint }) {},
Err: nil, Err: nil,
}, },
{ {
@ -197,8 +196,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *float64 }) {},
FnCtx: func(context.Context, struct{ Test1 *float64 }) {}, FnCtx: func(api.Ctx, struct{ Test1 *float64 }) {},
Err: nil, Err: nil,
}, },
{ {
@ -206,8 +205,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *[]byte }) {},
FnCtx: func(context.Context, struct{ Test1 *[]byte }) {}, FnCtx: func(api.Ctx, struct{ Test1 *[]byte }) {},
Err: nil, Err: nil,
}, },
{ {
@ -215,8 +214,8 @@ func TestInputCheck(t *testing.T) {
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(struct{ Test1 *[]rune }) {},
FnCtx: func(context.Context, struct{ Test1 *[]rune }) {}, FnCtx: func(api.Ctx, struct{ Test1 *[]rune }) {},
Err: nil, Err: nil,
}, },
} }
@ -226,27 +225,47 @@ func TestInputCheck(t *testing.T) {
t.Parallel() t.Parallel()
// mock spec // mock spec
s := Signature{ s := signature{
Input: tcase.Input, Input: tcase.Input,
Output: nil, Output: nil,
} }
err := s.ValidateInput(reflect.TypeOf(tcase.FnCtx)) t.Run("with-context", func(t *testing.T) {
if err == nil && tcase.Err != nil { err := s.checkInput(reflect.TypeOf(tcase.FnCtx), 1)
t.Errorf("expected an error: '%s'", tcase.Err.Error()) if err == nil && tcase.Err != nil {
t.FailNow() t.Errorf("expected an error: '%s'", tcase.Err.Error())
}
if err != nil && tcase.Err == nil {
t.Errorf("unexpected error: '%s'", err.Error())
t.FailNow()
}
if err != nil && tcase.Err != nil {
if !errors.Is(err, tcase.Err) {
t.Errorf("expected the error <%s> got <%s>", tcase.Err, err)
t.FailNow() t.FailNow()
} }
} if err != nil && tcase.Err == nil {
t.Errorf("unexpected error: '%s'", err.Error())
t.FailNow()
}
if err != nil && tcase.Err != nil {
if !errors.Is(err, tcase.Err) {
t.Errorf("expected the error <%s> got <%s>", tcase.Err, err)
t.FailNow()
}
}
})
t.Run("without-context", func(t *testing.T) {
err := s.checkInput(reflect.TypeOf(tcase.Fn), 0)
if err == nil && tcase.Err != nil {
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()
}
if err != nil && tcase.Err != nil {
if !errors.Is(err, tcase.Err) {
t.Errorf("expected the error <%s> got <%s>", tcase.Err, err)
t.FailNow()
}
}
})
}) })
} }
} }
@ -260,37 +279,25 @@ func TestOutputCheck(t *testing.T) {
// no input -> missing api.Err // no input -> missing api.Err
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func(context.Context) {}, Fn: func() {},
Err: errMissingHandlerOutputArgument, Err: errMissingHandlerOutput,
}, },
// no input -> with last type not api.Err // no input -> with last type not api.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: errMissingHandlerErrorOutput,
}, },
// no input -> with api.Err // no input -> with api.Err
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func(context.Context) api.Err { return api.ErrSuccess }, Fn: func() api.Err { return api.ErrSuccess },
Err: nil, Err: nil,
}, },
// no input -> missing context.Context
{
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 // func can have output if not specified
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess }, Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
Err: nil, Err: nil,
}, },
// missing output struct in func // missing output struct in func
@ -299,7 +306,7 @@ func TestOutputCheck(t *testing.T) {
"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: errWrongOutputArgumentType, Err: errMissingParamOutput,
}, },
// output not a pointer // output not a pointer
{ {
@ -307,7 +314,7 @@ func TestOutputCheck(t *testing.T) {
"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: errMissingParamOutput,
}, },
// output not a pointer to struct // output not a pointer to struct
{ {
@ -315,7 +322,7 @@ func TestOutputCheck(t *testing.T) {
"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: errMissingParamOutput,
}, },
// unexported param name // unexported param name
{ {
@ -331,7 +338,7 @@ func TestOutputCheck(t *testing.T) {
"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: errMissingParamFromConfig,
}, },
// output field invalid type // output field invalid type
{ {
@ -364,12 +371,12 @@ func TestOutputCheck(t *testing.T) {
t.Parallel() t.Parallel()
// mock spec // mock spec
s := Signature{ s := signature{
Input: nil, Input: nil,
Output: tcase.Output, Output: tcase.Output,
} }
err := s.ValidateOutput(reflect.TypeOf(tcase.Fn)) err := s.checkOutput(reflect.TypeOf(tcase.Fn))
if err == nil && tcase.Err != nil { if err == nil && tcase.Err != nil {
t.Errorf("expected an error: '%s'", tcase.Err.Error()) t.Errorf("expected an error: '%s'", tcase.Err.Error())
t.FailNow() t.FailNow()