ref 0: big refactor of concerns between api request, server, etc
This commit is contained in:
parent
0a63e9afcc
commit
7e66b6ddd5
|
@ -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
|
||||
}
|
|
@ -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} }
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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(),
|
||||
Request: req,
|
||||
Param: make(RequestParam),
|
||||
}
|
||||
|
||||
/* (3) Build dataset */
|
||||
inst.Data.Build(req)
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
|
@ -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})
|
||||
}
|
||||
|
|
14
api/types.go
14
api/types.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 <stdin> -> 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 <stdin> -> 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 <stdin> -> 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 <stdin> -> 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"
|
||||
|
||||
}
|
117
driver/plugin.go
117
driver/plugin.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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}
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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(" ")
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
// 2. We are done if GET method
|
||||
if req.Method == http.MethodGet {
|
||||
return ds
|
||||
}
|
||||
|
||||
// Build builds a 'DataSet' from an http request
|
||||
func (i *DataSet) Build(req *http.Request) {
|
||||
// 2. POST (body) data
|
||||
ds.fetchForm(req)
|
||||
|
||||
/* (1) GET (query) data */
|
||||
i.fetchGet(req)
|
||||
|
||||
/* (2) We are done if GET method */
|
||||
if req.Method == "GET" {
|
||||
return
|
||||
return ds
|
||||
}
|
||||
|
||||
/* (3) POST (body) data */
|
||||
i.fetchForm(req)
|
||||
|
||||
}
|
||||
|
||||
// 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="):]
|
|
@ -1,4 +1,4 @@
|
|||
package request
|
||||
package reqdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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 <name> => <middleware>
|
||||
type Registry map[string]driver.Middleware
|
295
server.go
295
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)
|
||||
for _, handler := range s.handlers {
|
||||
if handler.GetPath() == servicePath {
|
||||
serviceFound = true
|
||||
if handler.GetMethod() == req.Method {
|
||||
serviceHandler = handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fail if found no handler
|
||||
if serviceHandler == nil {
|
||||
if serviceFound {
|
||||
httpError(res, api.ErrorUnknownMethod())
|
||||
return
|
||||
}
|
||||
httpError(res, api.ErrorUnknownService())
|
||||
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
|
||||
/* (6) Execute handler and return response
|
||||
---------------------------------------------------------*/
|
||||
/* (1) Give HTTP METHOD */
|
||||
parameters["_HTTP_METHOD_"] = httpMethod
|
||||
// 1. execute
|
||||
apiResponse := api.NewResponse()
|
||||
serviceHandler.Handle(*apiRequest, apiResponse)
|
||||
|
||||
/* (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)
|
||||
}
|
||||
continue
|
||||
// 2. apply headers
|
||||
for key, values := range apiResponse.Headers {
|
||||
for _, value := range values {
|
||||
res.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/* (5) Build JSON response */
|
||||
httpPrint(res, response)
|
||||
// 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
|
||||
// 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)
|
||||
}
|
||||
|
||||
/* (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
|
||||
// Handle sets a new handler
|
||||
func (s *Server) Handle(handler *api.Handler) {
|
||||
s.handlers = append(s.handlers, handler)
|
||||
}
|
||||
|
|
116
util.go
116
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]...)
|
||||
// 2. set service uri
|
||||
servicePath = strings.Join(req.URI[:pathi], "/")
|
||||
|
||||
/* (3) Extract & store URI params */
|
||||
req.Data.SetURI(req.URI[pathi:])
|
||||
return
|
||||
}
|
||||
|
||||
/* (4) Return controller */
|
||||
return ctl
|
||||
// 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
|
||||
|
||||
}
|
||||
|
||||
// Redirects to another location (http protocol)
|
||||
func httpRedirect(r http.ResponseWriter, loc string) {
|
||||
r.Header().Add("Location", loc)
|
||||
r.WriteHeader(308) // permanent redirect
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue