Merge branch 'refactor-test' of go/aicra into 0.3.0
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Adrien Marquès 2020-04-04 15:33:43 +00:00 committed by Gogs
commit fb69dbb903
29 changed files with 701 additions and 879 deletions

173
README.md
View File

@ -7,40 +7,34 @@
[![Build Status](https://drone.xdrm.io/api/badges/go/aicra/status.svg)](https://drone.xdrm.io/go/aicra)
**Aicra** is a *configuration-driven* **web framework** written in Go that allows you to create a fully featured REST API.
Aicra is a *configuration-driven* REST API engine written in Go.
The whole management is done for you from a configuration file describing your API, you're left with implementing :
Most of the management is done for you using a configuration file describing your API. you're left with implementing :
- handlers
- optionnally middle-wares (_e.g. authentication, csrf_)
- and optionnally your custom type checkers to check input parameters
> A example project is available [here](https://git.xdrm.io/go/articles-api)
The aicra server fulfills the `net/http` [Server interface](https://golang.org/pkg/net/http/#Server).
> A example project is available [here](https://git.xdrm.io/go/tiny-url-ex)
### Table of contents
## Table of contents
<!-- toc -->
- [I/ Installation](#i-installation)
- [II/ Development](#ii-development)
* [1) Main executable](#1-main-executable)
* [2) API Configuration](#2-api-configuration)
- [Definition](#definition)
+ [Input Arguments](#input-arguments)
- [1. Input types](#1-input-types)
- [2. Global Format](#2-global-format)
- [II/ Usage](#ii-usage)
* [1) Build a server](#1-build-a-server)
* [2) API Configuration](#2-api-configuration)
- [Definition](#definition)
+ [Input Arguments](#input-arguments)
- [1. Input types](#1-input-types)
- [2. Global Format](#2-global-format)
- [III/ Change Log](#iii-change-log)
<!-- tocstop -->
### I/ Installation
## I/ Installation
You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.10**.
You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.14**.
```bash
@ -50,95 +44,112 @@ go get -u git.xdrm.io/go/aicra/cmd/aicra
The library should now be available as `git.xdrm.io/go/aicra` in your imports.
### II/ Development
## II/ Usage
#### 1) Main executable
### 1) Build a server
Your main executable will declare and run the aicra server, it might look quite like the code below.
Here is some sample code that builds and sets up an aicra server using your api configuration file.
```go
package main
import (
"log"
"net/http"
"log"
"net/http"
"os"
"git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/datatype"
"git.xdrm.io/go/aicra/datatype/builtin"
"git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype/builtin"
)
func main() {
// 1. select your datatypes (builtin, custom)
var dtypes []datatype.T
dtypes = append(dtypes, builtin.AnyDataType{})
dtypes = append(dtypes, builtin.BoolDataType{})
dtypes = append(dtypes, builtin.UintDataType{})
dtypes = append(dtypes, builtin.StringDataType{})
builder := &aicra.Builder{}
// 2. create the server from the configuration file
server, err := aicra.New("path/to/your/api/definition.json", dtypes...)
// add datatypes your api uses
builder.AddType(builtin.BoolDataType{})
builder.AddType(builtin.UintDataType{})
builder.AddType(builtin.StringDataType{})
config, err := os.Open("./api.json")
if err != nil {
log.Fatalf("cannot built aicra server: %s\n", err)
log.Fatalf("cannot open config: %s", err)
}
// 3. bind your implementations
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){
// ... process stuff ...
res.SetError(api.ErrorSuccess());
})
// 4. extract to http server
httpServer, err := server.ToHTTPServer()
// pass your configuration
err = builder.Setup(config)
config.Close()
if err != nil {
log.Fatalf("cannot get to http server: %s", err)
log.Fatalf("invalid config: %s", err)
}
// 4. launch server
log.Fatal( http.ListenAndServe("localhost:8080", server) )
// bind your handlers
builder.Bind(http.MethodGet, "/user/{id}", getUserById)
builder.Bind(http.MethodGet, "/user/{id}/username", getUsernameByID)
// build the server and start listening
server, err := builder.Build()
if err != nil {
log.Fatalf("cannot build server: %s", err)
}
http.ListenAndServe("localhost:8080", server)
}
```
Here is an example handler
```go
type req struct{
Param1 int
Param3 *string // optional are pointers
}
type res struct{
Output1 string
Output2 bool
}
#### 2) API Configuration
func myHandler(r req) (*res, api.Error) {
err := doSomething()
if err != nil {
return nil, api.ErrorFailure
}
return &res{}, api.ErrorSuccess
}
```
The whole project behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [template](https://git.xdrm.io/go/tiny-url-ex/src/master/api.json). This file defines :
### 2) API Configuration
The whole api behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [template](https://git.xdrm.io/go/articles-api/src/master/api.json). This file defines :
- routes and their methods
- every input for each method (called *argument*)
- every output for each method
- scope permissions (list of permissions needed by clients)
- input policy :
- type of argument (_i.e. for data types_)
- type of argument (_c.f. data types_)
- required/optional
- variable renaming
#### Format
###### Definition
The root of the json file must be an array containing your requests definitions.
For each, you will have to create fields described in the table above.
The root of the json file must be an array containing your requests definitions. For each, you will have to create fields described in the table above.
| field path | description | example |
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `info` | A short human-readable description of what the method does | `create a new user` |
| `scope` | A 2-dimensional array of permissions. The first dimension can be translated to a **or** operator, the second dimension as a **and**. It allows you to combine permissions in complex ways. | `[["A", "B"], ["C", "D"]]` can be translated to : this method needs users to have permissions (A **and** B) **or** (C **and** D) |
| `in` | The list of arguments that the clients will have to provide. See [here](#input-arguments) for details. | |
| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but is only use for readability purpose and documentation. | |
| `in` | The list of arguments that the clients will have to provide. [Read more](#input-arguments). | |
| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but optional parameters are not allowed |
##### Input Arguments
###### 1. Input types
### Input Arguments
Input arguments defines what data from the HTTP request the method needs. Aicra is able to extract 3 types of data :
- **URI** - Curly Braces enclosed strings inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part.
- **URI** - data from inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part.
- **Query** - data formatted at the end of the URL following the standard [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax.
- **URL encoded** - data send inside the body of the request but following the [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax.
- **Multipart** - data send inside the body of the request with a dedicated [format](https://tools.ietf.org/html/rfc2388#section-3). This format is not very lightweight but allows you to receive data as well as files.
@ -146,7 +157,7 @@ Input arguments defines what data from the HTTP request the method needs. Aicra
###### 2. Global Format
#### Format
The `in` field in each method contains as list of arguments where the key is the argument name, and the value defines how to manage the variable.
@ -159,10 +170,6 @@ The `in` field in each method contains as list of arguments where the key is the
In this example we want 3 arguments :
- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler.
- the 2^nd^ one is send in the query (_e.g. [http://host/uri?get-var=value](http://host/uri?get-var=value)_). It must be a valid `string` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `title`.
- the 3^rd^ can be send with a **JSON** body, in **multipart** or **URL encoded** it makes no difference and only give clients a choice over the technology to use. If not renamed, the variable will be given to the handler with the name `content`.
```json
[
{
@ -184,32 +191,6 @@ In this example we want 3 arguments :
]
```
### III/ Change Log
- [x] human-readable json configuration
- [x] nested routes (*i.e. `/user/:id:` and `/user/post/:id:`*)
- [x] nested URL arguments (*i.e. `/user/:id:` and `/user/:id:/post/:id:`*)
- [x] useful http methods: GET, POST, PUT, DELETE
- [x] manage URL, query and body arguments:
- [x] multipart/form-data (variables and file uploads)
- [x] application/x-www-form-urlencoded
- [x] application/json
- [x] required vs. optional parameters with a default value
- [x] parameter renaming
- [x] generic type check (*i.e. implement custom types alongside built-in ones*)
- [ ] built-in types
- [x] `any` - wildcard matching all values
- [x] `int` - see go types
- [x] `uint` - see go types
- [x] `float` - see go types
- [x] `string` - any text
- [x] `string(min, max)` - any string with a length between `min` and `max`
- [ ] `[a]` - array containing **only** elements matching `a` type
- [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
- [x] generic controllers implementation (shared objects)
- [x] response interface
- [x] log bound resources when building the aicra server
- [x] fail on check for unimplemented resources at server boot.
- [x] fail on check for unavailable types in api.json at server boot.
- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler.
- the 2^nd^ one is send in the query (_e.g. [http://host/uri?get-var=value](http://host/uri?get-var=value)_). It must be a valid `string` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `title`.
- the 3^rd^ can be send with a **JSON** body, in **multipart** or **URL encoded** it makes no difference and only give clients a choice over the technology to use. If not renamed, the variable will be given to the handler with the name `content`.

View File

@ -1,5 +1,7 @@
package api
import "net/http"
var (
// ErrorUnknown represents any error which cause is unknown.
// It might also be used for debug purposes as this error
@ -19,20 +21,17 @@ var (
// unique fields already exists
ErrorAlreadyExists Error = 3
// ErrorConfig has to be set when there is a configuration error
ErrorConfig Error = 4
// ErrorCreation has to be set when there is a creation/insert error
ErrorCreation Error = 5
ErrorCreation Error = 4
// ErrorModification has to be set when there is an update/modification error
ErrorModification Error = 6
ErrorModification Error = 5
// ErrorDeletion has to be set when there is a deletion/removal error
ErrorDeletion Error = 7
ErrorDeletion Error = 6
// ErrorTransaction has to be set when there is a transactional error
ErrorTransaction Error = 8
ErrorTransaction Error = 7
// ErrorUpload has to be set when a file upload failed
ErrorUpload Error = 100
@ -90,7 +89,6 @@ var errorReasons = map[Error]string{
ErrorFailure: "it failed",
ErrorNoMatchFound: "resource not found",
ErrorAlreadyExists: "already exists",
ErrorConfig: "configuration error",
ErrorCreation: "create error",
ErrorModification: "update error",
ErrorDeletion: "delete error",
@ -108,3 +106,26 @@ var errorReasons = map[Error]string{
ErrorInvalidParam: "invalid parameter",
ErrorInvalidDefaultParam: "invalid default param",
}
var errorStatus = map[Error]int{
ErrorUnknown: http.StatusOK,
ErrorSuccess: http.StatusOK,
ErrorFailure: http.StatusInternalServerError,
ErrorNoMatchFound: http.StatusOK,
ErrorAlreadyExists: http.StatusOK,
ErrorCreation: http.StatusOK,
ErrorModification: http.StatusOK,
ErrorDeletion: http.StatusOK,
ErrorTransaction: http.StatusOK,
ErrorUpload: http.StatusInternalServerError,
ErrorDownload: http.StatusInternalServerError,
MissingDownloadHeaders: http.StatusBadRequest,
ErrorMissingDownloadBody: http.StatusBadRequest,
ErrorUnknownService: http.StatusServiceUnavailable,
ErrorUncallableService: http.StatusServiceUnavailable,
ErrorNotImplemented: http.StatusNotImplemented,
ErrorPermission: http.StatusUnauthorized,
ErrorToken: http.StatusForbidden,
ErrorMissingParam: http.StatusBadRequest,
ErrorInvalidParam: http.StatusBadRequest,
ErrorInvalidDefaultParam: http.StatusBadRequest,
}

View File

@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
)
// Error represents an http response error following the api format.
@ -10,17 +11,23 @@ import (
// directly into the response as JSON alongside response output fields.
type Error int
// Error implements the error interface
func (e Error) Error() string {
// use unknown error if no reason
reason, ok := errorReasons[e]
if !ok {
return ErrorUnknown.Error()
}
return fmt.Sprintf("[%d] %s", e, reason)
}
// Status returns the associated HTTP status code
func (e Error) Status() int {
status, ok := errorStatus[e]
if !ok {
return http.StatusOK
}
return status
}
// MarshalJSON implements encoding/json.Marshaler interface
func (e Error) MarshalJSON() ([]byte, error) {
// use unknown error if no reason

View File

@ -1,54 +0,0 @@
package api
import (
"net/http"
"strings"
)
// Request represents an API request i.e. HTTP
type Request struct {
// corresponds to the list of uri components
// featured in the request URI
URI []string
// Scope from the configuration file of the current service
Scope [][]string
// original HTTP request
Request *http.Request
// input parameters
Param RequestParam
}
// NewRequest builds an interface request from a http.Request
func NewRequest(req *http.Request) *Request {
uri := normaliseURI(req.URL.Path)
uriparts := strings.Split(uri, "/")
return &Request{
URI: uriparts,
Scope: nil,
Request: req,
Param: make(RequestParam),
}
}
// normaliseURI removes the trailing '/' to always
// have the same Uri format for later processing
func normaliseURI(uri string) string {
if len(uri) < 1 {
return uri
}
if uri[0] == '/' {
uri = uri[1:]
}
if len(uri) > 1 && uri[len(uri)-1] == '/' {
uri = uri[0 : len(uri)-1]
}
return uri
}

View File

@ -1,162 +0,0 @@
package api
import (
"fmt"
)
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrReqParamNotFound is thrown when a request parameter is not found
const ErrReqParamNotFound = cerr("request parameter not found")
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
const ErrReqParamNotType = cerr("request parameter does not fulfills type")
// RequestParam defines input parameters of an api request
type RequestParam map[string]interface{}
// Get returns the raw value (not typed) and an error if not found
func (rp RequestParam) Get(key string) (interface{}, error) {
rawValue, found := rp[key]
if !found {
return "", ErrReqParamNotFound
}
return rawValue, nil
}
// GetString returns a string and an error if not found or invalid type
func (rp RequestParam) GetString(key string) (string, error) {
rawValue, err := rp.Get(key)
if err != nil {
return "", err
}
switch cast := rawValue.(type) {
case fmt.Stringer:
return cast.String(), nil
case []byte:
return string(cast), nil
case string:
return cast, nil
default:
return "", ErrReqParamNotType
}
}
// GetFloat returns a float64 and an error if not found or invalid type
func (rp RequestParam) GetFloat(key string) (float64, error) {
rawValue, err := rp.Get(key)
if err != nil {
return 0, err
}
switch cast := rawValue.(type) {
case float32:
return float64(cast), nil
case float64:
return cast, nil
case int, int8, int16, int32, int64:
intVal, ok := cast.(int)
if !ok || intVal != int(float64(intVal)) {
return 0, ErrReqParamNotType
}
return float64(intVal), nil
case uint, uint8, uint16, uint32, uint64:
uintVal, ok := cast.(uint)
if !ok || uintVal != uint(float64(uintVal)) {
return 0, ErrReqParamNotType
}
return float64(uintVal), nil
default:
return 0, ErrReqParamNotType
}
}
// GetInt returns an int and an error if not found or invalid type
func (rp RequestParam) GetInt(key string) (int, error) {
rawValue, err := rp.Get(key)
if err != nil {
return 0, err
}
switch cast := rawValue.(type) {
case float32, float64:
floatVal, ok := cast.(float64)
if !ok || floatVal < 0 || floatVal != float64(int(floatVal)) {
return 0, ErrReqParamNotType
}
return int(floatVal), nil
case int, int8, int16, int32, int64:
intVal, ok := cast.(int)
if !ok || intVal != int(int(intVal)) {
return 0, ErrReqParamNotType
}
return int(intVal), nil
default:
return 0, ErrReqParamNotType
}
}
// GetUint returns an uint and an error if not found or invalid type
func (rp RequestParam) GetUint(key string) (uint, error) {
rawValue, err := rp.Get(key)
if err != nil {
return 0, err
}
switch cast := rawValue.(type) {
case float32, float64:
floatVal, ok := cast.(float64)
if !ok || floatVal < 0 || floatVal != float64(uint(floatVal)) {
return 0, ErrReqParamNotType
}
return uint(floatVal), nil
case int, int8, int16, int32, int64:
intVal, ok := cast.(int)
if !ok || intVal != int(uint(intVal)) {
return 0, ErrReqParamNotType
}
return uint(intVal), nil
case uint, uint8, uint16, uint32, uint64:
uintVal, ok := cast.(uint)
if !ok {
return 0, ErrReqParamNotType
}
return uintVal, nil
default:
return 0, ErrReqParamNotType
}
}
// GetStrings returns an []slice and an error if not found or invalid type
func (rp RequestParam) GetStrings(key string) ([]string, error) {
rawValue, err := rp.Get(key)
if err != nil {
return nil, err
}
switch cast := rawValue.(type) {
case []fmt.Stringer:
strings := make([]string, len(cast))
for i, stringer := range cast {
strings[i] = stringer.String()
}
return strings, nil
case [][]byte:
strings := make([]string, len(cast))
for i, bytes := range cast {
strings[i] = string(bytes)
}
return strings, nil
case []string:
return cast, nil
default:
return nil, ErrReqParamNotType
}
}

View File

@ -26,13 +26,12 @@ func EmptyResponse() *Response {
}
}
// WithError sets the error from a base error with error arguments.
// WithError sets the error
func (res *Response) WithError(err Error) *Response {
res.err = err
return res
}
// Error implements the error interface and dispatches to internal error.
func (res *Response) Error() string {
return res.err.Error()
}
@ -42,36 +41,23 @@ func (res *Response) SetData(name string, value interface{}) {
res.Data[name] = value
}
// GetData gets a response field
func (res *Response) GetData(name string) interface{} {
value, _ := res.Data[name]
return value
}
// MarshalJSON implements the 'json.Marshaler' interface and is used
// to generate the JSON representation of the response
func (res *Response) MarshalJSON() ([]byte, error) {
fmt := make(map[string]interface{})
for k, v := range res.Data {
fmt[k] = v
}
fmt["error"] = res.err
return json.Marshal(fmt)
}
// ServeHTTP implements http.Handler and writes the API response.
func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(res.Status)
w.WriteHeader(res.err.Status())
encoded, err := json.Marshal(res)
if err != nil {
return err
}
w.Write(encoded)
return nil
}

View File

@ -29,7 +29,7 @@ func (b *Builder) AddType(t datatype.T) {
b.conf = &config.Server{}
}
if b.conf.Services != nil {
panic(ErrLateType)
panic(errLateType)
}
b.conf.Types = append(b.conf.Types, t)
}
@ -41,7 +41,7 @@ func (b *Builder) Setup(r io.Reader) error {
b.conf = &config.Server{}
}
if b.conf.Services != nil {
panic(ErrAlreadySetup)
panic(errAlreadySetup)
}
return b.conf.Parse(r)
}
@ -49,7 +49,7 @@ func (b *Builder) Setup(r io.Reader) error {
// Bind a dynamic handler to a REST service
func (b *Builder) Bind(method, path string, fn interface{}) error {
if b.conf.Services == nil {
return ErrNotSetup
return errNotSetup
}
// find associated service
@ -62,7 +62,7 @@ func (b *Builder) Bind(method, path string, fn interface{}) error {
}
if service == nil {
return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService)
return fmt.Errorf("%s '%s': %w", method, path, errUnknownService)
}
dyn, err := dynfunc.Build(fn, *service)
@ -91,7 +91,7 @@ func (b Builder) Build() (http.Handler, error) {
}
}
if !hasAssociatedHandler {
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler)
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, errMissingHandler)
}
}

23
datatype/datatype.go Normal file
View File

@ -0,0 +1,23 @@
package datatype
import (
"reflect"
)
// Validator returns whether a given value fulfills the datatype
// and casts the value into a common go type.
//
// for example, if a validator checks for upper case strings,
// whether the value is a []byte, a string or a []rune, if the
// value matches the validator's checks, it will be cast it into
// a common go type, say, string.
type Validator func(value interface{}) (cast interface{}, valid bool)
// T represents a datatype. The Build function returns a Validator if
// it manages types with the name `typeDefinition` (from the configuration field "type"); else it or returns NIL if the type
// definition does not match this datatype; the registry is passed to allow recursive datatypes (e.g. slices, structs, etc)
// The datatype's validator (when input is valid) must return a cast's go type matching the `Type() reflect.Type`
type T interface {
Type() reflect.Type
Build(typeDefinition string, registry ...T) Validator
}

View File

@ -1,15 +0,0 @@
package datatype
import "reflect"
// Validator returns whether a given value fulfills a datatype
// and casts the value into a compatible type
type Validator func(value interface{}) (cast interface{}, valid bool)
// T builds a T from the type definition (from the configuration field "type") and returns NIL if the type
// definition does not match this T ; the registry is passed for recursive datatypes (e.g. slices, structs, etc)
// to be able to access other datatypes
type T interface {
Type() reflect.Type
Build(typeDefinition string, registry ...T) Validator
}

View File

@ -3,22 +3,21 @@ package aicra
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrLateType - cannot add datatype after setting up the definition
const ErrLateType = cerr("types cannot be added after Setup")
// errLateType - cannot add datatype after setting up the definition
const errLateType = cerr("types cannot be added after Setup")
// ErrNotSetup - not set up yet
const ErrNotSetup = cerr("not set up")
// errNotSetup - not set up yet
const errNotSetup = cerr("not set up")
// ErrAlreadySetup - already set up
const ErrAlreadySetup = cerr("already set up")
// errAlreadySetup - already set up
const errAlreadySetup = cerr("already set up")
// ErrUnknownService - no service matching this handler
const ErrUnknownService = cerr("unknown service")
// errUnknownService - no service matching this handler
const errUnknownService = cerr("unknown service")
// ErrMissingHandler - missing handler
const ErrMissingHandler = cerr("missing handler")
// errMissingHandler - missing handler
const errMissingHandler = cerr("missing handler")

182
internal/config/config.go Normal file
View File

@ -0,0 +1,182 @@
package config
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"git.xdrm.io/go/aicra/datatype"
)
// Server definition
type Server struct {
Types []datatype.T
Services []*Service
}
// Parse a configuration into a server. Server.Types must be set beforehand to
// make datatypes available when checking and formatting the read configuration.
func (srv *Server) Parse(r io.Reader) error {
err := json.NewDecoder(r).Decode(&srv.Services)
if err != nil {
return fmt.Errorf("%s: %w", errRead, err)
}
err = srv.validate()
if err != nil {
return fmt.Errorf("%s: %w", errFormat, err)
}
return nil
}
// validate implements the validator interface
func (server Server) validate(datatypes ...datatype.T) error {
for _, service := range server.Services {
err := service.validate(server.Types...)
if err != nil {
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
}
}
if err := server.collide(); err != nil {
return fmt.Errorf("%s: %w", errFormat, err)
}
return nil
}
// Find a service matching an incoming HTTP request
func (server Server) Find(r *http.Request) *Service {
for _, service := range server.Services {
if matches := service.Match(r); matches {
return service
}
}
return nil
}
// collide returns if there is collision between any service for the same method and colliding paths.
// Note that service path collision detection relies on datatypes:
// - example 1: `/user/{id}` and `/user/articles` will not collide as {id} is an int and "articles" is not
// - example 2: `/user/{name}` and `/user/articles` will collide as {name} is a string so as "articles"
// - example 3: `/user/{name}` and `/user/{id}` will collide as {name} and {id} cannot be checked against their potential values
func (server *Server) collide() error {
length := len(server.Services)
// for each service combination
for a := 0; a < length; a++ {
for b := a + 1; b < length; b++ {
aService := server.Services[a]
bService := server.Services[b]
if aService.Method != bService.Method {
continue
}
aURIParts := SplitURL(aService.Pattern)
bURIParts := SplitURL(bService.Pattern)
if len(aURIParts) != len(bURIParts) {
continue
}
err := checkURICollision(aURIParts, bURIParts, aService.Input, bService.Input)
if err != nil {
return fmt.Errorf("(%s '%s') vs (%s '%s'): %w", aService.Method, aService.Pattern, bService.Method, bService.Pattern, err)
}
}
}
return nil
}
// check if uri of services A and B collide
func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter) error {
var errors = []error{}
// for each part
for pi, aPart := range uriA {
bPart := uriB[pi]
// no need for further check as it has been done earlier in the validation process
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
// both captures -> as we cannot check, consider a collision
if aIsCapture && bIsCapture {
errors = append(errors, fmt.Errorf("%w (path %s and %s)", errPatternCollision, aPart, bPart))
continue
}
// no capture -> check strict equality
if !aIsCapture && !bIsCapture {
if aPart == bPart {
errors = append(errors, fmt.Errorf("%w (same path '%s')", errPatternCollision, aPart))
continue
}
}
// A captures B -> check type (B is A ?)
if aIsCapture {
input, exists := inputA[aPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, aPart))
continue
}
// fail if not valid
if _, valid := input.Validator(bPart); valid {
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, aPart, bPart))
continue
}
// B captures A -> check type (A is B ?)
} else if bIsCapture {
input, exists := inputB[bPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, bPart))
continue
}
// fail if not valid
if _, valid := input.Validator(aPart); valid {
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, bPart, aPart))
continue
}
}
errors = append(errors, nil)
}
// at least 1 URI part not matching -> no collision
var firstError error
for _, err := range errors {
if err != nil && firstError == nil {
firstError = err
}
if err == nil {
return nil
}
}
return firstError
}
// SplitURL without empty sets
func SplitURL(url string) []string {
trimmed := strings.Trim(url, " /\t\r\n")
split := strings.Split(trimmed, "/")
// remove empty set when empty url
if len(split) == 1 && len(split[0]) == 0 {
return []string{}
}
return split
}

View File

@ -21,15 +21,15 @@ func TestLegalServiceName(t *testing.T) {
// empty
{
`[ { "method": "GET", "info": "a", "path": "" } ]`,
ErrInvalidPattern,
errInvalidPattern,
},
{
`[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`,
ErrInvalidPattern,
errInvalidPattern,
},
{
`[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`,
ErrInvalidPattern,
errInvalidPattern,
},
{
`[ { "method": "GET", "info": "a", "path": "/" } ]`,
@ -45,35 +45,35 @@ func TestLegalServiceName(t *testing.T) {
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
ErrUndefinedBraceCapture,
errUndefinedBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
ErrUndefinedBraceCapture,
errUndefinedBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
{
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
ErrInvalidPatternBraceCapture,
errInvalidPatternBraceCapture,
},
}
@ -143,8 +143,8 @@ func TestAvailableMethods(t *testing.T) {
t.FailNow()
}
if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) {
t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err)
if !test.ValidMethod && !errors.Is(err, errUnknownMethod) {
t.Errorf("expected error <%s> got <%s>", errUnknownMethod, err)
t.FailNow()
}
})
@ -217,8 +217,8 @@ func TestParseMissingMethodDescription(t *testing.T) {
t.FailNow()
}
if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) {
t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err)
if !test.ValidDescription && !errors.Is(err, errMissingDescription) {
t.Errorf("expected error <%s> got <%s>", errMissingDescription, err)
t.FailNow()
}
})
@ -321,7 +321,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamDesc,
errMissingParamDesc,
},
{ // invalid param name suffix
`[
@ -334,7 +334,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamDesc,
errMissingParamDesc,
},
{ // missing param description
@ -348,7 +348,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamDesc,
errMissingParamDesc,
},
{ // empty param description
`[
@ -361,7 +361,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamDesc,
errMissingParamDesc,
},
{ // missing param type
@ -375,7 +375,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamType,
errMissingParamType,
},
{ // empty param type
`[
@ -388,7 +388,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMissingParamType,
errMissingParamType,
},
{ // invalid type (optional mark only)
`[
@ -402,7 +402,7 @@ func TestParseParameters(t *testing.T) {
}
]`,
ErrMissingParamType,
errMissingParamType,
},
{ // valid description + valid type
`[
@ -444,7 +444,7 @@ func TestParseParameters(t *testing.T) {
}
]`,
// 2 possible errors as map order is not deterministic
ErrParamNameConflict,
errParamNameConflict,
},
{ // rename conflict with name
`[
@ -459,7 +459,7 @@ func TestParseParameters(t *testing.T) {
}
]`,
// 2 possible errors as map order is not deterministic
ErrParamNameConflict,
errParamNameConflict,
},
{ // rename conflict with rename
`[
@ -474,7 +474,7 @@ func TestParseParameters(t *testing.T) {
}
]`,
// 2 possible errors as map order is not deterministic
ErrParamNameConflict,
errParamNameConflict,
},
{ // both renamed with no conflict
@ -503,7 +503,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMandatoryRename,
errMandatoryRename,
},
{
`[
@ -516,7 +516,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrMandatoryRename,
errMandatoryRename,
},
{
`[
@ -556,7 +556,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrIllegalOptionalURIParam,
errIllegalOptionalURIParam,
},
{ // URI parameter not specified
`[
@ -569,7 +569,7 @@ func TestParseParameters(t *testing.T) {
}
}
]`,
ErrUnspecifiedBraceCapture,
errUnspecifiedBraceCapture,
},
{ // URI parameter not defined
`[
@ -580,7 +580,7 @@ func TestParseParameters(t *testing.T) {
"in": { }
}
]`,
ErrUndefinedBraceCapture,
errUndefinedBraceCapture,
},
}
@ -637,7 +637,7 @@ func TestServiceCollision(t *testing.T) {
"info": "info", "in": {}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -672,7 +672,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -698,7 +698,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -711,7 +711,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -750,7 +750,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -789,7 +789,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -804,7 +804,7 @@ func TestServiceCollision(t *testing.T) {
}
}
]`,
ErrPatternCollision,
errPatternCollision,
},
{
`[
@ -877,6 +877,36 @@ func TestMatchSimple(t *testing.T) {
"/a",
false,
},
{ // root url
`[ {
"method": "GET",
"path": "/a",
"info": "info",
"in": {}
} ]`,
"/",
false,
},
{
`[ {
"method": "GET",
"path": "/a",
"info": "info",
"in": {}
} ]`,
"/",
false,
},
{
`[ {
"method": "GET",
"path": "/",
"info": "info",
"in": {}
} ]`,
"/",
true,
},
{
`[ {
"method": "GET",
@ -997,3 +1027,80 @@ func TestMatchSimple(t *testing.T) {
}
}
func TestFindPriority(t *testing.T) {
t.Parallel()
tests := []struct {
Config string
URL string
MatchingDesc string
}{
{
`[
{ "method": "GET", "path": "/a", "info": "s1" },
{ "method": "GET", "path": "/", "info": "s2" }
]`,
"/",
"s2",
},
{
`[
{ "method": "GET", "path": "/", "info": "s2" },
{ "method": "GET", "path": "/a", "info": "s1" }
]`,
"/",
"s2",
},
{
`[
{ "method": "GET", "path": "/a", "info": "s1" },
{ "method": "GET", "path": "/", "info": "s2" }
]`,
"/a",
"s1",
},
{
`[
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
{ "method": "GET", "path": "/a/b", "info": "s2" }
]`,
"/a/b/c",
"s1",
},
{
`[
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
{ "method": "GET", "path": "/a/b", "info": "s2" }
]`,
"/a/b/",
"s2",
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
srv := &Server{}
srv.Types = append(srv.Types, builtin.AnyDataType{})
srv.Types = append(srv.Types, builtin.IntDataType{})
srv.Types = append(srv.Types, builtin.BoolDataType{})
err := srv.Parse(strings.NewReader(test.Config))
if err != nil {
t.Errorf("unexpected error: '%s'", err)
t.FailNow()
}
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
service := srv.Find(req)
if service == nil {
t.Errorf("expected to find a service")
t.FailNow()
}
if service.Description != test.MatchingDesc {
t.Errorf("expected description '%s', got '%s'", test.MatchingDesc, service.Description)
t.FailNow()
}
})
}
}

View File

@ -3,58 +3,57 @@ package config
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrRead - a problem ocurred when trying to read the configuration file
const ErrRead = cerr("cannot read config")
// errRead - read error
const errRead = cerr("cannot read config")
// ErrUnknownMethod - invalid http method
const ErrUnknownMethod = cerr("unknown HTTP method")
// errUnknownMethod - unknown http method
const errUnknownMethod = cerr("unknown HTTP method")
// ErrFormat - a invalid format has been detected
const ErrFormat = cerr("invalid config format")
// errFormat - invalid format
const errFormat = cerr("invalid config format")
// ErrPatternCollision - there is a collision between 2 services' patterns (same method)
const ErrPatternCollision = cerr("pattern collision")
// errPatternCollision - collision between 2 services' patterns
const errPatternCollision = cerr("pattern collision")
// ErrInvalidPattern - a service pattern is malformed
const ErrInvalidPattern = cerr("must begin with a '/' and not end with")
// errInvalidPattern - malformed service pattern
const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with")
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces")
// errInvalidPatternBraceCapture - invalid brace capture
const errInvalidPatternBraceCapture = cerr("invalid uri parameter")
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path")
// errUnspecifiedBraceCapture - missing path brace capture
const errUnspecifiedBraceCapture = cerr("missing uri parameter")
// ErrMandatoryRename - capture/query parameters must have a rename
const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'")
// errUndefinedBraceCapture - missing capturing brace definition
const errUndefinedBraceCapture = cerr("missing uri parameter definition")
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition")
// errMandatoryRename - capture/query parameters must be renamed
const errMandatoryRename = cerr("uri and query parameters must be renamed")
// ErrMissingDescription - a service is missing its description
const ErrMissingDescription = cerr("missing description")
// errMissingDescription - a service is missing its description
const errMissingDescription = cerr("missing description")
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
// errIllegalOptionalURIParam - uri parameter cannot optional
const errIllegalOptionalURIParam = cerr("uri parameter cannot be optional")
// ErrOptionalOption - an output is optional
const ErrOptionalOption = cerr("output cannot be optional")
// errOptionalOption - cannot have optional output
const errOptionalOption = cerr("output cannot be optional")
// ErrMissingParamDesc - a parameter is missing its description
const ErrMissingParamDesc = cerr("missing parameter description")
// errMissingParamDesc - missing parameter description
const errMissingParamDesc = cerr("missing parameter description")
// ErrUnknownDataType - a parameter has an unknown datatype name
const ErrUnknownDataType = cerr("unknown data type")
// errUnknownDataType - unknown parameter datatype
const errUnknownDataType = cerr("unknown parameter datatype")
// ErrIllegalParamName - a parameter has an illegal name
const ErrIllegalParamName = cerr("illegal parameter name")
// errIllegalParamName - illegal parameter name
const errIllegalParamName = cerr("illegal parameter name")
// ErrMissingParamType - a parameter has an illegal type
const ErrMissingParamType = cerr("missing parameter type")
// errMissingParamType - missing parameter type
const errMissingParamType = cerr("missing parameter type")
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
const ErrParamNameConflict = cerr("name conflict for parameter")
// errParamNameConflict - name/rename conflict
const errParamNameConflict = cerr("parameter name conflict")

View File

@ -1,15 +0,0 @@
package config
import "strings"
// SplitURL without empty sets
func SplitURL(url string) []string {
trimmed := strings.Trim(url, " /\t\r\n")
split := strings.Split(trimmed, "/")
// remove empty set when empty url
if len(split) == 1 && len(split[0]) == 0 {
return []string{}
}
return split
}

View File

@ -11,33 +11,29 @@ type Parameter struct {
Description string `json:"info"`
Type string `json:"type"`
Rename string `json:"name,omitempty"`
// ExtractType is the type of data the datatype returns
Optional bool
// ExtractType is the type the Validator will cast into
ExtractType reflect.Type
// Optional is set to true when the type is prefixed with '?'
Optional bool
// Validator is inferred from @Type
// Validator is inferred from the "type" property
Validator datatype.Validator
}
func (param *Parameter) validate(datatypes ...datatype.T) error {
// missing description
if len(param.Description) < 1 {
return ErrMissingParamDesc
return errMissingParamDesc
}
// invalid type
if len(param.Type) < 1 || param.Type == "?" {
return ErrMissingParamType
return errMissingParamType
}
// optional type transform
// optional type
if param.Type[0] == '?' {
param.Optional = true
param.Type = param.Type[1:]
}
// assign the datatype
// find validator
for _, dtype := range datatypes {
param.Validator = dtype.Build(param.Type, datatypes...)
param.ExtractType = dtype.Type()
@ -46,8 +42,7 @@ func (param *Parameter) validate(datatypes ...datatype.T) error {
}
}
if param.Validator == nil {
return ErrUnknownDataType
return errUnknownDataType
}
return nil
}

View File

@ -1,165 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"io"
"net/http"
"git.xdrm.io/go/aicra/datatype"
)
// Server definition
type Server struct {
Types []datatype.T
Services []*Service
}
// Parse a reader into a server. Server.Types must be set beforehand to
// make datatypes available when checking and formatting the read configuration.
func (srv *Server) Parse(r io.Reader) error {
if err := json.NewDecoder(r).Decode(&srv.Services); err != nil {
return fmt.Errorf("%s: %w", ErrRead, err)
}
if err := srv.validate(); err != nil {
return fmt.Errorf("%s: %w", ErrFormat, err)
}
return nil
}
// validate implements the validator interface
func (server Server) validate(datatypes ...datatype.T) error {
for _, service := range server.Services {
err := service.validate(server.Types...)
if err != nil {
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
}
}
// check for collisions
if err := server.collide(); err != nil {
return fmt.Errorf("%s: %w", ErrFormat, err)
}
return nil
}
// Find a service matching an incoming HTTP request
func (server Server) Find(r *http.Request) *Service {
for _, service := range server.Services {
if matches := service.Match(r); matches {
return service
}
}
return nil
}
// collide returns if there is collision between services
func (server *Server) collide() error {
length := len(server.Services)
// for each service combination
for a := 0; a < length; a++ {
for b := a + 1; b < length; b++ {
aService := server.Services[a]
bService := server.Services[b]
// ignore different method
if aService.Method != bService.Method {
continue
}
aParts := SplitURL(aService.Pattern)
bParts := SplitURL(bService.Pattern)
// not same size
if len(aParts) != len(bParts) {
continue
}
partErrors := make([]error, 0)
// for each part
for pi, aPart := range aParts {
bPart := bParts[pi]
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
// both captures -> as we cannot check, consider a collision
if aIsCapture && bIsCapture {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (path %s and %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart, bPart))
continue
}
// no capture -> check equal
if !aIsCapture && !bIsCapture {
if aPart == bPart {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (same path '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart))
continue
}
}
// A captures B -> check type (B is A ?)
if aIsCapture {
input, exists := aService.Input[aPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart))
continue
}
// fail if not valid
if _, valid := input.Validator(bPart); valid {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart, bPart))
continue
}
// B captures A -> check type (A is B ?)
} else if bIsCapture {
input, exists := bService.Input[bPart]
// fail if no type or no validator
if !exists || input.Validator == nil {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, bPart))
continue
}
// fail if not valid
if _, valid := input.Validator(aPart); valid {
partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, bPart, aPart))
continue
}
}
partErrors = append(partErrors, nil)
}
// if at least 1 url part does not match -> ok
var firstError error
oneMismatch := false
for _, err := range partErrors {
if err != nil && firstError == nil {
firstError = err
}
if err == nil {
oneMismatch = true
continue
}
}
if !oneMismatch {
return firstError
}
}
}
return nil
}

View File

@ -22,15 +22,15 @@ type Service struct {
Input map[string]*Parameter `json:"in"`
Output map[string]*Parameter `json:"out"`
// references to url parameters
// format: '/uri/{param}'
// Captures contains references to URI parameters from the `Input` map. The format
// of these parameter names is "{paramName}"
Captures []*BraceCapture
// references to Query parameters
// format: 'GET@paranName'
// Query contains references to HTTP Query parameters from the `Input` map.
// Query parameters names are "GET@paramName", this map contains escaped names (e.g. "paramName")
Query map[string]*Parameter
// references for form parameters (all but Captures and Query)
// Form references form parameters from the `Input` map (all but Captures and Query).
Form map[string]*Parameter
}
@ -43,16 +43,12 @@ type BraceCapture struct {
// Match returns if this service would handle this HTTP request
func (svc *Service) Match(req *http.Request) bool {
// method
if req.Method != svc.Method {
return false
}
// check path
if !svc.matchPattern(req.RequestURI) {
return false
}
return true
}
@ -61,13 +57,12 @@ func (svc *Service) matchPattern(uri string) bool {
uriparts := SplitURL(uri)
parts := SplitURL(svc.Pattern)
// fail if size differ
if len(uriparts) != len(parts) {
return false
}
// root url '/'
if len(parts) == 0 {
if len(parts) == 0 && len(uriparts) == 0 {
return true
}
@ -118,7 +113,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error {
// check description
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
return fmt.Errorf("field 'description': %w", errMissingDescription)
}
// check input parameters
@ -130,7 +125,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error {
// fail if a brace capture remains undefined
for _, capture := range svc.Captures {
if capture.Ref == nil {
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
return fmt.Errorf("field 'in': %s: %w", capture.Name, errUndefinedBraceCapture)
}
}
@ -149,7 +144,7 @@ func (svc *Service) isMethodAvailable() error {
return nil
}
}
return ErrUnknownMethod
return errUnknownMethod
}
func (svc *Service) isPatternValid() error {
@ -157,13 +152,13 @@ func (svc *Service) isPatternValid() error {
// empty pattern
if length < 1 {
return ErrInvalidPattern
return errInvalidPattern
}
if length > 1 {
// pattern not starting with '/' or ending with '/'
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' {
return ErrInvalidPattern
return errInvalidPattern
}
}
@ -171,7 +166,7 @@ func (svc *Service) isPatternValid() error {
parts := SplitURL(svc.Pattern)
for i, part := range parts {
if len(part) < 1 {
return ErrInvalidPattern
return errInvalidPattern
}
// if brace capture
@ -192,7 +187,7 @@ func (svc *Service) isPatternValid() error {
// fail on invalid format
if strings.ContainsAny(part, "{}") {
return ErrInvalidPatternBraceCapture
return errInvalidPatternBraceCapture
}
}
@ -211,7 +206,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// for each parameter
for paramName, param := range svc.Input {
if len(paramName) < 1 {
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
return fmt.Errorf("%s: %w", paramName, errIllegalParamName)
}
// fail if brace capture does not exists in pattern
@ -228,7 +223,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
}
}
if !found {
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture)
return fmt.Errorf("%s: %w", paramName, errUnspecifiedBraceCapture)
}
iscapture = true
@ -251,7 +246,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// fail if capture or query without rename
if len(param.Rename) < 1 && (iscapture || isquery) {
return fmt.Errorf("%s: %w", paramName, ErrMandatoryRename)
return fmt.Errorf("%s: %w", paramName, errMandatoryRename)
}
// use param name if no rename
@ -266,7 +261,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// capture parameter cannot be optional
if iscapture && param.Optional {
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
return fmt.Errorf("%s: %w", paramName, errIllegalOptionalURIParam)
}
// fail on name/rename conflict
@ -280,7 +275,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// 3.2.2. Not-renamed field matches a renamed field
// 3.2.3. Renamed field matches name
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
return fmt.Errorf("%s: %w", paramName, errParamNameConflict)
}
}
@ -301,7 +296,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
// for each parameter
for paramName, param := range svc.Output {
if len(paramName) < 1 {
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
return fmt.Errorf("%s: %w", paramName, errIllegalParamName)
}
// use param name if no rename
@ -315,7 +310,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
}
if param.Optional {
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption)
return fmt.Errorf("%s: %w", paramName, errOptionalOption)
}
// fail on name/rename conflict
@ -329,7 +324,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
// 3.2.2. Not-renamed field matches a renamed field
// 3.2.3. Renamed field matches name
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
return fmt.Errorf("%s: %w", paramName, errParamNameConflict)
}
}

View File

@ -3,49 +3,48 @@ package dynfunc
// cerr allows you to create constant "const" error with type boxing.
type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string {
return string(err)
}
// ErrHandlerNotFunc - handler is not a func
const ErrHandlerNotFunc = cerr("handler must be a func")
// errHandlerNotFunc - handler is not a func
const errHandlerNotFunc = cerr("handler must be a func")
// ErrNoServiceForHandler - no service matching this handler
const ErrNoServiceForHandler = cerr("no service found for this handler")
// errNoServiceForHandler - no service matching this handler
const errNoServiceForHandler = cerr("no service found for this handler")
// ErrMissingHandlerArgumentParam - missing params arguments for handler
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
// errMissingHandlerArgumentParam - missing params arguments for handler
const errMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
// ErrUnexpectedInput - input argument is not expected
const ErrUnexpectedInput = cerr("unexpected input struct")
// errUnexpectedInput - input argument is not expected
const errUnexpectedInput = cerr("unexpected input struct")
// ErrMissingHandlerOutput - missing output for handler
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output")
// errMissingHandlerOutput - missing output for handler
const errMissingHandlerOutput = cerr("handler must have at least 1 output")
// ErrMissingHandlerOutputError - missing error output for handler
const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error")
// errMissingHandlerOutputError - missing error output for handler
const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Error")
// ErrMissingRequestArgument - missing request argument for handler
const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request")
// errMissingRequestArgument - missing request argument for handler
const errMissingRequestArgument = cerr("handler first argument must be of type api.Request")
// ErrMissingParamArgument - missing parameters argument for handler
const ErrMissingParamArgument = cerr("handler second argument must be a struct")
// errMissingParamArgument - missing parameters argument for handler
const errMissingParamArgument = cerr("handler second argument must be a struct")
// ErrUnexportedName - argument is unexported in struct
const ErrUnexportedName = cerr("unexported name")
// errUnexportedName - argument is unexported in struct
const errUnexportedName = cerr("unexported name")
// ErrMissingParamOutput - missing output argument for handler
const ErrMissingParamOutput = cerr("handler first output must be a *struct")
// errMissingParamOutput - missing output argument for handler
const errMissingParamOutput = cerr("handler first output must be a *struct")
// ErrMissingParamFromConfig - missing a parameter in handler struct
const ErrMissingParamFromConfig = cerr("missing a parameter from configuration")
// errMissingParamFromConfig - missing a parameter in handler struct
const errMissingParamFromConfig = cerr("missing a parameter from configuration")
// ErrMissingOutputFromConfig - missing a parameter in handler struct
const ErrMissingOutputFromConfig = cerr("missing a parameter from configuration")
// errMissingOutputFromConfig - missing a parameter in handler struct
const errMissingOutputFromConfig = cerr("missing a parameter from configuration")
// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
const ErrWrongParamTypeFromConfig = cerr("invalid struct field type")
// errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
const errWrongParamTypeFromConfig = cerr("invalid struct field type")
// ErrMissingHandlerErrorOutput - missing handler output error
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error")
// errMissingHandlerErrorOutput - missing handler output error
const errMissingHandlerErrorOutput = cerr("last output must be of type api.Error")

View File

@ -8,6 +8,12 @@ import (
"git.xdrm.io/go/aicra/internal/config"
)
// Handler represents a dynamic api handler
type Handler struct {
spec spec
fn interface{}
}
// Build a handler from a service configuration and a dynamic function
//
// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)`
@ -26,7 +32,7 @@ func Build(fn interface{}, service config.Service) (*Handler, error) {
fnv := reflect.ValueOf(fn)
if fnv.Type().Kind() != reflect.Func {
return nil, ErrHandlerNotFunc
return nil, errHandlerNotFunc
}
if err := h.spec.checkInput(fnv); err != nil {

View File

@ -9,6 +9,11 @@ import (
"git.xdrm.io/go/aicra/internal/config"
)
type spec struct {
Input map[string]reflect.Type
Output map[string]reflect.Type
}
// builds a spec from the configuration service
func makeSpec(service config.Service) spec {
spec := spec{
@ -45,34 +50,34 @@ func (s spec) checkInput(fnv reflect.Value) error {
// no input -> ok
if len(s.Input) == 0 {
if fnt.NumIn() > 0 {
return ErrUnexpectedInput
return errUnexpectedInput
}
return nil
}
if fnt.NumIn() != 1 {
return ErrMissingHandlerArgumentParam
return errMissingHandlerArgumentParam
}
// arg must be a struct
structArg := fnt.In(0)
if structArg.Kind() != reflect.Struct {
return ErrMissingParamArgument
return errMissingParamArgument
}
// check for invalid param
for name, ptype := range s.Input {
if name[0] == strings.ToLower(name)[0] {
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
return fmt.Errorf("%s: %w", name, errUnexportedName)
}
field, exists := structArg.FieldByName(name)
if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig)
return fmt.Errorf("%s: %w", name, errMissingParamFromConfig)
}
if !ptype.AssignableTo(field.Type) {
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype)
}
}
@ -83,13 +88,13 @@ func (s spec) checkInput(fnv reflect.Value) error {
func (s spec) checkOutput(fnv reflect.Value) error {
fnt := fnv.Type()
if fnt.NumOut() < 1 {
return ErrMissingHandlerOutput
return errMissingHandlerOutput
}
// last output must be api.Error
errOutput := fnt.Out(fnt.NumOut() - 1)
if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) {
return ErrMissingHandlerErrorOutput
return errMissingHandlerErrorOutput
}
// no output -> ok
@ -98,29 +103,29 @@ func (s spec) checkOutput(fnv reflect.Value) error {
}
if fnt.NumOut() != 2 {
return ErrMissingParamOutput
return errMissingParamOutput
}
// fail if first output is not a pointer to struct
structOutputPtr := fnt.Out(0)
if structOutputPtr.Kind() != reflect.Ptr {
return ErrMissingParamOutput
return errMissingParamOutput
}
structOutput := structOutputPtr.Elem()
if structOutput.Kind() != reflect.Struct {
return ErrMissingParamOutput
return errMissingParamOutput
}
// fail on invalid output
for name, ptype := range s.Output {
if name[0] == strings.ToLower(name)[0] {
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
return fmt.Errorf("%s: %w", name, errUnexportedName)
}
field, exists := structOutput.FieldByName(name)
if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig)
return fmt.Errorf("%s: %w", name, errMissingOutputFromConfig)
}
// ignore types evalutating to nil
@ -129,7 +134,7 @@ func (s spec) checkOutput(fnv reflect.Value) error {
}
if !field.Type.ConvertibleTo(ptype) {
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype)
}
}

View File

@ -25,7 +25,7 @@ func TestInputCheck(t *testing.T) {
{
Input: map[string]reflect.Type{},
Fn: func(int, string) {},
Err: ErrUnexpectedInput,
Err: errUnexpectedInput,
},
// missing input struct in func
{
@ -33,7 +33,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() {},
Err: ErrMissingHandlerArgumentParam,
Err: errMissingHandlerArgumentParam,
},
// input not a struct
{
@ -41,7 +41,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func(int) {},
Err: ErrMissingParamArgument,
Err: errMissingParamArgument,
},
// unexported param name
{
@ -49,7 +49,7 @@ func TestInputCheck(t *testing.T) {
"test1": reflect.TypeOf(int(0)),
},
Fn: func(struct{}) {},
Err: ErrUnexportedName,
Err: errUnexportedName,
},
// input field missing
{
@ -57,7 +57,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func(struct{}) {},
Err: ErrMissingParamFromConfig,
Err: errMissingParamFromConfig,
},
// input field invalid type
{
@ -65,7 +65,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func(struct{ Test1 string }) {},
Err: ErrWrongParamTypeFromConfig,
Err: errWrongParamTypeFromConfig,
},
// input field valid type
{
@ -115,13 +115,13 @@ func TestOutputCheck(t *testing.T) {
{
Output: map[string]reflect.Type{},
Fn: func() {},
Err: ErrMissingHandlerOutput,
Err: errMissingHandlerOutput,
},
// no input -> with last type not api.Error
{
Output: map[string]reflect.Type{},
Fn: func() bool { return true },
Err: ErrMissingHandlerErrorOutput,
Err: errMissingHandlerErrorOutput,
},
// no input -> with api.Error
{
@ -141,7 +141,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() api.Error { return api.ErrorSuccess },
Err: ErrMissingParamOutput,
Err: errMissingParamOutput,
},
// output not a pointer
{
@ -149,7 +149,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() (int, api.Error) { return 0, api.ErrorSuccess },
Err: ErrMissingParamOutput,
Err: errMissingParamOutput,
},
// output not a pointer to struct
{
@ -157,7 +157,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess },
Err: ErrMissingParamOutput,
Err: errMissingParamOutput,
},
// unexported param name
{
@ -165,7 +165,7 @@ func TestOutputCheck(t *testing.T) {
"test1": reflect.TypeOf(int(0)),
},
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
Err: ErrUnexportedName,
Err: errUnexportedName,
},
// output field missing
{
@ -173,7 +173,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
Err: ErrMissingParamFromConfig,
Err: errMissingParamFromConfig,
},
// output field invalid type
{
@ -181,7 +181,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)),
},
Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess },
Err: ErrWrongParamTypeFromConfig,
Err: errWrongParamTypeFromConfig,
},
// output field valid type
{

View File

@ -1,14 +0,0 @@
package dynfunc
import "reflect"
// Handler represents a dynamic api handler
type Handler struct {
spec spec
fn interface{}
}
type spec struct {
Input map[string]reflect.Type
Output map[string]reflect.Type
}

View File

@ -13,19 +13,19 @@ func (comp *Component) parseHeaders(_raw []byte) error {
// 1. Extract lines
_lines := strings.Split(string(_raw), "\n")
if len(_lines) < 2 {
return ErrNoHeader
return errNoHeader
}
// 2. trim each line + remove 'Content-Disposition' prefix
header := strings.Trim(_lines[0], " \t\r")
if !strings.HasPrefix(header, "Content-Disposition: form-data;") {
return ErrNoHeader
return errNoHeader
}
header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r")
if len(header) < 1 {
return ErrNoHeader
return errNoHeader
}
// 3. Extract each key-value pair

View File

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

View File

@ -71,11 +71,11 @@ func (reader *Reader) Parse() error {
name := comp.GetHeader("name")
if len(name) < 1 {
return ErrMissingDataName
return errMissingDataName
}
if _, nameUsed := reader.Data[name]; nameUsed {
return ErrDataNameConflict
return errDataNameConflict
}
reader.Data[name] = comp

View File

@ -196,8 +196,8 @@ func TestNoName(t *testing.T) {
return
}
if err = mpr.Parse(); err != ErrMissingDataName {
t.Errorf("expected the error <%s>, got <%s>", ErrMissingDataName, err)
if err = mpr.Parse(); err != errMissingDataName {
t.Errorf("expected the error <%s>, got <%s>", errMissingDataName, err)
return
}
})
@ -238,8 +238,8 @@ func TestNoHeader(t *testing.T) {
return
}
if err = mpr.Parse(); err != ErrNoHeader {
t.Errorf("expected the error <%s>, got <%s>", ErrNoHeader, err)
if err = mpr.Parse(); err != errNoHeader {
t.Errorf("expected the error <%s>, got <%s>", errNoHeader, err)
return
}
})
@ -274,8 +274,8 @@ facebook.com
t.Fatalf("unexpected error <%s>", err)
}
if err = mpr.Parse(); err != ErrDataNameConflict {
t.Fatalf("expected the error <%s>, got <%s>", ErrDataNameConflict, err)
if err = mpr.Parse(); err != errDataNameConflict {
t.Fatalf("expected the error <%s>, got <%s>", errDataNameConflict, err)
}
}

View File

@ -13,33 +13,29 @@ import (
"strings"
)
// Set represents all data that can be caught:
// T represents all data that can be caught from an http request for a specific
// configuration Service; it features:
// - URI (from the URI)
// - GET (default url data)
// - GET (standard url data)
// - POST (from json, form-data, url-encoded)
// - 'application/json' => key-value pair is parsed as json into the map
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
// - 'multipart/form-data' => parse form-data format
type Set struct {
type T struct {
service *config.Service
// contains URL+GET+FORM data with prefixes:
// - FORM: no prefix
// - URL: '{uri_var}'
// - GET: 'GET@' followed by the key in GET
Data map[string]interface{}
Data map[string]interface{}
}
// New creates a new empty store.
func New(service *config.Service) *Set {
return &Set{
func New(service *config.Service) *T {
return &T{
service: service,
Data: make(map[string]interface{}),
Data: map[string]interface{}{},
}
}
// ExtractURI fills 'Set' with creating pointers inside 'Url'
func (i *Set) ExtractURI(req http.Request) error {
// GetURI parameters
func (i *T) GetURI(req http.Request) error {
uriparts := config.SplitURL(req.URL.RequestURI())
for _, capture := range i.service.Captures {
@ -54,122 +50,97 @@ func (i *Set) ExtractURI(req http.Request) error {
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
}
// parse parameter
parsed := parseParameter(value)
// check type
cast, valid := capture.Ref.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
}
// store cast value in 'Set'
i.Data[capture.Ref.Rename] = cast
}
return nil
}
// ExtractQuery data from the url query parameters
func (i *Set) ExtractQuery(req http.Request) error {
// GetQuery data from the url query parameters
func (i *T) GetQuery(req http.Request) error {
query := req.URL.Query()
for name, param := range i.service.Query {
value, exist := query[name]
// fail on missing required
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
// optional
if !exist {
continue
}
// parse parameter
parsed := parseParameter(value)
// check type
cast, valid := param.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
}
// store cast value
i.Data[param.Rename] = cast
}
return nil
}
// ExtractForm data from request
//
// GetForm parameters the from request
// - parse 'form-data' if not supported for non-POST requests
// - parse 'x-www-form-urlencoded'
// - parse 'application/json'
func (i *Set) ExtractForm(req http.Request) error {
// ignore GET method
func (i *T) GetForm(req http.Request) error {
if req.Method == http.MethodGet {
return nil
}
contentType := req.Header.Get("Content-Type")
// parse json
if strings.HasPrefix(contentType, "application/json") {
ct := req.Header.Get("Content-Type")
switch {
case strings.HasPrefix(ct, "application/json"):
return i.parseJSON(req)
}
// parse urlencoded
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
return i.parseUrlencoded(req)
}
// parse multipart
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
case strings.HasPrefix(ct, "multipart/form-data; boundary="):
return i.parseMultipart(req)
}
// nothing to parse
return nil
default:
return nil
}
}
// parseJSON parses JSON from the request body inside 'Form'
// and 'Set'
func (i *Set) parseJSON(req http.Request) error {
parsed := make(map[string]interface{}, 0)
func (i *T) parseJSON(req http.Request) error {
var parsed map[string]interface{}
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&parsed); err != nil {
if err == io.EOF {
return nil
}
err := decoder.Decode(&parsed)
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
}
for name, param := range i.service.Form {
value, exist := parsed[name]
// fail on missing required
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
// optional
if !exist {
continue
}
// fail on invalid type
cast, valid := param.Validator(value)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
}
// store cast value
i.Data[param.Rename] = cast
}
@ -178,8 +149,7 @@ func (i *Set) parseJSON(req http.Request) error {
// parseUrlencoded parses urlencoded from the request body inside 'Form'
// and 'Set'
func (i *Set) parseUrlencoded(req http.Request) error {
// use http.Request interface
func (i *T) parseUrlencoded(req http.Request) error {
if err := req.ParseForm(); err != nil {
return err
}
@ -187,26 +157,19 @@ func (i *Set) parseUrlencoded(req http.Request) error {
for name, param := range i.service.Form {
value, exist := req.PostForm[name]
// fail on missing required
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
// optional
if !exist {
continue
}
// parse parameter
parsed := parseParameter(value)
// check type
cast, valid := param.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
}
// store cast value
i.Data[param.Rename] = cast
}
@ -215,46 +178,37 @@ func (i *Set) parseUrlencoded(req http.Request) error {
// parseMultipart parses multi-part from the request body inside 'Form'
// and 'Set'
func (i *Set) parseMultipart(req http.Request) error {
// 1. create reader
func (i *T) parseMultipart(req http.Request) error {
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
mpr, err := multipart.NewReader(req.Body, boundary)
if err == io.EOF {
return nil
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
// 2. parse multipart
if err = mpr.Parse(); err != nil {
err = mpr.Parse()
if err != nil {
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
}
for name, param := range i.service.Form {
component, exist := mpr.Data[name]
// fail on missing required
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
// optional
if !exist {
continue
}
// parse parameter
parsed := parseParameter(string(component.Data))
// fail on invalid type
cast, valid := param.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
}
// store cast value
i.Data[param.Rename] = cast
}
@ -266,58 +220,47 @@ func (i *Set) parseMultipart(req http.Request) error {
// - []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)
rt := reflect.TypeOf(data)
rv := reflect.ValueOf(data)
switch dtype.Kind() {
switch rt.Kind() {
/* (1) []string -> recursive */
// []string -> recursive
case reflect.Slice:
// 1. ignore empty
if dvalue.Len() == 0 {
if rv.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())
slice := make([]interface{}, rv.Len())
for i, l := 0, rv.Len(); i < l; i++ {
element := rv.Index(i)
slice[i] = parseParameter(element.Interface())
}
return result
return slice
/* (2) string -> parse */
// string -> parse as json
// keep as string if invalid json
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
var cast interface{}
wrapper := fmt.Sprintf("{\"wrapped\":%s}", rv.String())
err := json.Unmarshal([]byte(wrapper), &cast)
if err != nil {
return dvalue.String()
return rv.String()
}
mapval, ok := result.(map[string]interface{})
mapval, ok := cast.(map[string]interface{})
if !ok {
return dvalue.String()
return rv.String()
}
wrapped, ok := mapval["wrapped"]
if !ok {
return dvalue.String()
return rv.String()
}
return wrapped
// any type -> unchanged
default:
return rv.Interface()
}
/* (3) NIL if unknown type */
return dvalue.Interface()
}

View File

@ -131,7 +131,7 @@ func TestStoreWithUri(t *testing.T) {
store := New(service)
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
err := store.ExtractURI(*req)
err := store.GetURI(*req)
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
@ -242,7 +242,7 @@ func TestExtractQuery(t *testing.T) {
store := New(getServiceWithQuery(test.ServiceParam...))
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
err := store.ExtractQuery(*req)
err := store.GetQuery(*req)
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
@ -324,7 +324,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
// defer req.Body.Close()
store := New(nil)
err := store.ExtractForm(*req)
err := store.GetForm(*req)
if err == nil {
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
t.FailNow()
@ -420,7 +420,7 @@ func TestExtractFormUrlEncoded(t *testing.T) {
defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req)
err := store.GetForm(*req)
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
@ -563,7 +563,7 @@ func TestJsonParameters(t *testing.T) {
defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req)
err := store.GetForm(*req)
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
@ -720,7 +720,7 @@ x
defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req)
err := store.GetForm(*req)
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {

View File

@ -18,14 +18,14 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// 1. find a matching service in the config
service := server.conf.Find(req)
if service == nil {
errorHandler(api.ErrorUnknownService)
errorHandler(api.ErrorUnknownService).ServeHTTP(res, req)
return
}
// 2. extract request data
dataset, err := extractRequestData(service, *req)
if err != nil {
errorHandler(api.ErrorMissingParam)
errorHandler(api.ErrorMissingParam).ServeHTTP(res, req)
return
}
@ -39,7 +39,7 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// 4. fail if found no handler
if handler == nil {
errorHandler(api.ErrorUncallableService)
errorHandler(api.ErrorUncallableService).ServeHTTP(res, req)
return
}
@ -76,23 +76,23 @@ func errorHandler(err api.Error) http.HandlerFunc {
}
}
func extractRequestData(service *config.Service, req http.Request) (*reqdata.Set, error) {
func extractRequestData(service *config.Service, req http.Request) (*reqdata.T, error) {
dataset := reqdata.New(service)
// 3. extract URI data
err := dataset.ExtractURI(req)
err := dataset.GetURI(req)
if err != nil {
return nil, err
}
// 4. extract query data
err = dataset.ExtractQuery(req)
err = dataset.GetQuery(req)
if err != nil {
return nil, err
}
// 5. extract form/json data
err = dataset.ExtractForm(req)
err = dataset.GetForm(req)
if err != nil {
return nil, err
}