ref 0: big refactor of concerns between api request, server, etc

This commit is contained in:
Adrien Marquès 2019-05-01 13:44:45 +02:00
parent 0a63e9afcc
commit 7e66b6ddd5
23 changed files with 396 additions and 1093 deletions

View File

@ -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
}

78
api/error.defaults.go Normal file
View File

@ -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} }
)

33
api/error.go Normal file
View File

@ -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)
}

39
api/handler.go Normal file
View File

@ -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
}

View File

@ -1,40 +1,40 @@
package request package api
import ( import (
"net/http" "net/http"
"strings" "strings"
) )
// RequestParam defines input parameters of an api request
type RequestParam map[string]interface{}
// Request represents an API request i.e. HTTP // Request represents an API request i.e. HTTP
type Request struct { type Request struct {
// corresponds to the list of uri components // corresponds to the list of uri components
// featuring in the request URI // featured in the request URI
URI []string URI []string
// controller path (portion of 'Uri') // original HTTP request
Path []string Request *http.Request
// contains all data from URL, GET, and FORM // input parameters
Data *DataSet Param RequestParam
} }
// New builds an interface request from a http.Request // NewRequest builds an interface request from a http.Request
func New(req *http.Request) (*Request, error) { func NewRequest(req *http.Request) (*Request, error) {
/* (1) Get useful data */ // 1. get useful data
uri := normaliseURI(req.URL.Path) uri := normaliseURI(req.URL.Path)
uriparts := strings.Split(uri, "/") uriparts := strings.Split(uri, "/")
/* (2) Init request */ // 3. Init request
inst := &Request{ inst := &Request{
URI: uriparts, URI: uriparts,
Path: make([]string, 0, len(uriparts)), Request: req,
Data: NewDataset(), Param: make(RequestParam),
} }
/* (3) Build dataset */
inst.Data.Build(req)
return inst, nil return inst, nil
} }

View File

@ -1,30 +1,47 @@
package api package api
import ( 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 { func NewResponse() *Response {
return &Response{ return &Response{
data: make(map[string]interface{}), Data: make(ResponseData),
Err: err.Success, Err: ErrorFailure(),
} }
} }
// Set adds/overrides a new response field // SetData adds/overrides a new response field
func (i *Response) Set(name string, value interface{}) { func (i *Response) SetData(name string, value interface{}) {
i.data[name] = value i.Data[name] = value
} }
// Get gets a response field // GetData gets a response field
func (i *Response) Get(name string) interface{} { func (i *Response) GetData(name string) interface{} {
value, _ := i.data[name] value, _ := i.Data[name]
return value return value
} }
// Dump gets all key/value pairs type jsonResponse struct {
func (i *Response) Dump() map[string]interface{} { Error
return i.data 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})
} }

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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}
)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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(" ")
}
}

View File

@ -3,8 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"strings" "strings"
"git.xdrm.io/go/aicra/middleware"
) )
// checkAndFormat checks for errors and missing fields and sets default values for optional fields. // 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 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 // scopeHasPermission returns whether the permission fulfills a given scope
func scopeHasPermission(permission string, scope []string) bool { func scopeHasPermission(permission string, scope []string) bool {
for _, s := range scope { for _, s := range scope {

View File

@ -1,30 +1,31 @@
package request package reqdata
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"net/http"
"strings"
"git.xdrm.io/go/aicra/internal/multipart" "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) // - URI (guessed from the URI by removing the controller path)
// - GET (default url data) // - GET (default url data)
// - POST (from json, form-data, url-encoded) // - POST (from json, form-data, url-encoded)
type DataSet struct { type Store struct {
// ordered values from the URI // ordered values from the URI
// catches all after the controller path // catches all after the controller path
// //
// points to DataSet.Data // points to Store.Data
URI []*Parameter URI []*Parameter
// uri parameters following the QUERY format // uri parameters following the QUERY format
// //
// points to DataSet.Data // points to Store.Data
Get map[string]*Parameter Get map[string]*Parameter
// form data depending on the Content-Type: // form data depending on the Content-Type:
@ -32,7 +33,7 @@ type DataSet struct {
// 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters // 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
// 'multipart/form-data' => parse form-data format // 'multipart/form-data' => parse form-data format
// //
// points to DataSet.Data // points to Store.Data
Form map[string]*Parameter Form map[string]*Parameter
// contains URL+GET+FORM data with prefixes: // contains URL+GET+FORM data with prefixes:
@ -42,37 +43,33 @@ type DataSet struct {
Set map[string]*Parameter Set map[string]*Parameter
} }
// NewDataset creates an empty request dataset // New creates a new store from an http request.
func NewDataset() *DataSet { func New(req *http.Request) *Store {
return &DataSet{ ds := &Store{
URI: make([]*Parameter, 0), URI: make([]*Parameter, 0),
Get: make(map[string]*Parameter), Get: make(map[string]*Parameter),
Form: make(map[string]*Parameter), Form: make(map[string]*Parameter),
Set: make(map[string]*Parameter), Set: make(map[string]*Parameter),
} }
} // 1. GET (query) data
ds.fetchGet(req)
// Build builds a 'DataSet' from an http request // 2. We are done if GET method
func (i *DataSet) Build(req *http.Request) { if req.Method == http.MethodGet {
return ds
/* (1) GET (query) data */
i.fetchGet(req)
/* (2) We are done if GET method */
if req.Method == "GET" {
return
} }
/* (3) POST (body) data */ // 2. POST (body) data
i.fetchForm(req) ds.fetchForm(req)
return ds
} }
// SetURI stores URL data and fills 'Set' // SetURIParameters stores URL orderedURIParams and fills 'Set'
// with creating pointers inside 'Url' // 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 // create set index
setindex := fmt.Sprintf("URL#%d", 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) // 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() { 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 'form-data' if not supported (not POST requests)
// - parse 'x-www-form-urlencoded' // - parse 'x-www-form-urlencoded'
// - parse 'application/json' // - parse 'application/json'
func (i *DataSet) fetchForm(req *http.Request) { func (i *Store) fetchForm(req *http.Request) {
contentType := req.Header.Get("Content-Type") 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' // parseJSON parses JSON from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *DataSet) parseJSON(req *http.Request) { func (i *Store) parseJSON(req *http.Request) {
parsed := make(map[string]interface{}, 0) 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' // parseUrlencoded parses urlencoded from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *DataSet) parseUrlencoded(req *http.Request) { func (i *Store) parseUrlencoded(req *http.Request) {
// use http.Request interface // use http.Request interface
if err := req.ParseForm(); err != nil { 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' // parseMultipart parses multi-part from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *DataSet) parseMultipart(req *http.Request) { func (i *Store) parseMultipart(req *http.Request) {
/* (1) Create reader */ /* (1) Create reader */
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]

View File

@ -1,4 +1,4 @@
package request package reqdata
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,4 +1,4 @@
package request package reqdata
// Parameter represents an http request parameter // Parameter represents an http request parameter
// that can be of type URL, GET, or FORM (multipart, json, urlencoded) // that can be of type URL, GET, or FORM (multipart, json, urlencoded)

View File

@ -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
}

View File

@ -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

301
server.go
View File

@ -1,106 +1,55 @@
package aicra package aicra
import ( import (
"errors"
"log" "log"
"net/http" "net/http"
"path/filepath" "os"
"strings" "strings"
"git.xdrm.io/go/aicra/api" "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" "git.xdrm.io/go/aicra/internal/config"
apirequest "git.xdrm.io/go/aicra/internal/request" "git.xdrm.io/go/aicra/internal/reqdata"
"git.xdrm.io/go/aicra/middleware" checker "git.xdrm.io/go/aicra/typecheck"
) )
// Server represents an AICRA instance featuring: // Server represents an AICRA instance featuring: type checkers, services
// * its type checkers
// * its middlewares
// * its controllers (api config)
type Server struct { type Server struct {
controller *apidef.Controller // controllers services *config.Service
checker checker.Registry // type checker registry checkers *checker.Set
middleware middleware.Registry // middlewares handlers []*api.Handler
schema *config.Schema
} }
// New creates a framework instance from a configuration file // New creates a framework instance from a configuration file
// _path is the json configuration path func New(configPath string) (*Server, error) {
// _driver is used to load/run the controllers and middlewares (default: )
//
func New(_path string) (*Server, error) {
/* 1. Load config */ var err error
schema, err := config.Parse("./aicra.json")
if err != nil {
return nil, err
}
/* 2. Init instance */ // 1. init instance
var i = &Server{ var i = &Server{
controller: nil, services: nil,
schema: schema, checkers: checker.New(),
handlers: make([]*api.Handler, 0),
} }
/* 3. Load configuration */ // 2. open config file
i.controller, err = apidef.Parse(_path) 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 { if err != nil {
return nil, err return nil, err
} }
/* 4. Load type registry */ /* 3. Load type registry */
i.checker = checker.CreateRegistry() // TODO: add methods on the checker to set types programmatically
// add default types if set /* 4. Load middleware registry */
if schema.Types.Default { // TODO: add methods to set them manually
// 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)
}
return i, nil return i, nil
@ -111,179 +60,91 @@ func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
defer req.Body.Close() defer req.Body.Close()
/* (1) Build request */ // 1. build API request from HTTP request
apiRequest, err := apirequest.New(req) apiRequest, err := api.NewRequest(req)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
/* (2) Launch middlewares to build the scope */ // 2. find a matching service for this path in the config
scope := s.middleware.Run(*req) serviceDef, pathIndex := s.services.Browse(apiRequest.URI)
if serviceDef == nil {
/* (3) Find a matching controller */
controller := s.matchController(apiRequest)
if controller == nil {
return return
} }
servicePath := strings.Join(apiRequest.URI[:pathIndex], "/")
/* (4) Check if matching method exists */ // 3. check if matching method exists in config */
var method = controller.Method(req.Method) var method = serviceDef.Method(req.Method)
if method == nil { if method == nil {
httpError(res, e.UnknownMethod) httpError(res, api.ErrorUnknownMethod())
return return
} }
/* (5) Check scope permissions */ // 4. parse every input data from the request
if !method.CheckScope(scope) { store := reqdata.New(req)
httpError(res, e.Permission)
return
}
/* (4) Check parameters /* (4) Check parameters
---------------------------------------------------------*/ ---------------------------------------------------------*/
parameters, paramError := s.extractParameters(apiRequest, method.Parameters) parameters, paramError := s.extractParameters(store, method.Parameters)
// Fail if argument check failed // Fail if argument check failed
if paramError.Code != e.Success.Code { if paramError.Code != api.ErrorSuccess().Code {
httpError(res, paramError) httpError(res, paramError)
return return
} }
/* (5) Load controller apiRequest.Param = parameters
---------------------------------------------------------*/
// get paths
ctlBuildPath := strings.Join(apiRequest.Path, "/")
ctlBuildPath = s.schema.Driver.Build(s.schema.Root, s.schema.Controllers.Folder, ctlBuildPath)
// get controller /* (5) Search a matching handler
ctlObject, err := s.schema.Driver.LoadController(ctlBuildPath) ---------------------------------------------------------*/
httpMethod := strings.ToUpper(req.Method) var serviceHandler *api.Handler
if err != nil { var serviceFound bool
httpErr := e.UncallableController
httpErr.Put(err) for _, handler := range s.handlers {
httpError(res, httpErr) if handler.GetPath() == servicePath {
log.Printf("err( %s )\n", err) 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 return
} }
var ctlMethod func(api.Arguments) api.Response /* (6) Execute handler and return 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 */ // 1. execute
parameters["_HTTP_METHOD_"] = httpMethod apiResponse := api.NewResponse()
serviceHandler.Handle(*apiRequest, apiResponse)
/* (2) Give Authorization header into controller */ // 2. apply headers
parameters["_AUTHORIZATION_"] = req.Header.Get("Authorization") for key, values := range apiResponse.Headers {
for _, value := range values {
/* (3) Give Scope into controller */ res.Header().Add(key, value)
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
} }
} }
/* (5) Build JSON response */ // 3. build JSON apiResponse
httpPrint(res, response) httpPrint(res, apiResponse)
return return
} }
// extractParameters extracts parameters for the request and checks // HandleFunc sets a new handler for an HTTP method to a path
// every single one according to configuration options func (s *Server) HandleFunc(httpMethod, path string, handlerFunc api.HandlerFunc) {
func (s *Server) extractParameters(req *apirequest.Request, methodParam map[string]*apidef.Parameter) (map[string]interface{}, e.Error) { handler := api.NewHandler(httpMethod, path, handlerFunc)
s.handlers = append(s.handlers, handler)
// init vars }
err := e.Success
parameters := make(map[string]interface{}) // Handle sets a new handler
func (s *Server) Handle(handler *api.Handler) {
// for each param of the config s.handlers = append(s.handlers, handler)
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
} }

120
util.go
View File

@ -4,59 +4,111 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"strings"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/err" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/apidef" "git.xdrm.io/go/aicra/internal/reqdata"
apireq "git.xdrm.io/go/aicra/internal/request"
) )
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 */ // 1. try to find definition
pathi, ctl := s.controller.Browse(req.URI) serviceDef, pathi := s.services.Browse(req.URI)
/* (2) Set controller uri */ // 2. set service uri
req.Path = make([]string, 0, pathi) servicePath = strings.Join(req.URI[: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
return
} }
// Redirects to another location (http protocol) // extractParameters extracts parameters for the request and checks
func httpRedirect(r http.ResponseWriter, loc string) { // every single one according to configuration options
r.Header().Add("Location", loc) func (s *Server) extractParameters(store *reqdata.Store, methodParam map[string]*config.Parameter) (map[string]interface{}, api.Error) {
r.WriteHeader(308) // permanent redirect
// 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 // Prints an HTTP response
func httpPrint(r http.ResponseWriter, res api.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
}
// write this json // write this json
jsonResponse, _ := json.Marshal(formattedResponse) jsonResponse, _ := json.Marshal(res)
r.Header().Add("Content-Type", "application/json") r.Header().Add("Content-Type", "application/json")
r.Write(jsonResponse) r.Write(jsonResponse)
} }
// Prints an error as HTTP response // Prints an error as HTTP response
func httpError(r http.ResponseWriter, e err.Error) { func httpError(r http.ResponseWriter, e api.Error) {
JSON, _ := e.MarshalJSON() JSON, _ := json.Marshal(e)
r.Header().Add("Content-Type", "application/json") r.Header().Add("Content-Type", "application/json")
r.Write(JSON) r.Write(JSON)
log.Printf("[http.fail] %s\n", e.Reason) log.Printf("[http.fail] %s\n", e.Reason)