Compare commits

..

5 Commits

13 changed files with 222 additions and 486 deletions

View File

@ -42,18 +42,10 @@ var (
// the http request URI. // the http request URI.
ErrorUnknownService = func() Error { return Error{200, "unknown service", nil} } 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 service's // ErrorUncallableService is set when there the requested service's
// implementation (plugin file) is not found/callable // implementation (plugin file) is not found/callable
ErrorUncallableService = func() Error { return Error{202, "uncallable service", nil} } ErrorUncallableService = func() Error { return Error{202, "uncallable service", nil} }
// ErrorUncallableMethod is set when there the requested service'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 // ErrorPermission is set when there is a permission error by default
// the api returns a permission error when the current scope (built // the api returns a permission error when the current scope (built
// by middlewares) does not match the scope required in the config. // by middlewares) does not match the scope required in the config.

View File

@ -4,19 +4,19 @@ import (
"fmt" "fmt"
) )
// Error allows you to create constant "const" error with type boxing. // cerr allows you to create constant "const" error with type boxing.
type Error string type cerr string
// Error implements the error builtin interface. // Error implements the error builtin interface.
func (err Error) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrReqParamNotFound is thrown when a request parameter is not found // ErrReqParamNotFound is thrown when a request parameter is not found
const ErrReqParamNotFound = Error("request parameter not found") const ErrReqParamNotFound = cerr("request parameter not found")
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type // ErrReqParamNotType is thrown when a request parameter is not asked with the right type
const ErrReqParamNotType = Error("request parameter does not fulfills type") const ErrReqParamNotType = cerr("request parameter does not fulfills type")
// RequestParam defines input parameters of an api request // RequestParam defines input parameters of an api request
type RequestParam map[string]interface{} type RequestParam map[string]interface{}

0
go.sum Normal file
View File

126
http.go
View File

@ -3,7 +3,6 @@ package aicra
import ( import (
"log" "log"
"net/http" "net/http"
"strings"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/reqdata" "git.xdrm.io/go/aicra/internal/reqdata"
@ -13,103 +12,96 @@ import (
type httpServer Server type httpServer Server
// ServeHTTP implements http.Handler and has to be called on each request // ServeHTTP implements http.Handler and has to be called on each request
func (server httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
defer r.Body.Close() defer req.Body.Close()
/* (1) create api.Request from http.Request // 1. find a matching service in the config
---------------------------------------------------------*/ service := server.config.Find(req)
request, err := api.NewRequest(r) if service == nil {
response := api.NewResponse(api.ErrorUnknownService())
response.ServeHTTP(res, req)
logError(response)
return
}
// 2. build input parameter receiver
dataset := reqdata.New(service)
// 3. extract URI data
err := dataset.ExtractURI(req)
if err != nil { if err != nil {
log.Fatal(err) response := api.NewResponse(api.ErrorMissingParam())
} response.ServeHTTP(res, req)
logError(response)
// 2. find a matching service for this path in the config
serviceConf, pathIndex := server.config.Browse(request.URI)
if serviceConf == nil {
return return
} }
// 3. extract the service path from request URI // 4. extract query data
servicePath := strings.Join(request.URI[:pathIndex], "/") err = dataset.ExtractQuery(req)
if !strings.HasPrefix(servicePath, "/") { if err != nil {
servicePath = "/" + servicePath response := api.NewResponse(api.ErrorMissingParam())
} response.ServeHTTP(res, req)
logError(response)
// 4. find method configuration from http method */
var methodConf = serviceConf.Method(r.Method)
if methodConf == nil {
res := api.NewResponse(api.ErrorUnknownMethod())
res.ServeHTTP(w, r)
logError(res)
return return
} }
// 5. parse data from the request (uri, query, form, json) // 5. extract form/json data
data := reqdata.New(request.URI[pathIndex:], r) err = dataset.ExtractForm(req)
if err != nil {
/* (2) check parameters response := api.NewResponse(api.ErrorMissingParam())
---------------------------------------------------------*/ response.ServeHTTP(res, req)
parameters, paramError := server.extractParameters(data, methodConf.Parameters) logError(response)
// Fail if argument check failed
if paramError.Code != api.ErrorSuccess().Code {
res := api.NewResponse(paramError)
res.ServeHTTP(w, r)
logError(res)
return return
} }
request.Param = parameters // 6. find a matching handler
/* (3) search for the handler
---------------------------------------------------------*/
var foundHandler *api.Handler var foundHandler *api.Handler
var found bool var found bool
for _, handler := range server.handlers { for _, handler := range server.handlers {
if handler.GetPath() == servicePath { if handler.GetMethod() == service.Method && handler.GetPath() == service.Pattern {
found = true found = true
if handler.GetMethod() == r.Method {
foundHandler = handler
}
} }
} }
// fail if found no handler // 7. fail if found no handler
if foundHandler == nil { if foundHandler == nil {
if found { if found {
res := api.NewResponse() r := api.NewResponse()
res.SetError(api.ErrorUncallableMethod(), servicePath, r.Method) r.SetError(api.ErrorUncallableService(), service.Method, service.Pattern)
res.ServeHTTP(w, r) r.ServeHTTP(res, req)
logError(res) logError(r)
return return
} }
res := api.NewResponse() r := api.NewResponse()
res.SetError(api.ErrorUncallableService(), servicePath) r.SetError(api.ErrorUnknownService(), service.Method, service.Pattern)
res.ServeHTTP(w, r) r.ServeHTTP(res, req)
logError(res) logError(r)
return return
} }
/* (4) execute handler and return response // 8. build api.Request from http.Request
---------------------------------------------------------*/ apireq, err := api.NewRequest(req)
// 1. feed request with configuration scope if err != nil {
request.Scope = methodConf.Scope log.Fatal(err)
}
// 2. execute // 9. feed request with scope & parameters
res := api.NewResponse() apireq.Scope = service.Scope
foundHandler.Handle(*request, res) apireq.Param = dataset.Data
// 3. apply headers // 10. execute
for key, values := range res.Headers { response := api.NewResponse()
foundHandler.Handle(*apireq, response)
// 11. apply headers
for key, values := range response.Headers {
for _, value := range values { for _, value := range values {
w.Header().Add(key, value) res.Header().Add(key, value)
} }
} }
// 4. write to response // 12. write to response
res.ServeHTTP(w, r) response.ServeHTTP(res, req)
return
} }

View File

@ -1,54 +1,54 @@
package config package config
// Error allows you to create constant "const" error with type boxing. // cerr allows you to create constant "const" error with type boxing.
type Error string type cerr string
// Error implements the error builtin interface. // Error implements the error builtin interface.
func (err Error) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrRead - a problem ocurred when trying to read the configuration file // ErrRead - a problem ocurred when trying to read the configuration file
const ErrRead = Error("cannot read config") const ErrRead = cerr("cannot read config")
// ErrUnknownMethod - invalid http method // ErrUnknownMethod - invalid http method
const ErrUnknownMethod = Error("unknown HTTP method") const ErrUnknownMethod = cerr("unknown HTTP method")
// ErrFormat - a invalid format has been detected // ErrFormat - a invalid format has been detected
const ErrFormat = Error("invalid config format") const ErrFormat = cerr("invalid config format")
// ErrPatternCollision - there is a collision between 2 services' patterns (same method) // ErrPatternCollision - there is a collision between 2 services' patterns (same method)
const ErrPatternCollision = Error("invalid config format") const ErrPatternCollision = cerr("invalid config format")
// ErrInvalidPattern - a service pattern is malformed // ErrInvalidPattern - a service pattern is malformed
const ErrInvalidPattern = Error("must begin with a '/' and not end with") const ErrInvalidPattern = cerr("must begin with a '/' and not end with")
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid // ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
const ErrInvalidPatternBraceCapture = Error("invalid uri capturing braces") const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces")
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern // ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
const ErrUnspecifiedBraceCapture = Error("capturing brace missing in the path") const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path")
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters // ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
const ErrUndefinedBraceCapture = Error("capturing brace missing input definition") const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition")
// ErrMissingDescription - a service is missing its description // ErrMissingDescription - a service is missing its description
const ErrMissingDescription = Error("missing description") const ErrMissingDescription = cerr("missing description")
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional // ErrIllegalOptionalURIParam - an URI parameter cannot be optional
const ErrIllegalOptionalURIParam = Error("URI parameter cannot be optional") const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
// ErrMissingParamDesc - a parameter is missing its description // ErrMissingParamDesc - a parameter is missing its description
const ErrMissingParamDesc = Error("missing parameter description") const ErrMissingParamDesc = cerr("missing parameter description")
// ErrUnknownDataType - a parameter has an unknown datatype name // ErrUnknownDataType - a parameter has an unknown datatype name
const ErrUnknownDataType = Error("unknown data type") const ErrUnknownDataType = cerr("unknown data type")
// ErrIllegalParamName - a parameter has an illegal name // ErrIllegalParamName - a parameter has an illegal name
const ErrIllegalParamName = Error("illegal parameter name") const ErrIllegalParamName = cerr("illegal parameter name")
// ErrMissingParamType - a parameter has an illegal type // ErrMissingParamType - a parameter has an illegal type
const ErrMissingParamType = Error("missing parameter type") const ErrMissingParamType = cerr("missing parameter type")
// ErrParamNameConflict - a parameter has a conflict with its name/rename field // ErrParamNameConflict - a parameter has a conflict with its name/rename field
const ErrParamNameConflict = Error("name conflict for parameter") const ErrParamNameConflict = cerr("name conflict for parameter")

View File

@ -1,21 +1,21 @@
package multipart package multipart
// Error allows you to create constant "const" error with type boxing. // cerr allows you to create constant "const" error with type boxing.
type Error string type cerr string
// Error implements the error builtin interface. // Error implements the error builtin interface.
func (err Error) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrMissingDataName is set when a multipart variable/file has no name="..." // ErrMissingDataName is set when a multipart variable/file has no name="..."
const ErrMissingDataName = Error("data has no name") const ErrMissingDataName = cerr("data has no name")
// ErrDataNameConflict is set when a multipart variable/file name is already used // ErrDataNameConflict is set when a multipart variable/file name is already used
const ErrDataNameConflict = Error("data name conflict") const ErrDataNameConflict = cerr("data name conflict")
// ErrNoHeader is set when a multipart variable/file has no (valid) header // ErrNoHeader is set when a multipart variable/file has no (valid) header
const ErrNoHeader = Error("data has no header") const ErrNoHeader = cerr("data has no header")
// Component represents a multipart variable/file // Component represents a multipart variable/file
type Component struct { type Component struct {

View File

@ -1,30 +1,30 @@
package reqdata package reqdata
// Error allows you to create constant "const" error with type boxing. // cerr allows you to create constant "const" error with type boxing.
type Error string type cerr string
// Error implements the error builtin interface. // Error implements the error builtin interface.
func (err Error) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrUnknownType is returned when encountering an unknown type // ErrUnknownType is returned when encountering an unknown type
const ErrUnknownType = Error("unknown type") const ErrUnknownType = cerr("unknown type")
// ErrInvalidMultipart is returned when multipart parse failed
const ErrInvalidMultipart = cerr("invalid multipart")
// ErrParseParameter is returned when a parameter fails when parsing
const ErrParseParameter = cerr("cannot parse parameter")
// ErrInvalidJSON is returned when json parse failed // ErrInvalidJSON is returned when json parse failed
const ErrInvalidJSON = Error("invalid json") const ErrInvalidJSON = cerr("invalid json")
// ErrInvalidRootType is returned when json is a map
const ErrInvalidRootType = Error("invalid json root type")
// ErrInvalidParamName - parameter has an invalid
const ErrInvalidParamName = Error("invalid parameter name")
// ErrMissingRequiredParam - required param is missing // ErrMissingRequiredParam - required param is missing
const ErrMissingRequiredParam = Error("missing required param") const ErrMissingRequiredParam = cerr("missing required param")
// ErrInvalidType - parameter value does not satisfy its type // ErrInvalidType - parameter value does not satisfy its type
const ErrInvalidType = Error("invalid type") const ErrInvalidType = cerr("invalid type")
// ErrMissingURIParameter - missing an URI parameter // ErrMissingURIParameter - missing an URI parameter
const ErrMissingURIParameter = Error("missing URI parameter") const ErrMissingURIParameter = cerr("missing URI parameter")

View File

@ -1,117 +0,0 @@
package reqdata
import (
"encoding/json"
"fmt"
"reflect"
)
// Parameter represents an http request parameter
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
type Parameter struct {
// whether the value has been json-parsed
// for optimisation purpose, parameters are only parsed
// if they are required by the current service
Parsed bool
// whether the value is a file
File bool
// the actual parameter value
Value interface{}
}
// Parse parameter (json-like) if not already done
func (i *Parameter) Parse() error {
/* (1) Stop if already parsed or nil*/
if i.Parsed || i.Value == nil {
return nil
}
/* (2) Try to parse value */
parsed, err := parseParameter(i.Value)
if err != nil {
return err
}
i.Parsed = true
i.Value = parsed
return nil
}
// parseParameter parses http GET/POST data
// - []string
// - size = 1 : return json of first element
// - size > 1 : return array of json elements
// - string : return json if valid, else return raw string
func parseParameter(data interface{}) (interface{}, error) {
dtype := reflect.TypeOf(data)
dvalue := reflect.ValueOf(data)
switch dtype.Kind() {
/* (1) []string -> recursive */
case reflect.Slice:
// 1. ignore empty
if dvalue.Len() == 0 {
return data, nil
}
// 2. parse each element recursively
result := make([]interface{}, dvalue.Len())
for i, l := 0, dvalue.Len(); i < l; i++ {
element := dvalue.Index(i)
// ignore non-string
if element.Kind() != reflect.String {
result[i] = element.Interface()
continue
}
parsed, err := parseParameter(element.String())
if err != nil {
return data, err
}
result[i] = parsed
}
return result, nil
/* (2) string -> parse */
case reflect.String:
// build json wrapper
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
// try to parse as json
var result interface{}
err := json.Unmarshal([]byte(wrapper), &result)
// return if success
if err == nil {
mapval, ok := result.(map[string]interface{})
if !ok {
return dvalue.String(), ErrInvalidRootType
}
wrapped, ok := mapval["wrapped"]
if !ok {
return dvalue.String(), ErrInvalidJSON
}
return wrapped, nil
}
// else return as string
return dvalue.String(), nil
}
/* (3) NIL if unknown type */
return dvalue.Interface(), nil
}

View File

@ -6,21 +6,9 @@ import (
) )
func TestSimpleString(t *testing.T) { func TestSimpleString(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: "some-string"} p := parseParameter("some-string")
err := p.Parse() cast, canCast := p.(string)
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(string)
if !canCast { if !canCast {
t.Errorf("expected parameter to be a string") t.Errorf("expected parameter to be a string")
t.FailNow() t.FailNow()
@ -37,19 +25,9 @@ func TestSimpleFloat(t *testing.T) {
for i, tcase := range tcases { for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) { t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase} p := parseParameter(tcase)
if err := p.Parse(); err != nil { cast, canCast := p.(float64)
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(float64)
if !canCast { if !canCast {
t.Errorf("expected parameter to be a float64") t.Errorf("expected parameter to be a float64")
t.FailNow() t.FailNow()
@ -68,19 +46,9 @@ func TestSimpleBool(t *testing.T) {
for i, tcase := range tcases { for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) { t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase} p := parseParameter(tcase)
if err := p.Parse(); err != nil { cast, canCast := p.(bool)
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(bool)
if !canCast { if !canCast {
t.Errorf("expected parameter to be a bool") t.Errorf("expected parameter to be a bool")
t.FailNow() t.FailNow()
@ -95,21 +63,9 @@ func TestSimpleBool(t *testing.T) {
} }
func TestJsonStringSlice(t *testing.T) { func TestJsonStringSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`} p := parseParameter(`["str1", "str2"]`)
err := p.Parse() slice, canCast := p.([]interface{})
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast { if !canCast {
t.Errorf("expected parameter to be a []interface{}") t.Errorf("expected parameter to be a []interface{}")
t.FailNow() t.FailNow()
@ -139,21 +95,9 @@ func TestJsonStringSlice(t *testing.T) {
} }
func TestStringSlice(t *testing.T) { func TestStringSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []string{"str1", "str2"}} p := parseParameter([]string{"str1", "str2"})
err := p.Parse() slice, canCast := p.([]interface{})
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast { if !canCast {
t.Errorf("expected parameter to be a []interface{}") t.Errorf("expected parameter to be a []interface{}")
t.FailNow() t.FailNow()
@ -193,20 +137,9 @@ func TestJsonPrimitiveBool(t *testing.T) {
for i, tcase := range tcases { for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) { t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase.Raw} p := parseParameter(tcase.Raw)
err := p.Parse() cast, canCast := p.(bool)
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(bool)
if !canCast { if !canCast {
t.Errorf("expected parameter to be a bool") t.Errorf("expected parameter to be a bool")
t.FailNow() t.FailNow()
@ -241,20 +174,9 @@ func TestJsonPrimitiveFloat(t *testing.T) {
for i, tcase := range tcases { for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) { t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase.Raw} p := parseParameter(tcase.Raw)
err := p.Parse() cast, canCast := p.(float64)
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(float64)
if !canCast { if !canCast {
t.Errorf("expected parameter to be a float64") t.Errorf("expected parameter to be a float64")
t.FailNow() t.FailNow()
@ -270,21 +192,9 @@ func TestJsonPrimitiveFloat(t *testing.T) {
} }
func TestJsonBoolSlice(t *testing.T) { func TestJsonBoolSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []string{"true", "false"}} p := parseParameter([]string{"true", "false"})
err := p.Parse() slice, canCast := p.([]interface{})
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast { if !canCast {
t.Errorf("expected parameter to be a []interface{}") t.Errorf("expected parameter to be a []interface{}")
t.FailNow() t.FailNow()
@ -314,21 +224,9 @@ func TestJsonBoolSlice(t *testing.T) {
} }
func TestBoolSlice(t *testing.T) { func TestBoolSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []bool{true, false}} p := parseParameter([]bool{true, false})
err := p.Parse() slice, canCast := p.([]interface{})
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast { if !canCast {
t.Errorf("expected parameter to be a []interface{}") t.Errorf("expected parameter to be a []interface{}")
t.FailNow() t.FailNow()

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"reflect"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/multipart" "git.xdrm.io/go/aicra/internal/multipart"
@ -26,14 +27,14 @@ type Set struct {
// - FORM: no prefix // - FORM: no prefix
// - URL: '{uri_var}' // - URL: '{uri_var}'
// - GET: 'GET@' followed by the key in GET // - GET: 'GET@' followed by the key in GET
Data map[string]*Parameter Data map[string]interface{}
} }
// New creates a new empty store. // New creates a new empty store.
func New(service *config.Service) *Set { func New(service *config.Service) *Set {
return &Set{ return &Set{
service: service, service: service,
Data: make(map[string]*Parameter), Data: make(map[string]interface{}),
} }
} }
@ -53,17 +54,17 @@ func (i *Set) ExtractURI(req *http.Request) error {
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType) return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
} }
// parse parameter
parsed := parseParameter(value)
// check type // check type
cast, valid := capture.Ref.Validator(value) cast, valid := capture.Ref.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType) return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
} }
// store cast value in 'Set' // store cast value in 'Set'
i.Data[capture.Ref.Rename] = &Parameter{ i.Data[capture.Ref.Rename] = cast
Value: cast,
}
} }
return nil return nil
@ -86,16 +87,17 @@ func (i *Set) ExtractQuery(req *http.Request) error {
continue continue
} }
// parse parameter
parsed := parseParameter(value)
// check type // check type
cast, valid := param.Validator(value) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store value // store cast value
i.Data[param.Rename] = &Parameter{ i.Data[param.Rename] = cast
Value: cast,
}
} }
return nil return nil
@ -167,10 +169,8 @@ func (i *Set) parseJSON(req *http.Request) error {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store value // store cast value
i.Data[param.Rename] = &Parameter{ i.Data[param.Rename] = cast
Value: cast,
}
} }
return nil return nil
@ -197,16 +197,17 @@ func (i *Set) parseUrlencoded(req *http.Request) error {
continue continue
} }
// parse parameter
parsed := parseParameter(value)
// check type // check type
cast, valid := param.Validator(value) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store value // store cast value
i.Data[param.Rename] = &Parameter{ i.Data[param.Rename] = cast
Value: cast,
}
} }
return nil return nil
@ -244,18 +245,79 @@ func (i *Set) parseMultipart(req *http.Request) error {
continue continue
} }
// parse parameter
parsed := parseParameter(string(component.Data))
// fail on invalid type // fail on invalid type
cast, valid := param.Validator(string(component.Data)) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store value // store cast value
i.Data[param.Rename] = &Parameter{ i.Data[param.Rename] = cast
Value: cast,
}
} }
return nil return nil
} }
// parseParameter parses http URI/GET/POST data
// - []string : return array of json elements
// - string : return json if valid, else return raw string
func parseParameter(data interface{}) interface{} {
dtype := reflect.TypeOf(data)
dvalue := reflect.ValueOf(data)
switch dtype.Kind() {
/* (1) []string -> recursive */
case reflect.Slice:
// 1. ignore empty
if dvalue.Len() == 0 {
return data
}
// 2. parse each element recursively
result := make([]interface{}, dvalue.Len())
for i, l := 0, dvalue.Len(); i < l; i++ {
element := dvalue.Index(i)
result[i] = parseParameter(element.Interface())
}
return result
/* (2) string -> parse */
case reflect.String:
// build json wrapper
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
// try to parse as json
var result interface{}
err := json.Unmarshal([]byte(wrapper), &result)
// return if success
if err != nil {
return dvalue.String()
}
mapval, ok := result.(map[string]interface{})
if !ok {
return dvalue.String()
}
wrapped, ok := mapval["wrapped"]
if !ok {
return dvalue.String()
}
return wrapped
}
/* (3) NIL if unknown type */
return dvalue.Interface()
}

View File

@ -280,7 +280,7 @@ func TestExtractQuery(t *testing.T) {
t.FailNow() t.FailNow()
} }
cast, canCast := param.Value.([]string) cast, canCast := param.([]interface{})
if !canCast { if !canCast {
t.Errorf("should return a []string (got '%v')", cast) t.Errorf("should return a []string (got '%v')", cast)
t.FailNow() t.FailNow()
@ -458,9 +458,9 @@ func TestExtractFormUrlEncoded(t *testing.T) {
t.FailNow() t.FailNow()
} }
cast, canCast := param.Value.([]string) cast, canCast := param.([]interface{})
if !canCast { if !canCast {
t.Errorf("should return a []string (got '%v')", cast) t.Errorf("should return a []interface{} (got '%v')", cast)
t.FailNow() t.FailNow()
} }
@ -606,8 +606,8 @@ func TestJsonParameters(t *testing.T) {
valueType := reflect.TypeOf(value) valueType := reflect.TypeOf(value)
paramValue := param.Value paramValue := param
paramValueType := reflect.TypeOf(param.Value) paramValueType := reflect.TypeOf(param)
if valueType != paramValueType { if valueType != paramValueType {
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType) t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
@ -762,8 +762,8 @@ x
valueType := reflect.TypeOf(value) valueType := reflect.TypeOf(value)
paramValue := param.Value paramValue := param
paramValueType := reflect.TypeOf(param.Value) paramValueType := reflect.TypeOf(param)
if valueType != paramValueType { if valueType != paramValueType {
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType) t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)

View File

@ -6,20 +6,18 @@ import (
"os" "os"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
checker "git.xdrm.io/go/aicra/typecheck"
) )
// Server represents an AICRA instance featuring: type checkers, services // Server represents an AICRA instance featuring: type checkers, services
type Server struct { type Server struct {
config *config.Service config *config.Server
Checkers *checker.Set
handlers []*api.Handler handlers []*api.Handler
} }
// New creates a framework instance from a configuration file // New creates a framework instance from a configuration file
func New(configPath string) (*Server, error) { func New(configPath string, dtypes ...datatype.T) (*Server, error) {
var ( var (
err error err error
configFile io.ReadCloser configFile io.ReadCloser
@ -28,7 +26,6 @@ func New(configPath string) (*Server, error) {
// 1. init instance // 1. init instance
var i = &Server{ var i = &Server{
config: nil, config: nil,
Checkers: checker.New(),
handlers: make([]*api.Handler, 0), handlers: make([]*api.Handler, 0),
} }
@ -40,14 +37,16 @@ func New(configPath string) (*Server, error) {
defer configFile.Close() defer configFile.Close()
// 3. load configuration // 3. load configuration
i.config, err = config.Parse(configFile) i.config, err = config.Parse(configFile, dtypes...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 4. log configuration services // 4. log configuration services
log.Printf("🔧 Reading configuration '%s'\n", configPath) log.Printf("🔧 Reading configuration '%s'\n", configPath)
logService(*i.config, "") for _, service := range i.config.Services {
log.Printf(" ->\t%s\t'%s'\n", service.Method, service.Pattern)
}
return i, nil return i, nil

90
util.go
View File

@ -5,101 +5,11 @@ import (
"net/http" "net/http"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/reqdata"
) )
// extractParameters extracts parameters for the request and checks
// every single one according to configuration options
func (s *httpServer) extractParameters(store *reqdata.Store, methodParam map[string]*config.Parameter) (map[string]interface{}, api.Error) {
// init vars
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. fail if required & missing
if !isset && !param.Optional {
apiErr := api.ErrorMissingParam()
apiErr.SetArguments(name)
return nil, apiErr
}
// 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 {
apiErr := api.ErrorInvalidParam()
apiErr.SetArguments(param.Rename, "FILE")
return nil, apiErr
}
// 6. ignore type check if file
if gotFile {
parameters[param.Rename] = p.Value
continue
}
// 7. check type
if s.Checkers.Run(param.Type, p.Value) != nil {
apiErr := api.ErrorInvalidParam()
apiErr.SetArguments(param.Rename, param.Type, p.Value)
return nil, apiErr
}
parameters[param.Rename] = p.Value
}
return parameters, api.ErrorSuccess()
}
var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
// Prints an error as HTTP response // Prints an error as HTTP response
func logError(res *api.Response) { func logError(res *api.Response) {
log.Printf("[http.fail] %v\n", res) log.Printf("[http.fail] %v\n", res)
} }
// logService logs a service details
func logService(s config.Service, path string) {
for _, method := range handledMethods {
if m := s.Method(method); m != nil {
if path == "" {
log.Printf(" ->\t%s\t'/'\n", method)
} else {
log.Printf(" ->\t%s\t'%s'\n", method, path)
}
}
}
if s.Children != nil {
for subPath, child := range s.Children {
logService(*child, path+"/"+subPath)
}
}
}