diff --git a/api/error.defaults.go b/api/error.defaults.go index 4a9ee0e..75ea528 100644 --- a/api/error.defaults.go +++ b/api/error.defaults.go @@ -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", diff --git a/api/handler.go b/api/handler.go deleted file mode 100644 index c6e5cc9..0000000 --- a/api/handler.go +++ /dev/null @@ -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 -} diff --git a/datatype/builtin/any.go b/datatype/builtin/any.go index e12dd10..e1139b0 100644 --- a/datatype/builtin/any.go +++ b/datatype/builtin/any.go @@ -1,10 +1,19 @@ package builtin -import "git.xdrm.io/go/aicra/datatype" +import ( + "reflect" + + "git.xdrm.io/go/aicra/datatype" +) // AnyDataType is what its name tells type AnyDataType struct{} +// Type returns the type of data +func (AnyDataType) Type() reflect.Type { + return reflect.TypeOf(interface{}(nil)) +} + // Build returns the validator func (AnyDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { // nothing if type not handled diff --git a/datatype/builtin/bool.go b/datatype/builtin/bool.go index 1d5e225..4d95547 100644 --- a/datatype/builtin/bool.go +++ b/datatype/builtin/bool.go @@ -1,10 +1,19 @@ package builtin -import "git.xdrm.io/go/aicra/datatype" +import ( + "reflect" + + "git.xdrm.io/go/aicra/datatype" +) // BoolDataType is what its name tells type BoolDataType struct{} +// Type returns the type of data +func (BoolDataType) Type() reflect.Type { + return reflect.TypeOf(true) +} + // Build returns the validator func (BoolDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { // nothing if type not handled diff --git a/datatype/builtin/float.go b/datatype/builtin/float.go index d2f3204..5016f10 100644 --- a/datatype/builtin/float.go +++ b/datatype/builtin/float.go @@ -2,6 +2,7 @@ package builtin import ( "encoding/json" + "reflect" "git.xdrm.io/go/aicra/datatype" ) @@ -9,6 +10,11 @@ import ( // FloatDataType is what its name tells type FloatDataType struct{} +// Type returns the type of data +func (FloatDataType) Type() reflect.Type { + return reflect.TypeOf(float64(0)) +} + // Build returns the validator func (FloatDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { // nothing if type not handled diff --git a/datatype/builtin/int.go b/datatype/builtin/int.go index 7dfdbf9..36b1038 100644 --- a/datatype/builtin/int.go +++ b/datatype/builtin/int.go @@ -3,6 +3,7 @@ package builtin import ( "encoding/json" "math" + "reflect" "git.xdrm.io/go/aicra/datatype" ) @@ -10,6 +11,11 @@ import ( // IntDataType is what its name tells type IntDataType struct{} +// Type returns the type of data +func (IntDataType) Type() reflect.Type { + return reflect.TypeOf(int(0)) +} + // Build returns the validator func (IntDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { // nothing if type not handled diff --git a/datatype/builtin/string.go b/datatype/builtin/string.go index 1a94b82..02be6ae 100644 --- a/datatype/builtin/string.go +++ b/datatype/builtin/string.go @@ -1,6 +1,7 @@ package builtin import ( + "reflect" "regexp" "strconv" @@ -13,6 +14,11 @@ var variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`) // StringDataType is what its name tells type StringDataType struct{} +// Type returns the type of data +func (StringDataType) Type() reflect.Type { + return reflect.TypeOf(string("")) +} + // Build returns the validator. // availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`. func (s StringDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { diff --git a/datatype/builtin/uint.go b/datatype/builtin/uint.go index 53f71de..e59d2c1 100644 --- a/datatype/builtin/uint.go +++ b/datatype/builtin/uint.go @@ -3,6 +3,7 @@ package builtin import ( "encoding/json" "math" + "reflect" "git.xdrm.io/go/aicra/datatype" ) @@ -10,6 +11,11 @@ import ( // UintDataType is what its name tells type UintDataType struct{} +// Type returns the type of data +func (UintDataType) Type() reflect.Type { + return reflect.TypeOf(uint(0)) +} + // Build returns the validator func (UintDataType) Build(typeName string, registry ...datatype.T) datatype.Validator { // nothing if type not handled diff --git a/datatype/types.go b/datatype/types.go index fe529f1..c258b2f 100644 --- a/datatype/types.go +++ b/datatype/types.go @@ -1,5 +1,7 @@ package datatype +import "reflect" + // Validator returns whether a given value fulfills a datatype // and casts the value into a compatible type type Validator func(value interface{}) (cast interface{}, valid bool) @@ -8,5 +10,6 @@ type Validator func(value interface{}) (cast interface{}, valid bool) // definition does not match this T ; the registry is passed for recursive datatypes (e.g. slices, structs, etc) // to be able to access other datatypes type T interface { + Type() reflect.Type Build(typeDefinition string, registry ...T) Validator } diff --git a/dynamic/errors.go b/dynamic/errors.go new file mode 100644 index 0000000..e3d7546 --- /dev/null +++ b/dynamic/errors.go @@ -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") diff --git a/dynamic/handler.go b/dynamic/handler.go new file mode 100644 index 0000000..54bded1 --- /dev/null +++ b/dynamic/handler.go @@ -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()) +} diff --git a/dynamic/spec.go b/dynamic/spec.go new file mode 100644 index 0000000..354df52 --- /dev/null +++ b/dynamic/spec.go @@ -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 +} diff --git a/dynamic/types.go b/dynamic/types.go new file mode 100644 index 0000000..a180e63 --- /dev/null +++ b/dynamic/types.go @@ -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 +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..252f8ee --- /dev/null +++ b/errors.go @@ -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") diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..8af4f34 --- /dev/null +++ b/handler.go @@ -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 +} diff --git a/http.go b/http.go index 8db6033..d078ea1 100644 --- a/http.go +++ b/http.go @@ -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") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ca1569e..0bc3e2a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -481,6 +481,46 @@ func TestParseParameters(t *testing.T) { ]`, nil, }, + // missing rename + { + `[ + { + "method": "GET", + "path": "/{uri}", + "info": "info", + "in": { + "{uri}": { "info": "valid", "type": "any" } + } + } + ]`, + ErrMandatoryRename, + }, + { + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "GET@abc": { "info": "valid", "type": "any" } + } + } + ]`, + ErrMandatoryRename, + }, + { + `[ + { + "method": "GET", + "path": "/", + "info": "info", + "in": { + "GET@abc": { "info": "valid", "type": "any", "name": "abc" } + } + } + ]`, + nil, + }, { // URI parameter `[ @@ -616,7 +656,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "string" } + "{c}": { "info":"info", "type": "string", "name": "c" } } } ]`, @@ -629,7 +669,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -642,7 +682,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}/d", "info": "info", "in": { - "{c}": { "info":"info", "type": "string" } + "{c}": { "info":"info", "type": "string", "name": "c" } } } ]`, @@ -655,7 +695,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "string" } + "{c}": { "info":"info", "type": "string", "name": "c" } } } ]`, @@ -668,7 +708,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "string" } + "{c}": { "info":"info", "type": "string", "name": "c" } } } ]`, @@ -681,7 +721,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}/d", "info": "info", "in": { - "{c}": { "info":"info", "type": "string" } + "{c}": { "info":"info", "type": "string", "name": "c" } } } ]`, @@ -694,7 +734,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -707,7 +747,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -720,7 +760,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}/d", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -733,7 +773,7 @@ func TestServiceCollision(t *testing.T) { }, { "method": "GET", "path": "/a/{c}/d", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -743,12 +783,12 @@ func TestServiceCollision(t *testing.T) { `[ { "method": "GET", "path": "/a/{b}", "info": "info", "in": { - "{b}": { "info":"info", "type": "uint" } + "{b}": { "info":"info", "type": "uint", "name": "b" } } }, { "method": "GET", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -758,12 +798,12 @@ func TestServiceCollision(t *testing.T) { `[ { "method": "GET", "path": "/a/{b}", "info": "info", "in": { - "{b}": { "info":"info", "type": "uint" } + "{b}": { "info":"info", "type": "uint", "name": "b" } } }, { "method": "PUT", "path": "/a/{c}", "info": "info", "in": { - "{c}": { "info":"info", "type": "uint" } + "{c}": { "info":"info", "type": "uint", "name": "c" } } } ]`, @@ -850,7 +890,8 @@ func TestMatchSimple(t *testing.T) { "in": { "{id}": { "info": "info", - "type": "bool" + "type": "bool", + "name": "id" } } } ]`, @@ -865,7 +906,8 @@ func TestMatchSimple(t *testing.T) { "in": { "{id}": { "info": "info", - "type": "int" + "type": "int", + "name": "id" } } } ]`, @@ -880,7 +922,8 @@ func TestMatchSimple(t *testing.T) { "in": { "{valid}": { "info": "info", - "type": "bool" + "type": "bool", + "name": "valid" } } } ]`, @@ -895,7 +938,8 @@ func TestMatchSimple(t *testing.T) { "in": { "{valid}": { "info": "info", - "type": "bool" + "type": "bool", + "name": "valid" } } } ]`, diff --git a/internal/config/errors.go b/internal/config/errors.go index a4741e4..2a2df17 100644 --- a/internal/config/errors.go +++ b/internal/config/errors.go @@ -29,6 +29,9 @@ const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces") // ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path") +// ErrMandatoryRename - capture/query parameters must have a rename +const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'") + // ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition") @@ -38,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") diff --git a/internal/config/parameter.go b/internal/config/parameter.go index 4037ee6..1f8be92 100644 --- a/internal/config/parameter.go +++ b/internal/config/parameter.go @@ -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 { @@ -20,5 +22,17 @@ func (param *Parameter) Validate(datatypes ...datatype.T) error { param.Type = param.Type[1:] } + // assign the datatype + for _, dtype := range datatypes { + param.Validator = dtype.Build(param.Type, datatypes...) + param.ExtractType = dtype.Type() + if param.Validator != nil { + break + } + } + if param.Validator == nil { + return ErrUnknownDataType + } + return nil } diff --git a/internal/config/service.go b/internal/config/service.go index 9ea781a..87815d7 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -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 } @@ -183,7 +189,7 @@ func (svc *Service) validateInput(types []datatype.T) error { } // fail if brace capture does not exists in pattern - iscapture := false + var iscapture, isquery bool if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 { braceName := matches[0][1] @@ -209,7 +215,7 @@ func (svc *Service) validateInput(types []datatype.T) error { svc.Query = make(map[string]*Parameter) } svc.Query[queryName] = param - + isquery = true } else { if svc.Form == nil { svc.Form = make(map[string]*Parameter) @@ -217,12 +223,17 @@ func (svc *Service) validateInput(types []datatype.T) error { svc.Form[paramName] = param } + // fail if capture or query without rename + if len(param.Rename) < 1 && (iscapture || isquery) { + return fmt.Errorf("%s: %w", paramName, ErrMandatoryRename) + } + // use param name if no rename if len(param.Rename) < 1 { param.Rename = paramName } - err := param.Validate() + err := param.Validate(types...) if err != nil { return fmt.Errorf("%s: %w", paramName, err) } @@ -232,20 +243,7 @@ func (svc *Service) validateInput(types []datatype.T) error { return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam) } - // assign the datatype - datatypeFound := false - for _, dtype := range types { - param.Validator = dtype.Build(param.Type, types...) - if param.Validator != nil { - datatypeFound = true - break - } - } - if !datatypeFound { - return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType) - } - - // check for name/rename conflict + // fail on name/rename conflict for paramName2, param2 := range svc.Input { // ignore self if paramName == paramName2 { @@ -265,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 +} diff --git a/internal/config/types.go b/internal/config/types.go index 4b356a5..f3532a4 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -2,6 +2,7 @@ package config import ( "net/http" + "reflect" "git.xdrm.io/go/aicra/datatype" ) @@ -26,8 +27,7 @@ type Service struct { Scope [][]string `json:"scope"` Description string `json:"info"` Input map[string]*Parameter `json:"in"` - // Download *bool `json:"download"` - // Output map[string]*Parameter `json:"out"` + Output map[string]*Parameter `json:"out"` // references to url parameters // format: '/uri/{param}' @@ -46,6 +46,8 @@ 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 diff --git a/server.go b/server.go index a8bd3d0..54e53b4 100644 --- a/server.go +++ b/server.go @@ -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) } }