Compare commits

...

7 Commits

13 changed files with 420 additions and 54 deletions

View File

@ -76,7 +76,7 @@ var errorReasons = map[Error]string{
ErrorUnknown: "unknown error",
ErrorSuccess: "all right",
ErrorFailure: "it failed",
ErrorNoMatchFound: "resource found",
ErrorNoMatchFound: "resource not found",
ErrorAlreadyExists: "already exists",
ErrorConfig: "configuration error",
ErrorUpload: "upload failed",

View File

@ -1,34 +0,0 @@
package api
import (
"strings"
)
// HandlerFn defines the handler signature
type HandlerFn func(req Request, res *Response) Error
// Handler is an API handler ready to be bound
type Handler struct {
path string
method string
Fn HandlerFn
}
// NewHandler builds a handler from its http method and path
func NewHandler(method, path string, fn HandlerFn) (*Handler, error) {
return &Handler{
path: path,
method: strings.ToUpper(method),
Fn: fn,
}, nil
}
// GetMethod returns the handler's HTTP method
func (h *Handler) GetMethod() string {
return h.method
}
// GetPath returns the handler's path
func (h *Handler) GetPath() string {
return h.path
}

48
dynamic/errors.go Normal file
View File

@ -0,0 +1,48 @@
package dynamic
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrHandlerNotFunc - handler is not a func
const ErrHandlerNotFunc = cerr("handler must be a func")
// ErrNoServiceForHandler - no service matching this handler
const ErrNoServiceForHandler = cerr("no service found for this handler")
// ErrMissingHandlerArgumentParam - missing params arguments for handler
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
// ErrMissingHandlerOutput - missing output for handler
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output")
// ErrMissingHandlerOutputError - missing error output for handler
const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error")
// ErrMissingRequestArgument - missing request argument for handler
const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request")
// ErrMissingParamArgument - missing parameters argument for handler
const ErrMissingParamArgument = cerr("handler second argument must be a struct")
// ErrMissingParamOutput - missing output argument for handler
const ErrMissingParamOutput = cerr("handler first output must be a *struct")
// ErrMissingParamFromConfig - missing a parameter in handler struct
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
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
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error")

90
dynamic/handler.go Normal file
View File

@ -0,0 +1,90 @@
package dynamic
import (
"fmt"
"reflect"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/config"
)
// Build a handler from a service configuration and a HandlerFn
//
// a HandlerFn must have as a signature : `func(api.Request, inputStruct) (outputStruct, api.Error)`
// - `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)
//
// Special cases:
// - it there is no input, `inputStruct` can be omitted
// - it there is no output, `outputStruct` can be omitted
func Build(fn HandlerFn, service config.Service) (*Handler, error) {
h := &Handler{
spec: makeSpec(service),
fn: fn,
}
fnv := reflect.ValueOf(fn)
if fnv.Type().Kind() != reflect.Func {
return nil, ErrHandlerNotFunc
}
if err := h.spec.checkInput(fnv); err != nil {
return nil, fmt.Errorf("input: %w", err)
}
if err := h.spec.checkOutput(fnv); err != nil {
return nil, fmt.Errorf("output: %w", err)
}
return h, nil
}
// Handle binds input @data into HandleFn and returns map output
func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Error) {
fnv := reflect.ValueOf(h.fn)
callArgs := []reflect.Value{}
// bind input data
if fnv.Type().NumIn() > 0 {
// create zero value struct
callStructPtr := reflect.New(fnv.Type().In(0))
callStruct := callStructPtr.Elem()
// set each field
for name := range h.spec.Input {
field := callStruct.FieldByName(name)
if !field.CanSet() {
continue
}
// get value from @data
value, inData := data[name]
if !inData {
continue
}
field.Set(reflect.ValueOf(value).Convert(field.Type()))
}
callArgs = append(callArgs, callStruct)
}
// call the HandlerFn
output := fnv.Call(callArgs)
// no output OR pointer to output struct is nil
outdata := make(map[string]interface{})
if len(h.spec.Output) < 1 || output[0].IsNil() {
return outdata, api.Error(output[len(output)-1].Int())
}
// extract struct from pointer
returnStruct := output[0].Elem()
for name := range h.spec.Output {
field := returnStruct.FieldByName(name)
outdata[name] = field.Interface()
}
// extract api.Error
return outdata, api.Error(output[len(output)-1].Int())
}

119
dynamic/spec.go Normal file
View File

@ -0,0 +1,119 @@
package dynamic
import (
"fmt"
"reflect"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/config"
)
// builds a spec from the configuration service
func makeSpec(service config.Service) spec {
spec := spec{
Input: make(map[string]reflect.Type),
Output: make(map[string]reflect.Type),
}
for _, param := range service.Input {
// make a pointer if optional
if param.Optional {
spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
continue
}
spec.Input[param.Rename] = param.ExtractType
}
for _, param := range service.Output {
spec.Output[param.Rename] = param.ExtractType
}
return spec
}
// checks for HandlerFn input arguments
func (s spec) checkInput(fnv reflect.Value) error {
fnt := fnv.Type()
// no input -> ok
if len(s.Input) == 0 {
return nil
}
if fnt.NumIn() != 1 {
return ErrMissingHandlerArgumentParam
}
// arg must be a struct
structArg := fnt.In(0)
if structArg.Kind() != reflect.Struct {
return ErrMissingParamArgument
}
// check for invlaid param
for name, ptype := range s.Input {
field, exists := structArg.FieldByName(name)
if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig)
}
if !ptype.AssignableTo(field.Type) {
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
}
}
return nil
}
// checks for HandlerFn output arguments
func (s spec) checkOutput(fnv reflect.Value) error {
fnt := fnv.Type()
if fnt.NumOut() < 1 {
return ErrMissingHandlerOutput
}
// last output must be api.Error
errOutput := fnt.Out(fnt.NumOut() - 1)
if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) {
return ErrMissingHandlerErrorOutput
}
// no output -> ok
if len(s.Output) == 0 {
return nil
}
if fnt.NumOut() != 2 {
return ErrMissingParamOutput
}
// fail if first output is not a pointer to struct
structOutputPtr := fnt.Out(0)
if structOutputPtr.Kind() != reflect.Ptr {
return ErrMissingParamOutput
}
structOutput := structOutputPtr.Elem()
if structOutput.Kind() != reflect.Struct {
return ErrMissingParamOutput
}
// fail on invalid output
for name, ptype := range s.Output {
field, exists := structOutput.FieldByName(name)
if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig)
}
// ignore types evalutating to nil
if ptype == nil {
continue
}
if !ptype.ConvertibleTo(field.Type) {
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongOutputTypeFromConfig, field.Type, ptype)
}
}
return nil
}

17
dynamic/types.go Normal file
View File

@ -0,0 +1,17 @@
package dynamic
import "reflect"
// HandlerFn defines a dynamic handler function
type HandlerFn interface{}
// Handler represents a dynamic api handler
type Handler struct {
spec spec
fn HandlerFn
}
type spec struct {
Input map[string]reflect.Type
Output map[string]reflect.Type
}

15
errors.go Normal file
View File

@ -0,0 +1,15 @@
package aicra
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrNoServiceForHandler - no service matching this handler
const ErrNoServiceForHandler = cerr("no service found for this handler")
// ErrNoHandlerForService - no handler matching this service
const ErrNoHandlerForService = cerr("no handler found for this service")

32
handler.go Normal file
View File

@ -0,0 +1,32 @@
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
}

18
http.go
View File

@ -55,11 +55,11 @@ func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
}
// 6. find a matching handler
var foundHandler *api.Handler
var foundHandler *handler
var found bool
for _, handler := range server.handlers {
if handler.GetMethod() == service.Method && handler.GetPath() == service.Pattern {
if handler.Method == service.Method && handler.Path == service.Pattern {
foundHandler = handler
found = true
}
@ -91,9 +91,17 @@ func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
apireq.Param = dataset.Data
// 10. execute
response := api.EmptyResponse()
apiErr := foundHandler.Fn(*apireq, response)
response.WithError(apiErr)
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")

View File

@ -41,6 +41,9 @@ const ErrMissingDescription = cerr("missing description")
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
// ErrOptionalOption - an output is optional
const ErrOptionalOption = cerr("output cannot be optional")
// ErrMissingParamDesc - a parameter is missing its description
const ErrMissingParamDesc = cerr("missing parameter description")

View File

@ -1,6 +1,8 @@
package config
import "git.xdrm.io/go/aicra/datatype"
import (
"git.xdrm.io/go/aicra/datatype"
)
// Validate implements the validator interface
func (param *Parameter) Validate(datatypes ...datatype.T) error {
@ -21,16 +23,14 @@ func (param *Parameter) Validate(datatypes ...datatype.T) error {
}
// assign the datatype
datatypeFound := false
for _, dtype := range datatypes {
param.Validator = dtype.Build(param.Type, datatypes...)
param.ExtractType = dtype.Type()
if param.Validator != nil {
datatypeFound = true
param.ExtractType = dtype.Type()
break
}
}
if !datatypeFound {
if param.Validator == nil {
return ErrUnknownDataType
}

View File

@ -108,6 +108,12 @@ func (svc *Service) Validate(datatypes ...datatype.T) error {
}
}
// check output
err = svc.validateOutput(datatypes)
if err != nil {
return fmt.Errorf("field 'out': %w", err)
}
return nil
}
@ -257,3 +263,52 @@ func (svc *Service) validateInput(types []datatype.T) error {
return nil
}
func (svc *Service) validateOutput(types []datatype.T) error {
// ignore no parameter
if svc.Output == nil || len(svc.Output) < 1 {
svc.Output = make(map[string]*Parameter, 0)
return nil
}
// for each parameter
for paramName, param := range svc.Output {
if len(paramName) < 1 {
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
}
// use param name if no rename
if len(param.Rename) < 1 {
param.Rename = paramName
}
err := param.Validate(types...)
if err != nil {
return fmt.Errorf("%s: %w", paramName, err)
}
if param.Optional {
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption)
}
// fail on name/rename conflict
for paramName2, param2 := range svc.Output {
// ignore self
if paramName == paramName2 {
continue
}
// 3.2.1. Same rename field
// 3.2.2. Not-renamed field matches a renamed field
// 3.2.3. Renamed field matches name
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
}
}
}
return nil
}

View File

@ -5,15 +5,15 @@ import (
"io"
"os"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype"
"git.xdrm.io/go/aicra/dynamic"
"git.xdrm.io/go/aicra/internal/config"
)
// Server represents an AICRA instance featuring: type checkers, services
type Server struct {
config *config.Server
handlers []*api.Handler
handlers []*handler
}
// New creates a framework instance from a configuration file
@ -26,7 +26,7 @@ func New(configPath string, dtypes ...datatype.T) (*Server, error) {
// 1. init instance
var i = &Server{
config: nil,
handlers: make([]*api.Handler, 0),
handlers: make([]*handler, 0),
}
// 2. open config file
@ -46,13 +46,26 @@ func New(configPath string, dtypes ...datatype.T) (*Server, error) {
}
// HandleFunc sets a new handler for an HTTP method to a path
func (s *Server) Handle(httpMethod, path string, fn api.HandlerFn) {
handler, err := api.NewHandler(httpMethod, path, fn)
// 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 {
panic(err)
return err
}
s.handlers = append(s.handlers, handler)
return nil
}
// ToHTTPServer converts the server to a http server
@ -62,13 +75,13 @@ func (s Server) ToHTTPServer() (*httpServer, error) {
for _, service := range s.config.Services {
found := false
for _, handler := range s.handlers {
if handler.GetMethod() == service.Method && handler.GetPath() == service.Pattern {
if handler.Method == service.Method && handler.Path == service.Pattern {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("missing handler for %s '%s'", service.Method, service.Pattern)
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrNoHandlerForService)
}
}