diff --git a/api/arguments.go b/api/arguments.go deleted file mode 100644 index c8a19b1..0000000 --- a/api/arguments.go +++ /dev/null @@ -1,93 +0,0 @@ -package api - -import ( - "errors" -) - -// ErrUnknownKey is returned when a key does not exist using a getter -var ErrUnknownKey = errors.New("key does not exist") - -// ErrInvalidType is returned when a typed getter tries to get a value that cannot be -// translated into the requested type -var ErrInvalidType = errors.New("invalid type") - -// Has checks whether a key exists in the arguments -func (i Arguments) Has(key string) bool { - _, exists := i[key] - return exists -} - -// Get extracts a parameter as an interface{} value -func (i Arguments) Get(key string) (interface{}, error) { - val, ok := i[key] - if !ok { - return 0, ErrUnknownKey - } - - return val, nil -} - -// GetFloat extracts a parameter as a float value -func (i Arguments) GetFloat(key string) (float64, error) { - val, err := i.Get(key) - if err != nil { - return 0, err - } - - floatval, ok := val.(float64) - if !ok { - return 0, ErrInvalidType - } - - return floatval, nil -} - -// GetInt extracts a parameter as an int value -func (i Arguments) GetInt(key string) (int, error) { - floatval, err := i.GetFloat(key) - if err != nil { - return 0, err - } - - return int(floatval), nil -} - -// GetUint extracts a parameter as an uint value -func (i Arguments) GetUint(key string) (uint, error) { - floatval, err := i.GetFloat(key) - if err != nil { - return 0, err - } - - return uint(floatval), nil -} - -// GetString extracts a parameter as a string value -func (i Arguments) GetString(key string) (string, error) { - val, ok := i[key] - if !ok { - return "", ErrUnknownKey - } - - stringval, ok := val.(string) - if !ok { - return "", ErrInvalidType - } - - return stringval, nil -} - -// GetBool extracts a parameter as a bool value -func (i Arguments) GetBool(key string) (bool, error) { - val, ok := i[key] - if !ok { - return false, ErrUnknownKey - } - - boolval, ok := val.(bool) - if !ok { - return false, ErrInvalidType - } - - return boolval, nil -} diff --git a/api/error.defaults.go b/api/error.defaults.go new file mode 100644 index 0000000..b7cf3b0 --- /dev/null +++ b/api/error.defaults.go @@ -0,0 +1,78 @@ +package api + +var ( + // ErrorSuccess represents a generic successful controller execution + ErrorSuccess = func() Error { return Error{0, "all right", nil} } + + // ErrorFailure is the most generic error + ErrorFailure = func() Error { return Error{1, "it failed", nil} } + + // ErrorUnknown represents any error which cause is unknown. + // It might also be used for debug purposes as this error + // has to be used the less possible + ErrorUnknown = func() Error { return Error{-1, "", nil} } + + // ErrorNoMatchFound has to be set when trying to fetch data and there is no result + ErrorNoMatchFound = func() Error { return Error{2, "no resource found", nil} } + + // ErrorAlreadyExists has to be set when trying to insert data, but identifiers or + // unique fields already exists + ErrorAlreadyExists = func() Error { return Error{3, "resource already exists", nil} } + + // ErrorConfig has to be set when there is a configuration error + ErrorConfig = func() Error { return Error{4, "configuration error", nil} } + + // ErrorUpload has to be set when a file upload failed + ErrorUpload = func() Error { return Error{100, "upload failed", nil} } + + // ErrorDownload has to be set when a file download failed + ErrorDownload = func() Error { return Error{101, "download failed", nil} } + + // MissingDownloadHeaders has to be set when the implementation + // of a controller of type 'download' (which returns a file instead of + // a set or output fields) is missing its HEADER field + MissingDownloadHeaders = func() Error { return Error{102, "download headers are missing", nil} } + + // ErrorMissingDownloadBody has to be set when the implementation + // of a controller of type 'download' (which returns a file instead of + // a set or output fields) is missing its BODY field + ErrorMissingDownloadBody = func() Error { return Error{103, "download body is missing", nil} } + + // ErrorUnknownService is set when there is no controller matching + // the http request URI. + ErrorUnknownService = func() Error { return Error{200, "unknown service", nil} } + + // ErrorUnknownMethod is set when there is no method matching the + // request's http method + ErrorUnknownMethod = func() Error { return Error{201, "unknown method", nil} } + + // ErrorUncallableService is set when there the requested controller's + // implementation (plugin file) is not found/callable + // ErrorUncallableService = func() Error { return Error{202, "uncallable service", nil} } + + // ErrorUncallableMethod is set when there the requested controller's + // implementation does not features the requested method + // ErrorUncallableMethod = func() Error { return Error{203, "uncallable method", nil} } + + // ErrorPermission is set when there is a permission error by default + // the api returns a permission error when the current scope (built + // by middlewares) does not match the scope required in the config. + // You can add your own permission policy and use this error + ErrorPermission = func() Error { return Error{300, "permission error", nil} } + + // ErrorToken has to be set (usually in authentication middleware) to tell + // the user that this authentication token is expired or invalid + ErrorToken = func() Error { return Error{301, "token error", nil} } + + // ErrorMissingParam is set when a *required* parameter is missing from the + // http request + ErrorMissingParam = func() Error { return Error{400, "missing parameter", nil} } + + // ErrorInvalidParam is set when a given parameter fails its type check as + // defined in the config file. + ErrorInvalidParam = func() Error { return Error{401, "invalid parameter", nil} } + + // ErrorInvalidDefaultParam is set when an optional parameter's default value + // does not match its type. + ErrorInvalidDefaultParam = func() Error { return Error{402, "invalid default param", nil} } +) diff --git a/api/error.go b/api/error.go new file mode 100644 index 0000000..8c69727 --- /dev/null +++ b/api/error.go @@ -0,0 +1,33 @@ +package api + +import ( + "fmt" +) + +// Error represents an http response error following the api format. +// These are used by the controllers to set the *execution status* +// directly into the response as JSON alongside response output fields. +type Error struct { + Code int `json:"error"` + Reason string `json:"reason"` + Arguments []interface{} `json:"error_args"` +} + +// Put adds an argument to the error +// to be displayed back to API caller +func (e *Error) Put(arg interface{}) { + + /* (1) Make slice if not */ + if e.Arguments == nil { + e.Arguments = make([]interface{}, 0) + } + + /* (2) Append argument */ + e.Arguments = append(e.Arguments, arg) + +} + +// Implements 'error' +func (e Error) Error() string { + return fmt.Sprintf("[%d] %s", e.Code, e.Reason) +} diff --git a/api/handler.go b/api/handler.go new file mode 100644 index 0000000..b58d3ce --- /dev/null +++ b/api/handler.go @@ -0,0 +1,39 @@ +package api + +import ( + "strings" +) + +// HandlerFunc manages an API request +type HandlerFunc func(Request, *Response) + +// Handler is an API handler ready to be bound +type Handler struct { + path string + method string + handle HandlerFunc +} + +// NewHandler builds a handler from its http method and path +func NewHandler(method, path string, handlerFunc HandlerFunc) *Handler { + return &Handler{ + path: path, + method: strings.ToUpper(method), + handle: handlerFunc, + } +} + +// Handle fires a handler +func (h *Handler) Handle(req Request, res *Response) { + h.handle(req, res) +} + +// 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/internal/request/request.go b/api/request.go similarity index 56% rename from internal/request/request.go rename to api/request.go index 42973b5..6fd8cc2 100644 --- a/internal/request/request.go +++ b/api/request.go @@ -1,40 +1,40 @@ -package request +package api import ( "net/http" "strings" ) +// RequestParam defines input parameters of an api request +type RequestParam map[string]interface{} + // Request represents an API request i.e. HTTP type Request struct { // corresponds to the list of uri components - // featuring in the request URI + // featured in the request URI URI []string - // controller path (portion of 'Uri') - Path []string + // original HTTP request + Request *http.Request - // contains all data from URL, GET, and FORM - Data *DataSet + // input parameters + Param RequestParam } -// New builds an interface request from a http.Request -func New(req *http.Request) (*Request, error) { +// NewRequest builds an interface request from a http.Request +func NewRequest(req *http.Request) (*Request, error) { - /* (1) Get useful data */ + // 1. get useful data uri := normaliseURI(req.URL.Path) uriparts := strings.Split(uri, "/") - /* (2) Init request */ + // 3. Init request inst := &Request{ - URI: uriparts, - Path: make([]string, 0, len(uriparts)), - Data: NewDataset(), + URI: uriparts, + Request: req, + Param: make(RequestParam), } - /* (3) Build dataset */ - inst.Data.Build(req) - return inst, nil } diff --git a/api/response.go b/api/response.go index 27d0030..7c7cdb3 100644 --- a/api/response.go +++ b/api/response.go @@ -1,30 +1,47 @@ package api import ( - "git.xdrm.io/go/aicra/err" + "encoding/json" + "net/http" ) -// New creates an empty response +// ResponseData defines format for response parameters to return +type ResponseData map[string]interface{} + +// Response represents an API response to be sent +type Response struct { + Data ResponseData + Headers http.Header + Err Error +} + +// NewResponse creates an empty response func NewResponse() *Response { return &Response{ - data: make(map[string]interface{}), - Err: err.Success, + Data: make(ResponseData), + Err: ErrorFailure(), } } -// Set adds/overrides a new response field -func (i *Response) Set(name string, value interface{}) { - i.data[name] = value +// SetData adds/overrides a new response field +func (i *Response) SetData(name string, value interface{}) { + i.Data[name] = value } -// Get gets a response field -func (i *Response) Get(name string) interface{} { - value, _ := i.data[name] +// GetData gets a response field +func (i *Response) GetData(name string) interface{} { + value, _ := i.Data[name] return value } -// Dump gets all key/value pairs -func (i *Response) Dump() map[string]interface{} { - return i.data +type jsonResponse struct { + Error + ResponseData +} + +// MarshalJSON implements the 'json.Marshaler' interface and is used +// to generate the JSON representation of the response +func (i *Response) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonResponse{i.Err, i.Data}) } diff --git a/api/types.go b/api/types.go deleted file mode 100644 index 7faef5d..0000000 --- a/api/types.go +++ /dev/null @@ -1,14 +0,0 @@ -package api - -import ( - "git.xdrm.io/go/aicra/err" -) - -// Arguments contains all key-value arguments -type Arguments map[string]interface{} - -// Response represents an API response to be sent -type Response struct { - data map[string]interface{} - Err err.Error -} diff --git a/driver/generic.go b/driver/generic.go deleted file mode 100644 index c44fcae..0000000 --- a/driver/generic.go +++ /dev/null @@ -1,51 +0,0 @@ -package driver - -import ( - "path/filepath" -) - -// Generic tells the aicra instance to use the generic driver to load controller/middleware executables -// -// It will call an executable with the json input into the standard input (argument 1) -// the HTTP method is send as the key _HTTP_METHOD_ (in upper case) -// The standard output must be a json corresponding to the data -type Generic struct{} - -// Name returns the driver name -func (d *Generic) Name() string { return "generic" } - -// Path returns the universal path from the source path -func (d Generic) Path(_root, _folder, _src string) string { - return _src -} - -// Source returns the source path from the universal path -func (d Generic) Source(_root, _folder, _path string) string { - return filepath.Join(_root, _folder, _path) - -} - -// Build returns the build path from the universal path -func (d Generic) Build(_root, _folder, _path string) string { - return filepath.Join(_root, _folder, _path) -} - -// Compiled returns whether the driver has to be build -func (d Generic) Compiled() bool { return false } - -// LoadController implements the Driver interface -func (d *Generic) LoadController(_path string) (Controller, error) { - return genericController(_path), nil -} - -// LoadMiddleware returns a new middleware; it must be a -// valid and existing folder/filename file -func (d *Generic) LoadMiddleware(_path string) (Middleware, error) { - return genericMiddleware(_path), nil -} - -// LoadChecker returns a new middleware; it must be a -// valid and existing folder/filename file -func (d *Generic) LoadChecker(_path string) (Checker, error) { - return genericChecker(_path), nil -} diff --git a/driver/generic.mockup.go b/driver/generic.mockup.go deleted file mode 100644 index 8d27301..0000000 --- a/driver/generic.mockup.go +++ /dev/null @@ -1,178 +0,0 @@ -package driver - -import ( - "encoding/json" - "git.xdrm.io/go/aicra/api" - e "git.xdrm.io/go/aicra/err" - "net/http" - "os/exec" - "strings" -) - -// genericController is the mockup for returning a controller with as a string the path -type genericController string - -func (path genericController) Get(d api.Arguments) api.Response { - - res := api.NewResponse() - - /* (1) Prepare stdin data */ - stdin, err := json.Marshal(d) - if err != nil { - res.Err = e.UncallableController - return *res - } - - // extract HTTP method - rawMethod, ok := d["_HTTP_METHOD_"] - if !ok { - res.Err = e.UncallableController - return *res - } - method, ok := rawMethod.(string) - if !ok { - res.Err = e.UncallableController - return *res - } - - /* (2) Try to load command with -> stdout */ - cmd := exec.Command(string(path), method, string(stdin)) - - stdout, err := cmd.Output() - if err != nil { - res.Err = e.UncallableController - return *res - } - - /* (3) Get output json */ - var outputI interface{} - err = json.Unmarshal(stdout, &outputI) - if err != nil { - res.Err = e.UncallableController - return *res - } - - output, ok := outputI.(map[string]interface{}) - if !ok { - res.Err = e.UncallableController - return *res - } - - res.Err = e.Success - - // extract error (success by default or on error) - if outErr, ok := output["error"]; ok { - errCode, ok := outErr.(float64) - if ok { - res.Err = e.Error{Code: int(errCode), Reason: "unknown reason", Arguments: nil} - } - - delete(output, "error") - } - - /* (4) fill response */ - for k, v := range output { - res.Set(k, v) - } - - return *res - -} - -func (path genericController) Post(d api.Arguments) api.Response { - return path.Get(d) -} -func (path genericController) Put(d api.Arguments) api.Response { - return path.Get(d) -} -func (path genericController) Delete(d api.Arguments) api.Response { - return path.Get(d) -} - -// genericMiddleware is the mockup for returning a middleware as a string (its path) -type genericMiddleware string - -func (path genericMiddleware) Inspect(_req http.Request, _scope *[]string) { - - /* (1) Prepare stdin data */ - stdin, err := json.Marshal(_scope) - if err != nil { - return - } - - /* (2) Try to load command with -> stdout */ - cmd := exec.Command(string(path), string(stdin)) - - stdout, err := cmd.Output() - if err != nil { - return - } - - /* (3) Get output json */ - var outputI interface{} - err = json.Unmarshal(stdout, &outputI) - if err != nil { - return - } - - /* (4) Get as []string */ - scope, ok := outputI.([]interface{}) - if !ok { - return - } - - /* (5) Try to add each value to the scope */ - for _, v := range scope { - stringScope, ok := v.(string) - if !ok { - continue - } - *_scope = append(*_scope, stringScope) - } - -} - -// genericChecker is the mockup for returning a checker as a string (its path) -type genericChecker string - -func (path genericChecker) Match(_type string) bool { - - /* (1) Try to load command with -> stdout */ - cmd := exec.Command(string(path), "MATCH", _type) - - stdout, err := cmd.Output() - if err != nil { - return false - } - - /* (2) Parse output */ - output := strings.ToLower(strings.Trim(string(stdout), " \t\r\n")) - - return output == "true" || output == "1" - -} -func (path genericChecker) Check(_value interface{}) bool { - - /* (1) Prepare stdin data */ - indata := make(map[string]interface{}) - indata["value"] = _value - - stdin, err := json.Marshal(indata) - if err != nil { - return false - } - - /* (2) Try to load command with -> stdout */ - cmd := exec.Command(string(path), "CHECK", string(stdin)) - - stdout, err := cmd.Output() - if err != nil { - return false - } - - /* (2) Parse output */ - output := strings.ToLower(strings.Trim(string(stdout), " \t\r\n")) - - return output == "true" || output == "1" - -} diff --git a/driver/plugin.go b/driver/plugin.go deleted file mode 100644 index 114b7ea..0000000 --- a/driver/plugin.go +++ /dev/null @@ -1,117 +0,0 @@ -package driver - -import ( - "fmt" - "path/filepath" - "plugin" -) - -// Plugin tells the aicra instance to use the plugin driver to load controller/middleware executables -// -// It will load go .so plugins with the following interface : -// -// type Controller interface { -// Get(d i.Arguments, r *i.Response) i.Response -// Post(d i.Arguments, r *i.Response) i.Response -// Put(d i.Arguments, r *i.Response) i.Response -// Delete(d i.Arguments, r *i.Response) i.Response -// } -// -// The controllers are exported by calling the 'Export() Controller' method -type Plugin struct{} - -// Name returns the driver name -func (d Plugin) Name() string { return "plugin" } - -// Path returns the universal path from the source path -func (d Plugin) Path(_root, _folder, _src string) string { - return filepath.Dir(_src) -} - -// Source returns the source path from the universal path -func (d Plugin) Source(_root, _folder, _path string) string { - - return filepath.Join(_root, _folder, _path, "main.go") - -} - -// Build returns the build path from the universal path -func (d Plugin) Build(_root, _folder, _path string) string { - if _path == "" { - return fmt.Sprintf("%s", filepath.Join(_root, ".build", _folder, "ROOT.so")) - } - return fmt.Sprintf("%s.so", filepath.Join(_root, ".build", _folder, _path)) -} - -// Compiled returns whether the driver has to be build -func (d Plugin) Compiled() bool { return true } - -// LoadController returns a new Controller -func (d *Plugin) LoadController(_path string) (Controller, error) { - - /* 1. Try to load plugin */ - p, err := plugin.Open(_path) - if err != nil { - return nil, err - } - - /* 2. Try to extract exported field */ - m, err := p.Lookup("Export") - if err != nil { - return nil, err - } - - exporter, ok := m.(func() Controller) - if !ok { - return nil, err - } - - /* 3. Controller */ - return exporter(), nil -} - -// LoadMiddleware returns a new Middleware -func (d *Plugin) LoadMiddleware(_path string) (Middleware, error) { - - /* 1. Try to load plugin */ - p, err := plugin.Open(_path) - if err != nil { - return nil, err - } - - /* 2. Try to extract exported field */ - m, err := p.Lookup("Export") - if err != nil { - return nil, err - } - - exporter, ok := m.(func() Middleware) - if !ok { - return nil, err - } - - return exporter(), nil -} - -// LoadChecker returns a new Checker -func (d *Plugin) LoadChecker(_path string) (Checker, error) { - - /* 1. Try to load plugin */ - p, err := plugin.Open(_path) - if err != nil { - return nil, err - } - - /* 2. Try to extract exported field */ - m, err := p.Lookup("Export") - if err != nil { - return nil, err - } - - exporter, ok := m.(func() Checker) - if !ok { - return nil, err - } - - return exporter(), nil -} diff --git a/driver/types.go b/driver/types.go deleted file mode 100644 index f2d98f8..0000000 --- a/driver/types.go +++ /dev/null @@ -1,40 +0,0 @@ -package driver - -import ( - "git.xdrm.io/go/aicra/api" - "net/http" -) - -// Driver defines the driver interface to load controller/middleware implementation or executables -type Driver interface { - Name() string - Path(string, string, string) string - Source(string, string, string) string - Build(string, string, string) string - Compiled() bool - - LoadController(_path string) (Controller, error) - LoadMiddleware(_path string) (Middleware, error) - LoadChecker(_path string) (Checker, error) -} - -// Checker is the interface that type checkers implementation must follow -type Checker interface { - Match(string) bool - Check(interface{}) bool -} - -// Controller is the interface that controller implementation must follow -// it is used by the 'Import' driver -type Controller interface { - Get(d api.Arguments) api.Response - Post(d api.Arguments) api.Response - Put(d api.Arguments) api.Response - Delete(d api.Arguments) api.Response -} - -// Middleware is the interface that middleware implementation must follow -// it is used by the 'Import' driver -type Middleware interface { - Inspect(http.Request, *[]string) -} diff --git a/err/defaults.go b/err/defaults.go deleted file mode 100644 index df3f138..0000000 --- a/err/defaults.go +++ /dev/null @@ -1,78 +0,0 @@ -package err - -var ( - // Success represents a generic successful controller execution - Success = Error{0, "all right", nil} - - // Failure is the most generic error - Failure = Error{1, "it failed", nil} - - // Unknown represents any error which cause is unknown. - // It might also be used for debug purposes as this error - // has to be used the less possible - Unknown = Error{-1, "", nil} - - // NoMatchFound has to be set when trying to fetch data and there is no result - NoMatchFound = Error{2, "no resource found", nil} - - // AlreadyExists has to be set when trying to insert data, but identifiers or - // unique fields already exists - AlreadyExists = Error{3, "resource already exists", nil} - - // Config has to be set when there is a configuration error - Config = Error{4, "configuration error", nil} - - // Upload has to be set when a file upload failed - Upload = Error{100, "upload failed", nil} - - // Download has to be set when a file download failed - Download = Error{101, "download failed", nil} - - // MissingDownloadHeaders has to be set when the implementation - // of a controller of type 'download' (which returns a file instead of - // a set or output fields) is missing its HEADER field - MissingDownloadHeaders = Error{102, "download headers are missing", nil} - - // MissingDownloadBody has to be set when the implementation - // of a controller of type 'download' (which returns a file instead of - // a set or output fields) is missing its BODY field - MissingDownloadBody = Error{103, "download body is missing", nil} - - // UnknownController is set when there is no controller matching - // the http request URI. - UnknownController = Error{200, "unknown controller", nil} - - // UnknownMethod is set when there is no method matching the - // request's http method - UnknownMethod = Error{201, "unknown method", nil} - - // UncallableController is set when there the requested controller's - // implementation (plugin file) is not found/callable - UncallableController = Error{202, "uncallable controller", nil} - - // UncallableMethod is set when there the requested controller's - // implementation does not features the requested method - UncallableMethod = Error{203, "uncallable method", nil} - - // Permission is set when there is a permission error by default - // the api returns a permission error when the current scope (built - // by middlewares) does not match the scope required in the config. - // You can add your own permission policy and use this error - Permission = Error{300, "permission error", nil} - - // Token has to be set (usually in authentication middleware) to tell - // the user that this authentication token is expired or invalid - Token = Error{301, "token error", nil} - - // MissingParam is set when a *required* parameter is missing from the - // http request - MissingParam = Error{400, "missing parameter", nil} - - // InvalidParam is set when a given parameter fails its type check as - // defined in the config file. - InvalidParam = Error{401, "invalid parameter", nil} - - // InvalidDefaultParam is set when an optional parameter's default value - // does not match its type. - InvalidDefaultParam = Error{402, "invalid default param", nil} -) diff --git a/err/interface.go b/err/interface.go deleted file mode 100644 index 85a758c..0000000 --- a/err/interface.go +++ /dev/null @@ -1,56 +0,0 @@ -package err - -import ( - "encoding/json" - "fmt" -) - -// Error represents an http response error following the api format. -// These are used by the controllers to set the *execution status* -// directly into the response as JSON alongside response output fields. -type Error struct { - Code int - Reason string - Arguments []interface{} -} - -// Put adds an argument to the error -// to be displayed back to API caller -func (e *Error) Put(arg interface{}) { - - /* (1) Make slice if not */ - if e.Arguments == nil { - e.Arguments = make([]interface{}, 0) - } - - /* (2) Append argument */ - e.Arguments = append(e.Arguments, arg) - -} - -// Implements 'error' -func (e Error) Error() string { - - return fmt.Sprintf("[%d] %s", e.Code, e.Reason) - -} - -// MarshalJSON implements the 'json.Marshaler' interface and is used -// to generate the JSON representation of the error data -func (e Error) MarshalJSON() ([]byte, error) { - - var jsonArguments string - - /* (1) Marshal 'Arguments' if set */ - if e.Arguments != nil && len(e.Arguments) > 0 { - argRepresentation, err := json.Marshal(e.Arguments) - if err == nil { - jsonArguments = fmt.Sprintf(",\"arguments\":%s", argRepresentation) - } - - } - - /* (2) Render JSON manually */ - return []byte(fmt.Sprintf("{\"error\":%d,\"reason\":\"%s\"%s}", e.Code, e.Reason, jsonArguments)), nil - -} diff --git a/internal/clifmt/colors.go b/internal/clifmt/colors.go deleted file mode 100644 index eeb9d50..0000000 --- a/internal/clifmt/colors.go +++ /dev/null @@ -1,16 +0,0 @@ -package clifmt - -import ( - "fmt" -) - -// Color returns a bash-formatted string representing -// the string @text with the color code @color and in bold -// if @bold (1 optional argument) is set to true -func Color(color byte, text string, bold ...bool) string { - b := "0" - if len(bold) > 0 && bold[0] { - b = "1" - } - return fmt.Sprintf("\033[%s;%dm%s\033[0m", b, color, text) -} diff --git a/internal/clifmt/symbols.go b/internal/clifmt/symbols.go deleted file mode 100644 index d9e1619..0000000 --- a/internal/clifmt/symbols.go +++ /dev/null @@ -1,57 +0,0 @@ -package clifmt - -import ( - "fmt" - "strings" -) - -var titleIndex = 0 -var alignOffset = 30 - -// Warn returns a red warning ASCII sign. If a string is given -// as argument, it will print it after the warning sign -func Warn(s ...string) string { - if len(s) == 0 { - return Color(31, "/!\\") - } - - return fmt.Sprintf("%s %s", Warn(), s[0]) -} - -// Info returns a blue info ASCII sign. If a string is given -// as argument, it will print it after the info sign -func Info(s ...string) string { - if len(s) == 0 { - return Color(34, "(!)") - } - - return fmt.Sprintf("%s %s", Info(), s[0]) -} - -// Title prints a formatted title (auto-indexed from local counted) -func Title(s string) { - titleIndex++ - fmt.Printf("\n%s |%d| %s %s\n", Color(33, ">>", false), titleIndex, s, Color(33, "<<", false)) - -} - -// Align prints strings with space padding to align line ends (fixed width) -func Align(s string) { - - // 1. print string - fmt.Printf("%s", s) - - // 2. get actual size - size := len(s) - - // 3. remove \033[XYm format characters - size -= (len(strings.Split(s, "\033")) - 0) * 6 - - // 3. add 1 char for each \033[0m - size += len(strings.Split(s, "\033[0m")) - 1 - - // 4. print trailing spaces - for i := size; i < alignOffset; i++ { - fmt.Printf(" ") - } -} diff --git a/internal/config/method.go b/internal/config/method.go index 4f3ce7a..f570b84 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -3,8 +3,6 @@ package config import ( "fmt" "strings" - - "git.xdrm.io/go/aicra/middleware" ) // checkAndFormat checks for errors and missing fields and sets default values for optional fields. @@ -84,29 +82,6 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e return nil } -// CheckScope returns whether a given scope matches the method configuration. -// The scope format is: `[ [a,b], [c], [d,e] ]` where the first level is a bitwise `OR` and the second a bitwise `AND` -func (methodDef *Method) CheckScope(scope middleware.Scope) bool { - - for _, OR := range methodDef.Permission { - granted := true - - for _, AND := range OR { - if !scopeHasPermission(AND, scope) { - granted = false - break - } - } - - // if one is valid -> grant - if granted { - return true - } - } - - return false -} - // scopeHasPermission returns whether the permission fulfills a given scope func scopeHasPermission(permission string, scope []string) bool { for _, s := range scope { diff --git a/internal/request/dataset.go b/internal/reqdata/dataset.go similarity index 85% rename from internal/request/dataset.go rename to internal/reqdata/dataset.go index 79891c1..7d197d2 100644 --- a/internal/request/dataset.go +++ b/internal/reqdata/dataset.go @@ -1,30 +1,31 @@ -package request +package reqdata import ( "encoding/json" "fmt" "log" - "net/http" - "strings" "git.xdrm.io/go/aicra/internal/multipart" + + "net/http" + "strings" ) -// DataSet represents all data that can be caught: +// Store represents all data that can be caught: // - URI (guessed from the URI by removing the controller path) // - GET (default url data) // - POST (from json, form-data, url-encoded) -type DataSet struct { +type Store struct { // ordered values from the URI // catches all after the controller path // - // points to DataSet.Data + // points to Store.Data URI []*Parameter // uri parameters following the QUERY format // - // points to DataSet.Data + // points to Store.Data Get map[string]*Parameter // form data depending on the Content-Type: @@ -32,7 +33,7 @@ type DataSet struct { // 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters // 'multipart/form-data' => parse form-data format // - // points to DataSet.Data + // points to Store.Data Form map[string]*Parameter // contains URL+GET+FORM data with prefixes: @@ -42,37 +43,33 @@ type DataSet struct { Set map[string]*Parameter } -// NewDataset creates an empty request dataset -func NewDataset() *DataSet { - return &DataSet{ +// New creates a new store from an http request. +func New(req *http.Request) *Store { + ds := &Store{ URI: make([]*Parameter, 0), Get: make(map[string]*Parameter), Form: make(map[string]*Parameter), Set: make(map[string]*Parameter), } -} + // 1. GET (query) data + ds.fetchGet(req) -// Build builds a 'DataSet' from an http request -func (i *DataSet) Build(req *http.Request) { - - /* (1) GET (query) data */ - i.fetchGet(req) - - /* (2) We are done if GET method */ - if req.Method == "GET" { - return + // 2. We are done if GET method + if req.Method == http.MethodGet { + return ds } - /* (3) POST (body) data */ - i.fetchForm(req) + // 2. POST (body) data + ds.fetchForm(req) + return ds } -// SetURI stores URL data and fills 'Set' +// SetURIParameters stores URL orderedURIParams and fills 'Set' // with creating pointers inside 'Url' -func (i *DataSet) SetURI(data []string) { +func (i *Store) SetURIParameters(orderedUParams []string) { - for index, value := range data { + for index, value := range orderedUParams { // create set index setindex := fmt.Sprintf("URL#%d", index) @@ -91,7 +88,7 @@ func (i *DataSet) SetURI(data []string) { } // fetchGet stores data from the QUERY (in url parameters) -func (i *DataSet) fetchGet(req *http.Request) { +func (i *Store) fetchGet(req *http.Request) { for name, value := range req.URL.Query() { @@ -128,7 +125,7 @@ func (i *DataSet) fetchGet(req *http.Request) { // - parse 'form-data' if not supported (not POST requests) // - parse 'x-www-form-urlencoded' // - parse 'application/json' -func (i *DataSet) fetchForm(req *http.Request) { +func (i *Store) fetchForm(req *http.Request) { contentType := req.Header.Get("Content-Type") @@ -155,7 +152,7 @@ func (i *DataSet) fetchForm(req *http.Request) { // parseJSON parses JSON from the request body inside 'Form' // and 'Set' -func (i *DataSet) parseJSON(req *http.Request) { +func (i *Store) parseJSON(req *http.Request) { parsed := make(map[string]interface{}, 0) @@ -197,7 +194,7 @@ func (i *DataSet) parseJSON(req *http.Request) { // parseUrlencoded parses urlencoded from the request body inside 'Form' // and 'Set' -func (i *DataSet) parseUrlencoded(req *http.Request) { +func (i *Store) parseUrlencoded(req *http.Request) { // use http.Request interface if err := req.ParseForm(); err != nil { @@ -233,7 +230,7 @@ func (i *DataSet) parseUrlencoded(req *http.Request) { // parseMultipart parses multi-part from the request body inside 'Form' // and 'Set' -func (i *DataSet) parseMultipart(req *http.Request) { +func (i *Store) parseMultipart(req *http.Request) { /* (1) Create reader */ boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] diff --git a/internal/request/param_reflect.go b/internal/reqdata/param_reflect.go similarity index 99% rename from internal/request/param_reflect.go rename to internal/reqdata/param_reflect.go index 2adf371..6cbd403 100644 --- a/internal/request/param_reflect.go +++ b/internal/reqdata/param_reflect.go @@ -1,4 +1,4 @@ -package request +package reqdata import ( "encoding/json" diff --git a/internal/request/parameter.go b/internal/reqdata/parameter.go similarity index 97% rename from internal/request/parameter.go rename to internal/reqdata/parameter.go index 4848cd6..71092e7 100644 --- a/internal/request/parameter.go +++ b/internal/reqdata/parameter.go @@ -1,4 +1,4 @@ -package request +package reqdata // Parameter represents an http request parameter // that can be of type URL, GET, or FORM (multipart, json, urlencoded) diff --git a/middleware/public.go b/middleware/public.go deleted file mode 100644 index 7596573..0000000 --- a/middleware/public.go +++ /dev/null @@ -1,31 +0,0 @@ -package middleware - -import ( - "git.xdrm.io/go/aicra/driver" - "net/http" -) - -// CreateRegistry creates an empty registry -func CreateRegistry() Registry { - return make(Registry) -} - -// Add adds a new middleware for a path -func (reg Registry) Add(_path string, _element driver.Middleware) { - reg[_path] = _element -} - -// Run executes all middlewares (default browse order) -func (reg Registry) Run(req http.Request) []string { - - /* (1) Initialise scope */ - scope := make([]string, 0) - - /* (2) Execute each middleware */ - for _, mw := range reg { - mw.Inspect(req, &scope) - } - - return scope - -} diff --git a/middleware/types.go b/middleware/types.go deleted file mode 100644 index 09eb417..0000000 --- a/middleware/types.go +++ /dev/null @@ -1,18 +0,0 @@ -package middleware - -import ( - "git.xdrm.io/go/aicra/driver" -) - -// Scope represents a list of scope processed by middlewares -// and used by the router to block/allow some uris -// it is also passed to controllers -// -// DISCLAIMER: it is used to help developers but for compatibility -// purposes, the type is always used as its definition ([]string) -type Scope []string - -// Registry represents a registry containing all registered -// middlewares to be processed before routing any request -// The map is => -type Registry map[string]driver.Middleware diff --git a/server.go b/server.go index e717440..374cd4e 100644 --- a/server.go +++ b/server.go @@ -1,106 +1,55 @@ package aicra import ( - "errors" "log" "net/http" - "path/filepath" + "os" "strings" "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/aicra/driver" - e "git.xdrm.io/go/aicra/err" - "git.xdrm.io/go/aicra/internal/apidef" - "git.xdrm.io/go/aicra/internal/checker" + "git.xdrm.io/go/aicra/internal/config" - apirequest "git.xdrm.io/go/aicra/internal/request" - "git.xdrm.io/go/aicra/middleware" + "git.xdrm.io/go/aicra/internal/reqdata" + checker "git.xdrm.io/go/aicra/typecheck" ) -// Server represents an AICRA instance featuring: -// * its type checkers -// * its middlewares -// * its controllers (api config) +// Server represents an AICRA instance featuring: type checkers, services type Server struct { - controller *apidef.Controller // controllers - checker checker.Registry // type checker registry - middleware middleware.Registry // middlewares - schema *config.Schema + services *config.Service + checkers *checker.Set + handlers []*api.Handler } // New creates a framework instance from a configuration file -// _path is the json configuration path -// _driver is used to load/run the controllers and middlewares (default: ) -// -func New(_path string) (*Server, error) { +func New(configPath string) (*Server, error) { - /* 1. Load config */ - schema, err := config.Parse("./aicra.json") - if err != nil { - return nil, err - } + var err error - /* 2. Init instance */ + // 1. init instance var i = &Server{ - controller: nil, - schema: schema, + services: nil, + checkers: checker.New(), + handlers: make([]*api.Handler, 0), } - /* 3. Load configuration */ - i.controller, err = apidef.Parse(_path) + // 2. open config file + configFile, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer configFile.Close() + + // 3. load configuration + i.services, err = config.Parse(configFile) if err != nil { return nil, err } - /* 4. Load type registry */ - i.checker = checker.CreateRegistry() + /* 3. Load type registry */ + // TODO: add methods on the checker to set types programmatically - // add default types if set - if schema.Types.Default { - - // driver is Plugin for defaults (even if generic for the controllers etc) - defaultTypesDriver := new(driver.Plugin) - files, err := filepath.Glob(filepath.Join(schema.Root, ".build/DEFAULT_TYPES/*.so")) - if err != nil { - return nil, errors.New("cannot load default types") - } - for _, path := range files { - - name := strings.TrimSuffix(filepath.Base(path), ".so") - - mwFunc, err := defaultTypesDriver.LoadChecker(path) - if err != nil { - log.Printf("cannot load default type checker '%s' | %s", name, err) - } - i.checker.Add(name, mwFunc) - - } - } - - // add custom types - for name, path := range schema.Types.Map { - - fullpath := schema.Driver.Build(schema.Root, schema.Types.Folder, path) - mwFunc, err := schema.Driver.LoadChecker(fullpath) - if err != nil { - log.Printf("cannot load type checker '%s' | %s", name, err) - } - i.checker.Add(path, mwFunc) - - } - - /* 5. Load middleware registry */ - i.middleware = middleware.CreateRegistry() - for name, path := range schema.Middlewares.Map { - - fullpath := schema.Driver.Build(schema.Root, schema.Middlewares.Folder, path) - mwFunc, err := schema.Driver.LoadMiddleware(fullpath) - if err != nil { - log.Printf("cannot load middleware '%s' | %s", name, err) - } - i.middleware.Add(path, mwFunc) - - } + /* 4. Load middleware registry */ + // TODO: add methods to set them manually return i, nil @@ -111,179 +60,91 @@ func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { defer req.Body.Close() - /* (1) Build request */ - apiRequest, err := apirequest.New(req) + // 1. build API request from HTTP request + apiRequest, err := api.NewRequest(req) if err != nil { log.Fatal(err) } - /* (2) Launch middlewares to build the scope */ - scope := s.middleware.Run(*req) - - /* (3) Find a matching controller */ - controller := s.matchController(apiRequest) - if controller == nil { + // 2. find a matching service for this path in the config + serviceDef, pathIndex := s.services.Browse(apiRequest.URI) + if serviceDef == nil { return } + servicePath := strings.Join(apiRequest.URI[:pathIndex], "/") - /* (4) Check if matching method exists */ - var method = controller.Method(req.Method) - + // 3. check if matching method exists in config */ + var method = serviceDef.Method(req.Method) if method == nil { - httpError(res, e.UnknownMethod) + httpError(res, api.ErrorUnknownMethod()) return } - /* (5) Check scope permissions */ - if !method.CheckScope(scope) { - httpError(res, e.Permission) - return - } + // 4. parse every input data from the request + store := reqdata.New(req) /* (4) Check parameters ---------------------------------------------------------*/ - parameters, paramError := s.extractParameters(apiRequest, method.Parameters) + parameters, paramError := s.extractParameters(store, method.Parameters) // Fail if argument check failed - if paramError.Code != e.Success.Code { + if paramError.Code != api.ErrorSuccess().Code { httpError(res, paramError) return } - /* (5) Load controller + apiRequest.Param = parameters + + /* (5) Search a matching handler ---------------------------------------------------------*/ - // get paths - ctlBuildPath := strings.Join(apiRequest.Path, "/") - ctlBuildPath = s.schema.Driver.Build(s.schema.Root, s.schema.Controllers.Folder, ctlBuildPath) + var serviceHandler *api.Handler + var serviceFound bool - // get controller - ctlObject, err := s.schema.Driver.LoadController(ctlBuildPath) - httpMethod := strings.ToUpper(req.Method) - if err != nil { - httpErr := e.UncallableController - httpErr.Put(err) - httpError(res, httpErr) - log.Printf("err( %s )\n", err) - return - } - - var ctlMethod func(api.Arguments) api.Response - // select method - switch httpMethod { - case "GET": - ctlMethod = ctlObject.Get - case "POST": - ctlMethod = ctlObject.Post - case "PUT": - ctlMethod = ctlObject.Put - case "DELETE": - ctlMethod = ctlObject.Delete - default: - httpError(res, e.UnknownMethod) - return - } - - /* (6) Execute and get response - ---------------------------------------------------------*/ - /* (1) Give HTTP METHOD */ - parameters["_HTTP_METHOD_"] = httpMethod - - /* (2) Give Authorization header into controller */ - parameters["_AUTHORIZATION_"] = req.Header.Get("Authorization") - - /* (3) Give Scope into controller */ - parameters["_SCOPE_"] = scope - - /* (4) Execute */ - response := ctlMethod(parameters) - - /* (5) Extract http headers */ - for k, v := range response.Dump() { - if k == "_REDIRECT_" { - if newLocation, ok := v.(string); ok { - httpRedirect(res, newLocation) + for _, handler := range s.handlers { + if handler.GetPath() == servicePath { + serviceFound = true + if handler.GetMethod() == req.Method { + serviceHandler = handler } - continue } } - /* (5) Build JSON response */ - httpPrint(res, response) + // fail if found no handler + if serviceHandler == nil { + if serviceFound { + httpError(res, api.ErrorUnknownMethod()) + return + } + httpError(res, api.ErrorUnknownService()) + return + } + + /* (6) Execute handler and return response + ---------------------------------------------------------*/ + // 1. execute + apiResponse := api.NewResponse() + serviceHandler.Handle(*apiRequest, apiResponse) + + // 2. apply headers + for key, values := range apiResponse.Headers { + for _, value := range values { + res.Header().Add(key, value) + } + } + + // 3. build JSON apiResponse + httpPrint(res, apiResponse) return } -// extractParameters extracts parameters for the request and checks -// every single one according to configuration options -func (s *Server) extractParameters(req *apirequest.Request, methodParam map[string]*apidef.Parameter) (map[string]interface{}, e.Error) { - - // init vars - err := e.Success - parameters := make(map[string]interface{}) - - // for each param of the config - for name, param := range methodParam { - - /* (1) Extract value */ - p, isset := req.Data.Set[name] - - /* (2) Required & missing */ - if !isset && !param.Optional { - err = e.MissingParam - err.Put(name) - return nil, err - } - - /* (3) Optional & missing: set default value */ - if !isset { - p = &apirequest.Parameter{ - Parsed: true, - File: param.Type == "FILE", - Value: nil, - } - if param.Default != nil { - p.Value = *param.Default - } - - // we are done - parameters[param.Rename] = p.Value - continue - } - - /* (4) Parse parameter if not file */ - if !p.File { - p.Parse() - } - - /* (5) Fail on unexpected multipart file */ - waitFile, gotFile := param.Type == "FILE", p.File - if gotFile && !waitFile || !gotFile && waitFile { - err = e.InvalidParam - err.Put(param.Rename) - err.Put("FILE") - return nil, err - } - - /* (6) Do not check if file */ - if gotFile { - parameters[param.Rename] = p.Value - continue - } - - /* (7) Check type */ - if s.checker.Run(param.Type, p.Value) != nil { - - err = e.InvalidParam - err.Put(param.Rename) - err.Put(param.Type) - err.Put(p.Value) - break - - } - - parameters[param.Rename] = p.Value - - } - - return parameters, err +// HandleFunc sets a new handler for an HTTP method to a path +func (s *Server) HandleFunc(httpMethod, path string, handlerFunc api.HandlerFunc) { + handler := api.NewHandler(httpMethod, path, handlerFunc) + s.handlers = append(s.handlers, handler) +} + +// Handle sets a new handler +func (s *Server) Handle(handler *api.Handler) { + s.handlers = append(s.handlers, handler) } diff --git a/util.go b/util.go index 153b21b..3b75b61 100644 --- a/util.go +++ b/util.go @@ -4,59 +4,111 @@ import ( "encoding/json" "log" "net/http" + "strings" "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/aicra/err" - "git.xdrm.io/go/aicra/internal/apidef" - apireq "git.xdrm.io/go/aicra/internal/request" + "git.xdrm.io/go/aicra/internal/config" + "git.xdrm.io/go/aicra/internal/reqdata" ) -func (s *Server) matchController(req *apireq.Request) *apidef.Controller { +func (s *Server) findServiceDef(req *api.Request) (serviceDef *config.Service, servicePath string) { - /* (1) Try to browse by URI */ - pathi, ctl := s.controller.Browse(req.URI) + // 1. try to find definition + serviceDef, pathi := s.services.Browse(req.URI) - /* (2) Set controller uri */ - req.Path = make([]string, 0, pathi) - req.Path = append(req.Path, req.URI[:pathi]...) - - /* (3) Extract & store URI params */ - req.Data.SetURI(req.URI[pathi:]) - - /* (4) Return controller */ - return ctl + // 2. set service uri + servicePath = strings.Join(req.URI[:pathi], "/") + return } -// Redirects to another location (http protocol) -func httpRedirect(r http.ResponseWriter, loc string) { - r.Header().Add("Location", loc) - r.WriteHeader(308) // permanent redirect +// extractParameters extracts parameters for the request and checks +// every single one according to configuration options +func (s *Server) extractParameters(store *reqdata.Store, methodParam map[string]*config.Parameter) (map[string]interface{}, api.Error) { + + // init vars + apiError := api.ErrorSuccess() + parameters := make(map[string]interface{}) + + // for each param of the config + for name, param := range methodParam { + + /* (1) Extract value */ + p, isset := store.Set[name] + + /* (2) Required & missing */ + if !isset && !param.Optional { + apiError = api.ErrorMissingParam() + apiError.Put(name) + return nil, apiError + } + + /* (3) Optional & missing: set default value */ + if !isset { + p = &reqdata.Parameter{ + Parsed: true, + File: param.Type == "FILE", + Value: nil, + } + if param.Default != nil { + p.Value = *param.Default + } + + // we are done + parameters[param.Rename] = p.Value + continue + } + + /* (4) Parse parameter if not file */ + if !p.File { + p.Parse() + } + + /* (5) Fail on unexpected multipart file */ + waitFile, gotFile := param.Type == "FILE", p.File + if gotFile && !waitFile || !gotFile && waitFile { + apiError = api.ErrorInvalidParam() + apiError.Put(param.Rename) + apiError.Put("FILE") + return nil, apiError + } + + /* (6) Do not check if file */ + if gotFile { + parameters[param.Rename] = p.Value + continue + } + + /* (7) Check type */ + if s.checkers.Run(param.Type, p.Value) != nil { + + apiError = api.ErrorInvalidParam() + apiError.Put(param.Rename) + apiError.Put(param.Type) + apiError.Put(p.Value) + break + + } + + parameters[param.Rename] = p.Value + + } + + return parameters, apiError } // Prints an HTTP response -func httpPrint(r http.ResponseWriter, res api.Response) { - // get response data - formattedResponse := res.Dump() - - // add error fields - formattedResponse["error"] = res.Err.Code - formattedResponse["reason"] = res.Err.Reason - - // add arguments if any - if res.Err.Arguments != nil && len(res.Err.Arguments) > 0 { - formattedResponse["args"] = res.Err.Arguments - } +func httpPrint(r http.ResponseWriter, res *api.Response) { // write this json - jsonResponse, _ := json.Marshal(formattedResponse) + jsonResponse, _ := json.Marshal(res) r.Header().Add("Content-Type", "application/json") r.Write(jsonResponse) } // Prints an error as HTTP response -func httpError(r http.ResponseWriter, e err.Error) { - JSON, _ := e.MarshalJSON() +func httpError(r http.ResponseWriter, e api.Error) { + JSON, _ := json.Marshal(e) r.Header().Add("Content-Type", "application/json") r.Write(JSON) log.Printf("[http.fail] %s\n", e.Reason)