Merge branch 'test/dynamic' of go/aicra into 0.3.0
test dynfunc package; standardize and refactor api
This commit is contained in:
commit
6319761731
|
@ -22,6 +22,18 @@ var (
|
||||||
// ErrorConfig has to be set when there is a configuration error
|
// ErrorConfig has to be set when there is a configuration error
|
||||||
ErrorConfig Error = 4
|
ErrorConfig Error = 4
|
||||||
|
|
||||||
|
// ErrorCreation has to be set when there is a creation/insert error
|
||||||
|
ErrorCreation Error = 5
|
||||||
|
|
||||||
|
// ErrorModification has to be set when there is an update/modification error
|
||||||
|
ErrorModification Error = 6
|
||||||
|
|
||||||
|
// ErrorDeletion has to be set when there is a deletion/removal error
|
||||||
|
ErrorDeletion Error = 7
|
||||||
|
|
||||||
|
// ErrorTransaction has to be set when there is a transactional error
|
||||||
|
ErrorTransaction Error = 8
|
||||||
|
|
||||||
// ErrorUpload has to be set when a file upload failed
|
// ErrorUpload has to be set when a file upload failed
|
||||||
ErrorUpload Error = 100
|
ErrorUpload Error = 100
|
||||||
|
|
||||||
|
@ -79,6 +91,10 @@ var errorReasons = map[Error]string{
|
||||||
ErrorNoMatchFound: "resource not found",
|
ErrorNoMatchFound: "resource not found",
|
||||||
ErrorAlreadyExists: "already exists",
|
ErrorAlreadyExists: "already exists",
|
||||||
ErrorConfig: "configuration error",
|
ErrorConfig: "configuration error",
|
||||||
|
ErrorCreation: "create error",
|
||||||
|
ErrorModification: "update error",
|
||||||
|
ErrorDeletion: "delete error",
|
||||||
|
ErrorTransaction: "transactional error",
|
||||||
ErrorUpload: "upload failed",
|
ErrorUpload: "upload failed",
|
||||||
ErrorDownload: "download failed",
|
ErrorDownload: "download failed",
|
||||||
MissingDownloadHeaders: "download headers are missing",
|
MissingDownloadHeaders: "download headers are missing",
|
||||||
|
|
|
@ -22,21 +22,16 @@ type Request struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequest builds an interface request from a http.Request
|
// NewRequest builds an interface request from a http.Request
|
||||||
func NewRequest(req *http.Request) (*Request, error) {
|
func NewRequest(req *http.Request) *Request {
|
||||||
|
|
||||||
// 1. get useful data
|
|
||||||
uri := normaliseURI(req.URL.Path)
|
uri := normaliseURI(req.URL.Path)
|
||||||
uriparts := strings.Split(uri, "/")
|
uriparts := strings.Split(uri, "/")
|
||||||
|
|
||||||
// 3. Init request
|
return &Request{
|
||||||
inst := &Request{
|
|
||||||
URI: uriparts,
|
URI: uriparts,
|
||||||
Scope: nil,
|
Scope: nil,
|
||||||
Request: req,
|
Request: req,
|
||||||
Param: make(RequestParam),
|
Param: make(RequestParam),
|
||||||
}
|
}
|
||||||
|
|
||||||
return inst, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// normaliseURI removes the trailing '/' to always
|
// normaliseURI removes the trailing '/' to always
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
"git.xdrm.io/go/aicra/internal/dynfunc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder for an aicra server
|
||||||
|
type Builder struct {
|
||||||
|
conf *config.Server
|
||||||
|
handlers []*apiHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// represents an server handler
|
||||||
|
type apiHandler struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
dyn *dynfunc.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddType adds an available datatype to the api definition
|
||||||
|
func (b *Builder) AddType(t datatype.T) {
|
||||||
|
if b.conf == nil {
|
||||||
|
b.conf = &config.Server{}
|
||||||
|
}
|
||||||
|
if b.conf.Services != nil {
|
||||||
|
panic(ErrLateType)
|
||||||
|
}
|
||||||
|
b.conf.Types = append(b.conf.Types, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the builder with its api definition
|
||||||
|
// panics if already setup
|
||||||
|
func (b *Builder) Setup(r io.Reader) error {
|
||||||
|
if b.conf == nil {
|
||||||
|
b.conf = &config.Server{}
|
||||||
|
}
|
||||||
|
if b.conf.Services != nil {
|
||||||
|
panic(ErrAlreadySetup)
|
||||||
|
}
|
||||||
|
return b.conf.Parse(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind a dynamic handler to a REST service
|
||||||
|
func (b *Builder) Bind(method, path string, fn interface{}) error {
|
||||||
|
if b.conf.Services == nil {
|
||||||
|
return ErrNotSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
// find associated service
|
||||||
|
var service *config.Service
|
||||||
|
for _, s := range b.conf.Services {
|
||||||
|
if method == s.Method && path == s.Pattern {
|
||||||
|
service = s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService)
|
||||||
|
}
|
||||||
|
|
||||||
|
dyn, err := dynfunc.Build(fn, *service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s '%s' handler: %w", method, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.handlers = append(b.handlers, &apiHandler{
|
||||||
|
Path: path,
|
||||||
|
Method: method,
|
||||||
|
dyn: dyn,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a fully-featured HTTP server
|
||||||
|
func (b Builder) Build() (http.Handler, error) {
|
||||||
|
|
||||||
|
for _, service := range b.conf.Services {
|
||||||
|
var hasAssociatedHandler bool
|
||||||
|
for _, handler := range b.handlers {
|
||||||
|
if handler.Method == service.Method && handler.Path == service.Pattern {
|
||||||
|
hasAssociatedHandler = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasAssociatedHandler {
|
||||||
|
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Server(b), nil
|
||||||
|
}
|
17
errors.go
17
errors.go
|
@ -8,8 +8,17 @@ func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrNoServiceForHandler - no service matching this handler
|
// ErrLateType - cannot add datatype after setting up the definition
|
||||||
const ErrNoServiceForHandler = cerr("no service found for this handler")
|
const ErrLateType = cerr("types cannot be added after Setup")
|
||||||
|
|
||||||
// ErrNoHandlerForService - no handler matching this service
|
// ErrNotSetup - not set up yet
|
||||||
const ErrNoHandlerForService = cerr("no handler found for this service")
|
const ErrNotSetup = cerr("not set up")
|
||||||
|
|
||||||
|
// ErrAlreadySetup - already set up
|
||||||
|
const ErrAlreadySetup = cerr("already set up")
|
||||||
|
|
||||||
|
// ErrUnknownService - no service matching this handler
|
||||||
|
const ErrUnknownService = cerr("unknown service")
|
||||||
|
|
||||||
|
// ErrMissingHandler - missing handler
|
||||||
|
const ErrMissingHandler = cerr("missing handler")
|
||||||
|
|
32
handler.go
32
handler.go
|
@ -1,32 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/dynamic"
|
|
||||||
"git.xdrm.io/go/aicra/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type handler struct {
|
|
||||||
Method string
|
|
||||||
Path string
|
|
||||||
dynHandler *dynamic.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// createHandler builds a handler from its http method and path
|
|
||||||
// also it checks whether the function signature is valid
|
|
||||||
func createHandler(method, path string, service config.Service, fn dynamic.HandlerFn) (*handler, error) {
|
|
||||||
method = strings.ToUpper(method)
|
|
||||||
|
|
||||||
dynHandler, err := dynamic.Build(fn, service)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s '%s' handler: %w", method, path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &handler{
|
|
||||||
Path: path,
|
|
||||||
Method: method,
|
|
||||||
dynHandler: dynHandler,
|
|
||||||
}, nil
|
|
||||||
}
|
|
116
http.go
116
http.go
|
@ -1,116 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/api"
|
|
||||||
"git.xdrm.io/go/aicra/internal/reqdata"
|
|
||||||
)
|
|
||||||
|
|
||||||
// httpServer wraps the aicra server to allow handling http requests
|
|
||||||
type httpServer Server
|
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and has to be called on each request
|
|
||||||
func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
|
||||||
defer req.Body.Close()
|
|
||||||
|
|
||||||
// 1. find a matching service in the config
|
|
||||||
service := server.config.Find(req)
|
|
||||||
if service == nil {
|
|
||||||
response := api.EmptyResponse().WithError(api.ErrorUnknownService)
|
|
||||||
response.ServeHTTP(res, req)
|
|
||||||
logError(response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. build input parameter receiver
|
|
||||||
dataset := reqdata.New(service)
|
|
||||||
|
|
||||||
// 3. extract URI data
|
|
||||||
err := dataset.ExtractURI(req)
|
|
||||||
if err != nil {
|
|
||||||
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
|
||||||
response.ServeHTTP(res, req)
|
|
||||||
logError(response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. extract query data
|
|
||||||
err = dataset.ExtractQuery(req)
|
|
||||||
if err != nil {
|
|
||||||
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
|
||||||
response.ServeHTTP(res, req)
|
|
||||||
logError(response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. extract form/json data
|
|
||||||
err = dataset.ExtractForm(req)
|
|
||||||
if err != nil {
|
|
||||||
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
|
||||||
response.ServeHTTP(res, req)
|
|
||||||
logError(response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. find a matching handler
|
|
||||||
var foundHandler *handler
|
|
||||||
var found bool
|
|
||||||
|
|
||||||
for _, handler := range server.handlers {
|
|
||||||
if handler.Method == service.Method && handler.Path == service.Pattern {
|
|
||||||
foundHandler = handler
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. fail if found no handler
|
|
||||||
if foundHandler == nil {
|
|
||||||
if found {
|
|
||||||
r := api.EmptyResponse().WithError(api.ErrorUncallableService)
|
|
||||||
r.ServeHTTP(res, req)
|
|
||||||
logError(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r := api.EmptyResponse().WithError(api.ErrorUnknownService)
|
|
||||||
r.ServeHTTP(res, req)
|
|
||||||
logError(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. build api.Request from http.Request
|
|
||||||
apireq, err := api.NewRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. feed request with scope & parameters
|
|
||||||
apireq.Scope = service.Scope
|
|
||||||
apireq.Param = dataset.Data
|
|
||||||
|
|
||||||
// 10. execute
|
|
||||||
returned, apiErr := foundHandler.dynHandler.Handle(dataset.Data)
|
|
||||||
response := api.EmptyResponse().WithError(apiErr)
|
|
||||||
for key, value := range returned {
|
|
||||||
|
|
||||||
// find original name from rename
|
|
||||||
for name, param := range service.Output {
|
|
||||||
if param.Rename == key {
|
|
||||||
response.SetData(name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. apply headers
|
|
||||||
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
for key, values := range response.Headers {
|
|
||||||
for _, value := range values {
|
|
||||||
res.Header().Add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. write to response
|
|
||||||
response.ServeHTTP(res, req)
|
|
||||||
}
|
|
|
@ -80,7 +80,8 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -134,7 +135,8 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if test.ValidMethod && err != nil {
|
if test.ValidMethod && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
@ -150,20 +152,22 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestParseEmpty(t *testing.T) {
|
func TestParseEmpty(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[]`)
|
r := strings.NewReader(`[]`)
|
||||||
_, err := Parse(reader)
|
srv := &Server{}
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error (got '%s')", err)
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestParseJsonError(t *testing.T) {
|
func TestParseJsonError(t *testing.T) {
|
||||||
reader := strings.NewReader(`{
|
r := strings.NewReader(`{
|
||||||
"GET": {
|
"GET": {
|
||||||
"info": "info
|
"info": "info
|
||||||
},
|
},
|
||||||
}`) // trailing ',' is invalid JSON
|
}`) // trailing ',' is invalid JSON
|
||||||
_, err := Parse(reader)
|
srv := &Server{}
|
||||||
|
err := srv.Parse(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error")
|
t.Errorf("expected error")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -205,7 +209,8 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if test.ValidDescription && err != nil {
|
if test.ValidDescription && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -223,7 +228,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
|
||||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
r := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -233,7 +238,9 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv, err := Parse(reader, builtin.AnyDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -254,7 +261,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestOptionalParam(t *testing.T) {
|
func TestOptionalParam(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
r := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -267,7 +274,10 @@ func TestOptionalParam(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -577,7 +587,9 @@ func TestParseParameters(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -814,7 +826,10 @@ func TestServiceCollision(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Config), builtin.StringDataType{}, builtin.UintDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.StringDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.UintDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -951,7 +966,11 @@ func TestMatchSimple(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.IntDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Parameter represents a parameter definition (from api.json)
|
||||||
func (param *Parameter) Validate(datatypes ...datatype.T) error {
|
type Parameter struct {
|
||||||
|
Description string `json:"info"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Rename string `json:"name,omitempty"`
|
||||||
|
// ExtractType is the type of data the datatype returns
|
||||||
|
ExtractType reflect.Type
|
||||||
|
// Optional is set to true when the type is prefixed with '?'
|
||||||
|
Optional bool
|
||||||
|
|
||||||
|
// Validator is inferred from @Type
|
||||||
|
Validator datatype.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (param *Parameter) validate(datatypes ...datatype.T) error {
|
||||||
// missing description
|
// missing description
|
||||||
if len(param.Description) < 1 {
|
if len(param.Description) < 1 {
|
||||||
return ErrMissingParamDesc
|
return ErrMissingParamDesc
|
||||||
|
|
|
@ -9,34 +9,30 @@ import (
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse builds a server configuration from a json reader and checks for most format errors.
|
// Server definition
|
||||||
// you can provide additional DataTypes as variadic arguments
|
type Server struct {
|
||||||
func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
|
Types []datatype.T
|
||||||
server := &Server{
|
Services []*Service
|
||||||
Types: make([]datatype.T, 0),
|
|
||||||
Services: make([]*Service, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add data types
|
// Parse a reader into a server. Server.Types must be set beforehand to
|
||||||
for _, dtype := range dtypes {
|
// make datatypes available when checking and formatting the read configuration.
|
||||||
server.Types = append(server.Types, dtype)
|
func (srv *Server) Parse(r io.Reader) error {
|
||||||
|
if err := json.NewDecoder(r).Decode(&srv.Services); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrRead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
|
if err := srv.validate(); err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
return fmt.Errorf("%s: %w", ErrFormat, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Validate(); err != nil {
|
return nil
|
||||||
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return server, nil
|
// validate implements the validator interface
|
||||||
}
|
func (server Server) validate(datatypes ...datatype.T) error {
|
||||||
|
|
||||||
// Validate implements the validator interface
|
|
||||||
func (server Server) Validate(datatypes ...datatype.T) error {
|
|
||||||
for _, service := range server.Services {
|
for _, service := range server.Services {
|
||||||
err := service.Validate(server.Types...)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,35 @@ import (
|
||||||
|
|
||||||
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
|
// Service definition
|
||||||
|
type Service struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Pattern string `json:"path"`
|
||||||
|
Scope [][]string `json:"scope"`
|
||||||
|
Description string `json:"info"`
|
||||||
|
Input map[string]*Parameter `json:"in"`
|
||||||
|
Output map[string]*Parameter `json:"out"`
|
||||||
|
|
||||||
|
// references to url parameters
|
||||||
|
// format: '/uri/{param}'
|
||||||
|
Captures []*BraceCapture
|
||||||
|
|
||||||
|
// references to Query parameters
|
||||||
|
// format: 'GET@paranName'
|
||||||
|
Query map[string]*Parameter
|
||||||
|
|
||||||
|
// references for form parameters (all but Captures and Query)
|
||||||
|
Form map[string]*Parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// BraceCapture links to the related URI parameter
|
||||||
|
type BraceCapture struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
Ref *Parameter
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -24,9 +53,6 @@ func (svc *Service) Match(req *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and extract input
|
|
||||||
// todo: check if input match and extract models
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +102,7 @@ func (svc *Service) matchPattern(uri string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Validate implements the validator interface
|
||||||
func (svc *Service) Validate(datatypes ...datatype.T) error {
|
func (svc *Service) validate(datatypes ...datatype.T) error {
|
||||||
// check method
|
// check method
|
||||||
err := svc.isMethodAvailable()
|
err := svc.isMethodAvailable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -233,7 +259,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
|
||||||
param.Rename = paramName
|
param.Rename = paramName
|
||||||
}
|
}
|
||||||
|
|
||||||
err := param.Validate(types...)
|
err := param.validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
@ -283,7 +309,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
|
||||||
param.Rename = paramName
|
param.Rename = paramName
|
||||||
}
|
}
|
||||||
|
|
||||||
err := param.Validate(types...)
|
err := param.validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
|
||||||
)
|
|
||||||
|
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
|
|
||||||
// validator unifies the check and format routine
|
|
||||||
type validator interface {
|
|
||||||
Validate(...datatype.T) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server represents a full server configuration
|
|
||||||
type Server struct {
|
|
||||||
Types []datatype.T
|
|
||||||
Services []*Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service represents a service definition (from api.json)
|
|
||||||
type Service struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Pattern string `json:"path"`
|
|
||||||
Scope [][]string `json:"scope"`
|
|
||||||
Description string `json:"info"`
|
|
||||||
Input map[string]*Parameter `json:"in"`
|
|
||||||
Output map[string]*Parameter `json:"out"`
|
|
||||||
|
|
||||||
// references to url parameters
|
|
||||||
// format: '/uri/{param}'
|
|
||||||
Captures []*BraceCapture
|
|
||||||
|
|
||||||
// references to Query parameters
|
|
||||||
// format: 'GET@paranName'
|
|
||||||
Query map[string]*Parameter
|
|
||||||
|
|
||||||
// references for form parameters (all but Captures and Query)
|
|
||||||
Form map[string]*Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter represents a parameter definition (from api.json)
|
|
||||||
type Parameter struct {
|
|
||||||
Description string `json:"info"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Rename string `json:"name,omitempty"`
|
|
||||||
// ExtractType is the type of data the datatype returns
|
|
||||||
ExtractType reflect.Type
|
|
||||||
// Optional is set to true when the type is prefixed with '?'
|
|
||||||
Optional bool
|
|
||||||
|
|
||||||
// Validator is inferred from @Type
|
|
||||||
Validator datatype.Validator
|
|
||||||
}
|
|
||||||
|
|
||||||
// BraceCapture links to the related URI parameter
|
|
||||||
type BraceCapture struct {
|
|
||||||
Name string
|
|
||||||
Index int
|
|
||||||
Ref *Parameter
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package dynamic
|
package dynfunc
|
||||||
|
|
||||||
// cerr allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type cerr string
|
type cerr string
|
||||||
|
@ -17,6 +17,9 @@ const ErrNoServiceForHandler = cerr("no service found for this handler")
|
||||||
// ErrMissingHandlerArgumentParam - missing params arguments for handler
|
// ErrMissingHandlerArgumentParam - missing params arguments for handler
|
||||||
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
|
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
|
||||||
|
|
||||||
|
// ErrUnexpectedInput - input argument is not expected
|
||||||
|
const ErrUnexpectedInput = cerr("unexpected input struct")
|
||||||
|
|
||||||
// ErrMissingHandlerOutput - missing output for handler
|
// ErrMissingHandlerOutput - missing output for handler
|
||||||
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output")
|
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output")
|
||||||
|
|
||||||
|
@ -29,6 +32,9 @@ const ErrMissingRequestArgument = cerr("handler first argument must be of type a
|
||||||
// ErrMissingParamArgument - missing parameters argument for handler
|
// ErrMissingParamArgument - missing parameters argument for handler
|
||||||
const ErrMissingParamArgument = cerr("handler second argument must be a struct")
|
const ErrMissingParamArgument = cerr("handler second argument must be a struct")
|
||||||
|
|
||||||
|
// ErrUnexportedName - argument is unexported in struct
|
||||||
|
const ErrUnexportedName = cerr("unexported name")
|
||||||
|
|
||||||
// ErrMissingParamOutput - missing output argument for handler
|
// ErrMissingParamOutput - missing output argument for handler
|
||||||
const ErrMissingParamOutput = cerr("handler first output must be a *struct")
|
const ErrMissingParamOutput = cerr("handler first output must be a *struct")
|
||||||
|
|
||||||
|
@ -41,8 +47,5 @@ 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")
|
||||||
|
|
||||||
// ErrWrongOutputTypeFromConfig - a configuration output type is invalid in the handler output struct
|
|
||||||
const ErrWrongOutputTypeFromConfig = cerr("invalid struct field type")
|
|
||||||
|
|
||||||
// ErrMissingHandlerErrorOutput - missing handler output error
|
// ErrMissingHandlerErrorOutput - missing handler output error
|
||||||
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error")
|
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error")
|
|
@ -1,4 +1,4 @@
|
||||||
package dynamic
|
package dynfunc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,16 +8,16 @@ import (
|
||||||
"git.xdrm.io/go/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build a handler from a service configuration and a HandlerFn
|
// Build a handler from a service configuration and a dynamic function
|
||||||
//
|
//
|
||||||
// a HandlerFn must have as a signature : `func(api.Request, inputStruct) (outputStruct, api.Error)`
|
// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)`
|
||||||
// - `inputStruct` is a struct{} containing a field for each service input (with valid reflect.Type)
|
// - `inputStruct` 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)
|
// - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type)
|
||||||
//
|
//
|
||||||
// Special cases:
|
// Special cases:
|
||||||
// - it there is no input, `inputStruct` can be omitted
|
// - it there is no input, `inputStruct` must be omitted
|
||||||
// - it there is no output, `outputStruct` can be omitted
|
// - it there is no output, `outputStruct` must be omitted
|
||||||
func Build(fn HandlerFn, service config.Service) (*Handler, error) {
|
func Build(fn interface{}, service config.Service) (*Handler, error) {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
spec: makeSpec(service),
|
spec: makeSpec(service),
|
||||||
fn: fn,
|
fn: fn,
|
||||||
|
@ -39,7 +39,7 @@ func Build(fn HandlerFn, service config.Service) (*Handler, error) {
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle binds input @data into HandleFn and returns map output
|
// Handle binds input @data into the dynamic function and returns map output
|
||||||
func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Error) {
|
func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Error) {
|
||||||
fnv := reflect.ValueOf(h.fn)
|
fnv := reflect.ValueOf(h.fn)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package dynamic
|
package dynfunc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"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"
|
||||||
|
@ -16,6 +17,9 @@ func makeSpec(service config.Service) spec {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, param := range service.Input {
|
for _, param := range service.Input {
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// make a pointer if optional
|
// make a pointer if optional
|
||||||
if param.Optional {
|
if param.Optional {
|
||||||
spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
|
spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
|
||||||
|
@ -25,6 +29,9 @@ func makeSpec(service config.Service) spec {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, param := range service.Output {
|
for _, param := range service.Output {
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
spec.Output[param.Rename] = param.ExtractType
|
spec.Output[param.Rename] = param.ExtractType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +44,9 @@ func (s spec) checkInput(fnv reflect.Value) error {
|
||||||
|
|
||||||
// no input -> ok
|
// no input -> ok
|
||||||
if len(s.Input) == 0 {
|
if len(s.Input) == 0 {
|
||||||
|
if fnt.NumIn() > 0 {
|
||||||
|
return ErrUnexpectedInput
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +60,12 @@ func (s spec) checkInput(fnv reflect.Value) error {
|
||||||
return ErrMissingParamArgument
|
return ErrMissingParamArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for invlaid param
|
// check for invalid param
|
||||||
for name, ptype := range s.Input {
|
for name, ptype := range s.Input {
|
||||||
|
if name[0] == strings.ToLower(name)[0] {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
||||||
|
}
|
||||||
|
|
||||||
field, exists := structArg.FieldByName(name)
|
field, exists := structArg.FieldByName(name)
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig)
|
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig)
|
||||||
|
@ -100,6 +114,10 @@ func (s spec) checkOutput(fnv reflect.Value) error {
|
||||||
|
|
||||||
// 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] {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
||||||
|
}
|
||||||
|
|
||||||
field, exists := structOutput.FieldByName(name)
|
field, exists := structOutput.FieldByName(name)
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig)
|
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig)
|
||||||
|
@ -110,8 +128,8 @@ func (s spec) checkOutput(fnv reflect.Value) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ptype.ConvertibleTo(field.Type) {
|
if !field.Type.ConvertibleTo(ptype) {
|
||||||
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongOutputTypeFromConfig, field.Type, ptype)
|
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
package dynfunc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInputCheck(t *testing.T) {
|
||||||
|
tcases := []struct {
|
||||||
|
Input map[string]reflect.Type
|
||||||
|
Fn interface{}
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
// no input
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{},
|
||||||
|
Fn: func() {},
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
// func must have noarguments if none specified
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{},
|
||||||
|
Fn: func(int, string) {},
|
||||||
|
Err: ErrUnexpectedInput,
|
||||||
|
},
|
||||||
|
// missing input struct in func
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() {},
|
||||||
|
Err: ErrMissingHandlerArgumentParam,
|
||||||
|
},
|
||||||
|
// input not a struct
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func(int) {},
|
||||||
|
Err: ErrMissingParamArgument,
|
||||||
|
},
|
||||||
|
// unexported param name
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func(struct{}) {},
|
||||||
|
Err: ErrUnexportedName,
|
||||||
|
},
|
||||||
|
// input field missing
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func(struct{}) {},
|
||||||
|
Err: ErrMissingParamFromConfig,
|
||||||
|
},
|
||||||
|
// input field invalid type
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func(struct{ Test1 string }) {},
|
||||||
|
Err: ErrWrongParamTypeFromConfig,
|
||||||
|
},
|
||||||
|
// input field valid type
|
||||||
|
{
|
||||||
|
Input: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func(struct{ Test1 int }) {},
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) {
|
||||||
|
// mock spec
|
||||||
|
s := spec{
|
||||||
|
Input: tcase.Input,
|
||||||
|
Output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.checkInput(reflect.ValueOf(tcase.Fn))
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputCheck(t *testing.T) {
|
||||||
|
tcases := []struct {
|
||||||
|
Output map[string]reflect.Type
|
||||||
|
Fn interface{}
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
// no input -> missing api.Error
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func() {},
|
||||||
|
Err: ErrMissingHandlerOutput,
|
||||||
|
},
|
||||||
|
// no input -> with last type not api.Error
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func() bool { return true },
|
||||||
|
Err: ErrMissingHandlerErrorOutput,
|
||||||
|
},
|
||||||
|
// no input -> with api.Error
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func() api.Error { return api.ErrorSuccess },
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
// func can have output if not specified
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{},
|
||||||
|
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
// missing output struct in func
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() api.Error { return api.ErrorSuccess },
|
||||||
|
Err: ErrMissingParamOutput,
|
||||||
|
},
|
||||||
|
// output not a pointer
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (int, api.Error) { return 0, api.ErrorSuccess },
|
||||||
|
Err: ErrMissingParamOutput,
|
||||||
|
},
|
||||||
|
// output not a pointer to struct
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: ErrMissingParamOutput,
|
||||||
|
},
|
||||||
|
// unexported param name
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: ErrUnexportedName,
|
||||||
|
},
|
||||||
|
// output field missing
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: ErrMissingParamFromConfig,
|
||||||
|
},
|
||||||
|
// output field invalid type
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: ErrWrongParamTypeFromConfig,
|
||||||
|
},
|
||||||
|
// output field valid type
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": reflect.TypeOf(int(0)),
|
||||||
|
},
|
||||||
|
Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
// ignore type check on nil type
|
||||||
|
{
|
||||||
|
Output: map[string]reflect.Type{
|
||||||
|
"Test1": nil,
|
||||||
|
},
|
||||||
|
Fn: func() (*struct{ Test1 int }, api.Error) { return nil, api.ErrorSuccess },
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) {
|
||||||
|
// mock spec
|
||||||
|
s := spec{
|
||||||
|
Input: nil,
|
||||||
|
Output: tcase.Output,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.checkOutput(reflect.ValueOf(tcase.Fn))
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
package dynamic
|
package dynfunc
|
||||||
|
|
||||||
import "reflect"
|
import "reflect"
|
||||||
|
|
||||||
// HandlerFn defines a dynamic handler function
|
|
||||||
type HandlerFn interface{}
|
|
||||||
|
|
||||||
// Handler represents a dynamic api handler
|
// Handler represents a dynamic api handler
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
spec spec
|
spec spec
|
||||||
fn HandlerFn
|
fn interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type spec struct {
|
type spec struct {
|
|
@ -39,7 +39,7 @@ func New(service *config.Service) *Set {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
||||||
func (i *Set) ExtractURI(req *http.Request) error {
|
func (i *Set) ExtractURI(req http.Request) error {
|
||||||
uriparts := config.SplitURL(req.URL.RequestURI())
|
uriparts := config.SplitURL(req.URL.RequestURI())
|
||||||
|
|
||||||
for _, capture := range i.service.Captures {
|
for _, capture := range i.service.Captures {
|
||||||
|
@ -71,7 +71,7 @@ func (i *Set) ExtractURI(req *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractQuery data from the url query parameters
|
// ExtractQuery data from the url query parameters
|
||||||
func (i *Set) ExtractQuery(req *http.Request) error {
|
func (i *Set) ExtractQuery(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 {
|
||||||
|
@ -108,7 +108,7 @@ func (i *Set) ExtractQuery(req *http.Request) error {
|
||||||
// - parse 'form-data' if not supported for non-POST requests
|
// - parse 'form-data' if not supported for non-POST requests
|
||||||
// - parse 'x-www-form-urlencoded'
|
// - parse 'x-www-form-urlencoded'
|
||||||
// - parse 'application/json'
|
// - parse 'application/json'
|
||||||
func (i *Set) ExtractForm(req *http.Request) error {
|
func (i *Set) ExtractForm(req http.Request) error {
|
||||||
|
|
||||||
// ignore GET method
|
// ignore GET method
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
|
@ -138,7 +138,7 @@ func (i *Set) ExtractForm(req *http.Request) error {
|
||||||
|
|
||||||
// parseJSON parses JSON from the request body inside 'Form'
|
// parseJSON parses JSON from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *Set) parseJSON(req *http.Request) error {
|
func (i *Set) parseJSON(req http.Request) error {
|
||||||
|
|
||||||
parsed := make(map[string]interface{}, 0)
|
parsed := make(map[string]interface{}, 0)
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ func (i *Set) parseJSON(req *http.Request) error {
|
||||||
|
|
||||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *Set) parseUrlencoded(req *http.Request) error {
|
func (i *Set) parseUrlencoded(req http.Request) error {
|
||||||
// use http.Request interface
|
// use http.Request interface
|
||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -215,7 +215,7 @@ func (i *Set) parseUrlencoded(req *http.Request) error {
|
||||||
|
|
||||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *Set) parseMultipart(req *http.Request) error {
|
func (i *Set) parseMultipart(req http.Request) error {
|
||||||
|
|
||||||
// 1. create reader
|
// 1. create reader
|
||||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||||
|
|
|
@ -131,7 +131,7 @@ func TestStoreWithUri(t *testing.T) {
|
||||||
store := New(service)
|
store := New(service)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
||||||
err := store.ExtractURI(req)
|
err := store.ExtractURI(*req)
|
||||||
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) {
|
||||||
|
@ -242,7 +242,7 @@ func TestExtractQuery(t *testing.T) {
|
||||||
store := New(getServiceWithQuery(test.ServiceParam...))
|
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||||
err := store.ExtractQuery(req)
|
err := store.ExtractQuery(*req)
|
||||||
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) {
|
||||||
|
@ -324,7 +324,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||||
|
|
||||||
// defer req.Body.Close()
|
// defer req.Body.Close()
|
||||||
store := New(nil)
|
store := New(nil)
|
||||||
err := store.ExtractForm(req)
|
err := store.ExtractForm(*req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("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()
|
t.FailNow()
|
||||||
|
@ -420,7 +420,7 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
|
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
err := store.ExtractForm(req)
|
err := store.ExtractForm(*req)
|
||||||
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) {
|
||||||
|
@ -563,7 +563,7 @@ func TestJsonParameters(t *testing.T) {
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
err := store.ExtractForm(req)
|
err := store.ExtractForm(*req)
|
||||||
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) {
|
||||||
|
@ -720,7 +720,7 @@ x
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
err := store.ExtractForm(req)
|
err := store.ExtractForm(*req)
|
||||||
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) {
|
||||||
|
|
148
server.go
148
server.go
|
@ -1,91 +1,101 @@
|
||||||
package aicra
|
package aicra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"net/http"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/api"
|
||||||
"git.xdrm.io/go/aicra/dynamic"
|
|
||||||
"git.xdrm.io/go/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
"git.xdrm.io/go/aicra/internal/reqdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server represents an AICRA instance featuring: type checkers, services
|
// Server hides the builder and allows handling http requests
|
||||||
type Server struct {
|
type Server Builder
|
||||||
config *config.Server
|
|
||||||
handlers []*handler
|
// ServeHTTP implements http.Handler and is called on each request
|
||||||
|
func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
// 1. find a matching service in the config
|
||||||
|
service := server.conf.Find(req)
|
||||||
|
if service == nil {
|
||||||
|
errorHandler(api.ErrorUnknownService)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a framework instance from a configuration file
|
// 2. extract request data
|
||||||
func New(configPath string, dtypes ...datatype.T) (*Server, error) {
|
dataset, err := extractRequestData(service, *req)
|
||||||
var (
|
|
||||||
err error
|
|
||||||
configFile io.ReadCloser
|
|
||||||
)
|
|
||||||
|
|
||||||
// 1. init instance
|
|
||||||
var i = &Server{
|
|
||||||
config: nil,
|
|
||||||
handlers: make([]*handler, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. open config file
|
|
||||||
configFile, err = os.Open(configPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
errorHandler(api.ErrorMissingParam)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
// 3. load configuration
|
// 3. find a matching handler
|
||||||
i.config, err = config.Parse(configFile, dtypes...)
|
var handler *apiHandler
|
||||||
|
for _, h := range server.handlers {
|
||||||
|
if h.Method == service.Method && h.Path == service.Pattern {
|
||||||
|
handler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. fail if found no handler
|
||||||
|
if handler == nil {
|
||||||
|
errorHandler(api.ErrorUncallableService)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. execute
|
||||||
|
returned, apiErr := handler.dyn.Handle(dataset.Data)
|
||||||
|
|
||||||
|
// 6. build response from returned data
|
||||||
|
response := api.EmptyResponse().WithError(apiErr)
|
||||||
|
for key, value := range returned {
|
||||||
|
|
||||||
|
// find original name from rename
|
||||||
|
for name, param := range service.Output {
|
||||||
|
if param.Rename == key {
|
||||||
|
response.SetData(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. apply headers
|
||||||
|
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
for key, values := range response.Headers {
|
||||||
|
for _, value := range values {
|
||||||
|
res.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err api.Error) http.HandlerFunc {
|
||||||
|
return func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
r := api.EmptyResponse().WithError(err)
|
||||||
|
r.ServeHTTP(res, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRequestData(service *config.Service, req http.Request) (*reqdata.Set, error) {
|
||||||
|
dataset := reqdata.New(service)
|
||||||
|
|
||||||
|
// 3. extract URI data
|
||||||
|
err := dataset.ExtractURI(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return i, nil
|
// 4. extract query data
|
||||||
|
err = dataset.ExtractQuery(req)
|
||||||
}
|
|
||||||
|
|
||||||
// Handle sets a new handler for an HTTP method to a path
|
|
||||||
func (s *Server) Handle(method, path string, fn dynamic.HandlerFn) error {
|
|
||||||
// find associated service
|
|
||||||
var found *config.Service = nil
|
|
||||||
for _, service := range s.config.Services {
|
|
||||||
if method == service.Method && path == service.Pattern {
|
|
||||||
found = service
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found == nil {
|
|
||||||
return fmt.Errorf("%s '%s': %w", method, path, ErrNoServiceForHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler, err := createHandler(method, path, *found, fn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
s.handlers = append(s.handlers, handler)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToHTTPServer converts the server to a http server
|
// 5. extract form/json data
|
||||||
func (s Server) ToHTTPServer() (*httpServer, error) {
|
err = dataset.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
// check if handlers are missing
|
return nil, err
|
||||||
for _, service := range s.config.Services {
|
|
||||||
found := false
|
|
||||||
for _, handler := range s.handlers {
|
|
||||||
if handler.Method == service.Method && handler.Path == service.Pattern {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrNoHandlerForService)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. cast to http server
|
return dataset, nil
|
||||||
httpServer := httpServer(s)
|
|
||||||
return &httpServer, nil
|
|
||||||
}
|
}
|
||||||
|
|
15
util.go
15
util.go
|
@ -1,15 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
|
|
||||||
// Prints an error as HTTP response
|
|
||||||
func logError(res *api.Response) {
|
|
||||||
log.Printf("[http.fail] %v\n", res)
|
|
||||||
}
|
|
Loading…
Reference in New Issue