Compare commits

..

13 Commits

Author SHA1 Message Date
Adrien Marquès fb69dbb903 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
2020-04-04 15:33:43 +00:00
Adrien Marquès 658c66d2db
update readme
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2020-04-04 17:33:08 +02:00
Adrien Marquès 3c453e7f89
remove api useless request, update default errors and bind status codes to errors
continuous-integration/drone/push Build is passing Details
2020-04-04 16:03:50 +02:00
Adrien Marquès d198086dd4
fix http error handlers 2020-04-04 16:03:12 +02:00
Adrien Marquès 30862195a1
config: refactor, simplify, test, remove redundant comments 2020-04-04 15:39:00 +02:00
Adrien Marquès 990bb86919
rework reqdata api and remove redundant comments 2020-04-04 14:56:15 +02:00
Adrien Marquès 35ede5e266
unexport config errors 2020-04-04 14:34:20 +02:00
Adrien Marquès 90472b8bf7
unexport dynfunc errors 2020-04-04 12:46:43 +02:00
Adrien Marquès df56496a16
dynfunc: normalize file names 2020-04-04 12:45:36 +02:00
Adrien Marquès caa57889b4
multipart: rename files and unexport errors 2020-04-04 12:43:55 +02:00
Adrien Marquès 4ba62e19c7
remove func.go and standardize main file name 2020-04-04 12:42:18 +02:00
Adrien Marquès 5cadfcf78b
unexport aicra errors 2020-04-04 12:40:21 +02:00
Adrien Marquès e0ea0c97c5
clarify datatype comments and standardize file name 2020-04-04 12:40:01 +02:00
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) [![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 - handlers
- optionnally middle-wares (_e.g. authentication, csrf_) - optionnally middle-wares (_e.g. authentication, csrf_)
- and optionnally your custom type checkers to check input parameters - 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). ## Table of contents
> A example project is available [here](https://git.xdrm.io/go/tiny-url-ex)
### Table of contents
<!-- toc --> <!-- toc -->
- [I/ Installation](#i-installation) - [I/ Installation](#i-installation)
- [II/ Development](#ii-development) - [II/ Usage](#ii-usage)
* [1) Main executable](#1-main-executable) * [1) Build a server](#1-build-a-server)
* [2) API Configuration](#2-api-configuration) * [2) API Configuration](#2-api-configuration)
- [Definition](#definition) - [Definition](#definition)
+ [Input Arguments](#input-arguments) + [Input Arguments](#input-arguments)
- [1. Input types](#1-input-types) - [1. Input types](#1-input-types)
- [2. Global Format](#2-global-format) - [2. Global Format](#2-global-format)
- [III/ Change Log](#iii-change-log) - [III/ Change Log](#iii-change-log)
<!-- tocstop --> <!-- 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 ```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. 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 ```go
package main package main
import ( import (
"log" "log"
"net/http" "net/http"
"os"
"git.xdrm.io/go/aicra" "git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/datatype" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype/builtin" "git.xdrm.io/go/aicra/datatype/builtin"
) )
func main() { func main() {
// 1. select your datatypes (builtin, custom) builder := &aicra.Builder{}
var dtypes []datatype.T
dtypes = append(dtypes, builtin.AnyDataType{})
dtypes = append(dtypes, builtin.BoolDataType{})
dtypes = append(dtypes, builtin.UintDataType{})
dtypes = append(dtypes, builtin.StringDataType{})
// 2. create the server from the configuration file // add datatypes your api uses
server, err := aicra.New("path/to/your/api/definition.json", dtypes...) builder.AddType(builtin.BoolDataType{})
builder.AddType(builtin.UintDataType{})
builder.AddType(builtin.StringDataType{})
config, err := os.Open("./api.json")
if err != nil { if err != nil {
log.Fatalf("cannot built aicra server: %s\n", err) log.Fatalf("cannot open config: %s", err)
} }
// 3. bind your implementations // pass your configuration
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){ err = builder.Setup(config)
// ... process stuff ... config.Close()
res.SetError(api.ErrorSuccess());
})
// 4. extract to http server
httpServer, err := server.ToHTTPServer()
if err != nil { if err != nil {
log.Fatalf("cannot get to http server: %s", err) log.Fatalf("invalid config: %s", err)
} }
// 4. launch server // bind your handlers
log.Fatal( http.ListenAndServe("localhost:8080", server) ) 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 - routes and their methods
- every input for each method (called *argument*) - every input for each method (called *argument*)
- every output for each method - every output for each method
- scope permissions (list of permissions needed by clients) - scope permissions (list of permissions needed by clients)
- input policy : - input policy :
- type of argument (_i.e. for data types_) - type of argument (_c.f. data types_)
- required/optional - required/optional
- variable renaming - variable renaming
#### Format
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.
###### 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.
| field path | description | example | | field path | description | example |
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `info` | A short human-readable description of what the method does | `create a new user` | | `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) | | `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. | | | `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 is only use for readability purpose and documentation. | | | `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 ### Input Arguments
###### 1. Input types
Input arguments defines what data from the HTTP request the method needs. Aicra is able to extract 3 types of data : 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. - **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. - **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. - **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. 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 : 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 ```json
[ [
{ {
@ -184,32 +191,6 @@ 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`.
### III/ Change Log - 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`.
- [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.

View File

@ -1,5 +1,7 @@
package api package api
import "net/http"
var ( var (
// ErrorUnknown represents any error which cause is unknown. // ErrorUnknown represents any error which cause is unknown.
// It might also be used for debug purposes as this error // It might also be used for debug purposes as this error
@ -19,20 +21,17 @@ var (
// unique fields already exists // unique fields already exists
ErrorAlreadyExists Error = 3 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 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 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 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 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 has to be set when a file upload failed
ErrorUpload Error = 100 ErrorUpload Error = 100
@ -90,7 +89,6 @@ var errorReasons = map[Error]string{
ErrorFailure: "it failed", ErrorFailure: "it failed",
ErrorNoMatchFound: "resource not found", ErrorNoMatchFound: "resource not found",
ErrorAlreadyExists: "already exists", ErrorAlreadyExists: "already exists",
ErrorConfig: "configuration error",
ErrorCreation: "create error", ErrorCreation: "create error",
ErrorModification: "update error", ErrorModification: "update error",
ErrorDeletion: "delete error", ErrorDeletion: "delete error",
@ -108,3 +106,26 @@ var errorReasons = map[Error]string{
ErrorInvalidParam: "invalid parameter", ErrorInvalidParam: "invalid parameter",
ErrorInvalidDefaultParam: "invalid default param", 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 ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
) )
// Error represents an http response error following the api format. // 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. // directly into the response as JSON alongside response output fields.
type Error int type Error int
// Error implements the error interface
func (e Error) Error() string { func (e Error) Error() string {
// use unknown error if no reason
reason, ok := errorReasons[e] reason, ok := errorReasons[e]
if !ok { if !ok {
return ErrorUnknown.Error() return ErrorUnknown.Error()
} }
return fmt.Sprintf("[%d] %s", e, reason) 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 // MarshalJSON implements encoding/json.Marshaler interface
func (e Error) MarshalJSON() ([]byte, error) { func (e Error) MarshalJSON() ([]byte, error) {
// use unknown error if no reason // 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 { func (res *Response) WithError(err Error) *Response {
res.err = err res.err = err
return res return res
} }
// Error implements the error interface and dispatches to internal error.
func (res *Response) Error() string { func (res *Response) Error() string {
return res.err.Error() return res.err.Error()
} }
@ -42,36 +41,23 @@ func (res *Response) SetData(name string, value interface{}) {
res.Data[name] = value 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 // MarshalJSON implements the 'json.Marshaler' interface and is used
// to generate the JSON representation of the response // to generate the JSON representation of the response
func (res *Response) MarshalJSON() ([]byte, error) { func (res *Response) MarshalJSON() ([]byte, error) {
fmt := make(map[string]interface{}) fmt := make(map[string]interface{})
for k, v := range res.Data { for k, v := range res.Data {
fmt[k] = v fmt[k] = v
} }
fmt["error"] = res.err fmt["error"] = res.err
return json.Marshal(fmt) return json.Marshal(fmt)
} }
// ServeHTTP implements http.Handler and writes the API response.
func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error { 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) encoded, err := json.Marshal(res)
if err != nil { if err != nil {
return err return err
} }
w.Write(encoded) w.Write(encoded)
return nil return nil
} }

View File

@ -29,7 +29,7 @@ func (b *Builder) AddType(t datatype.T) {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
if b.conf.Services != nil { if b.conf.Services != nil {
panic(ErrLateType) panic(errLateType)
} }
b.conf.Types = append(b.conf.Types, t) b.conf.Types = append(b.conf.Types, t)
} }
@ -41,7 +41,7 @@ func (b *Builder) Setup(r io.Reader) error {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
if b.conf.Services != nil { if b.conf.Services != nil {
panic(ErrAlreadySetup) panic(errAlreadySetup)
} }
return b.conf.Parse(r) 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 // Bind a dynamic handler to a REST service
func (b *Builder) Bind(method, path string, fn interface{}) error { func (b *Builder) Bind(method, path string, fn interface{}) error {
if b.conf.Services == nil { if b.conf.Services == nil {
return ErrNotSetup return errNotSetup
} }
// find associated service // find associated service
@ -62,7 +62,7 @@ func (b *Builder) Bind(method, path string, fn interface{}) error {
} }
if service == nil { 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) dyn, err := dynfunc.Build(fn, *service)
@ -91,7 +91,7 @@ func (b Builder) Build() (http.Handler, error) {
} }
} }
if !hasAssociatedHandler { 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. // cerr allows you to create constant "const" error with type boxing.
type cerr string type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrLateType - cannot add datatype after setting up the definition // errLateType - cannot add datatype after setting up the definition
const ErrLateType = cerr("types cannot be added after Setup") const errLateType = cerr("types cannot be added after Setup")
// ErrNotSetup - not set up yet // errNotSetup - not set up yet
const ErrNotSetup = cerr("not set up") const errNotSetup = cerr("not set up")
// ErrAlreadySetup - already set up // errAlreadySetup - already set up
const ErrAlreadySetup = cerr("already set up") const errAlreadySetup = cerr("already set up")
// ErrUnknownService - no service matching this handler // errUnknownService - no service matching this handler
const ErrUnknownService = cerr("unknown service") const errUnknownService = cerr("unknown service")
// ErrMissingHandler - missing handler // errMissingHandler - missing handler
const ErrMissingHandler = cerr("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 // empty
{ {
`[ { "method": "GET", "info": "a", "path": "" } ]`, `[ { "method": "GET", "info": "a", "path": "" } ]`,
ErrInvalidPattern, errInvalidPattern,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`, `[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`,
ErrInvalidPattern, errInvalidPattern,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`, `[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`,
ErrInvalidPattern, errInvalidPattern,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/" } ]`, `[ { "method": "GET", "info": "a", "path": "/" } ]`,
@ -45,35 +45,35 @@ func TestLegalServiceName(t *testing.T) {
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
ErrUndefinedBraceCapture, errUndefinedBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
ErrUndefinedBraceCapture, errUndefinedBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
{ {
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`, `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
ErrInvalidPatternBraceCapture, errInvalidPatternBraceCapture,
}, },
} }
@ -143,8 +143,8 @@ func TestAvailableMethods(t *testing.T) {
t.FailNow() t.FailNow()
} }
if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) { if !test.ValidMethod && !errors.Is(err, errUnknownMethod) {
t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err) t.Errorf("expected error <%s> got <%s>", errUnknownMethod, err)
t.FailNow() t.FailNow()
} }
}) })
@ -217,8 +217,8 @@ func TestParseMissingMethodDescription(t *testing.T) {
t.FailNow() t.FailNow()
} }
if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) { if !test.ValidDescription && !errors.Is(err, errMissingDescription) {
t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err) t.Errorf("expected error <%s> got <%s>", errMissingDescription, err)
t.FailNow() t.FailNow()
} }
}) })
@ -321,7 +321,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamDesc, errMissingParamDesc,
}, },
{ // invalid param name suffix { // invalid param name suffix
`[ `[
@ -334,7 +334,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamDesc, errMissingParamDesc,
}, },
{ // missing param description { // missing param description
@ -348,7 +348,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamDesc, errMissingParamDesc,
}, },
{ // empty param description { // empty param description
`[ `[
@ -361,7 +361,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamDesc, errMissingParamDesc,
}, },
{ // missing param type { // missing param type
@ -375,7 +375,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamType, errMissingParamType,
}, },
{ // empty param type { // empty param type
`[ `[
@ -388,7 +388,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrMissingParamType, errMissingParamType,
}, },
{ // invalid type (optional mark only) { // invalid type (optional mark only)
`[ `[
@ -402,7 +402,7 @@ func TestParseParameters(t *testing.T) {
} }
]`, ]`,
ErrMissingParamType, errMissingParamType,
}, },
{ // valid description + valid type { // valid description + valid type
`[ `[
@ -444,7 +444,7 @@ func TestParseParameters(t *testing.T) {
} }
]`, ]`,
// 2 possible errors as map order is not deterministic // 2 possible errors as map order is not deterministic
ErrParamNameConflict, errParamNameConflict,
}, },
{ // rename conflict with name { // rename conflict with name
`[ `[
@ -459,7 +459,7 @@ func TestParseParameters(t *testing.T) {
} }
]`, ]`,
// 2 possible errors as map order is not deterministic // 2 possible errors as map order is not deterministic
ErrParamNameConflict, errParamNameConflict,
}, },
{ // rename conflict with rename { // rename conflict with rename
`[ `[
@ -474,7 +474,7 @@ func TestParseParameters(t *testing.T) {
} }
]`, ]`,
// 2 possible errors as map order is not deterministic // 2 possible errors as map order is not deterministic
ErrParamNameConflict, errParamNameConflict,
}, },
{ // both renamed with no conflict { // 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 { // URI parameter not specified
`[ `[
@ -569,7 +569,7 @@ func TestParseParameters(t *testing.T) {
} }
} }
]`, ]`,
ErrUnspecifiedBraceCapture, errUnspecifiedBraceCapture,
}, },
{ // URI parameter not defined { // URI parameter not defined
`[ `[
@ -580,7 +580,7 @@ func TestParseParameters(t *testing.T) {
"in": { } "in": { }
} }
]`, ]`,
ErrUndefinedBraceCapture, errUndefinedBraceCapture,
}, },
} }
@ -637,7 +637,7 @@ func TestServiceCollision(t *testing.T) {
"info": "info", "in": {} "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", "/a",
false, 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", "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. // cerr allows you to create constant "const" error with type boxing.
type cerr string type cerr string
// Error implements the error builtin interface.
func (err cerr) 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 - read error
const ErrRead = cerr("cannot read config") const errRead = cerr("cannot read config")
// ErrUnknownMethod - invalid http method // errUnknownMethod - unknown http method
const ErrUnknownMethod = cerr("unknown HTTP method") const errUnknownMethod = cerr("unknown HTTP method")
// ErrFormat - a invalid format has been detected // errFormat - invalid format
const ErrFormat = cerr("invalid config format") const errFormat = cerr("invalid config format")
// ErrPatternCollision - there is a collision between 2 services' patterns (same method) // errPatternCollision - collision between 2 services' patterns
const ErrPatternCollision = cerr("pattern collision") const errPatternCollision = cerr("pattern collision")
// ErrInvalidPattern - a service pattern is malformed // errInvalidPattern - malformed service pattern
const ErrInvalidPattern = cerr("must begin with a '/' and not end with") const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with")
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid // errInvalidPatternBraceCapture - invalid brace capture
const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces") const errInvalidPatternBraceCapture = cerr("invalid uri parameter")
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern // errUnspecifiedBraceCapture - missing path brace capture
const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path") const errUnspecifiedBraceCapture = cerr("missing uri parameter")
// ErrMandatoryRename - capture/query parameters must have a rename // errUndefinedBraceCapture - missing capturing brace definition
const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'") const errUndefinedBraceCapture = cerr("missing uri parameter definition")
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters // errMandatoryRename - capture/query parameters must be renamed
const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition") const errMandatoryRename = cerr("uri and query parameters must be renamed")
// ErrMissingDescription - a service is missing its description // errMissingDescription - a service is missing its description
const ErrMissingDescription = cerr("missing description") const errMissingDescription = cerr("missing description")
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional // errIllegalOptionalURIParam - uri parameter cannot optional
const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional") const errIllegalOptionalURIParam = cerr("uri parameter cannot be optional")
// ErrOptionalOption - an output is optional // errOptionalOption - cannot have optional output
const ErrOptionalOption = cerr("output cannot be optional") const errOptionalOption = cerr("output cannot be optional")
// ErrMissingParamDesc - a parameter is missing its description // errMissingParamDesc - missing parameter description
const ErrMissingParamDesc = cerr("missing parameter description") const errMissingParamDesc = cerr("missing parameter description")
// ErrUnknownDataType - a parameter has an unknown datatype name // errUnknownDataType - unknown parameter datatype
const ErrUnknownDataType = cerr("unknown data type") const errUnknownDataType = cerr("unknown parameter datatype")
// ErrIllegalParamName - a parameter has an illegal name // errIllegalParamName - illegal parameter name
const ErrIllegalParamName = cerr("illegal parameter name") const errIllegalParamName = cerr("illegal parameter name")
// ErrMissingParamType - a parameter has an illegal type // errMissingParamType - missing parameter type
const ErrMissingParamType = cerr("missing parameter type") const errMissingParamType = cerr("missing parameter type")
// ErrParamNameConflict - a parameter has a conflict with its name/rename field // errParamNameConflict - name/rename conflict
const ErrParamNameConflict = cerr("name conflict for parameter") 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"` Description string `json:"info"`
Type string `json:"type"` Type string `json:"type"`
Rename string `json:"name,omitempty"` 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 ExtractType reflect.Type
// Optional is set to true when the type is prefixed with '?' // Validator is inferred from the "type" property
Optional bool
// Validator is inferred from @Type
Validator datatype.Validator Validator datatype.Validator
} }
func (param *Parameter) validate(datatypes ...datatype.T) error { func (param *Parameter) validate(datatypes ...datatype.T) error {
// missing description
if len(param.Description) < 1 { if len(param.Description) < 1 {
return ErrMissingParamDesc return errMissingParamDesc
} }
// invalid type
if len(param.Type) < 1 || param.Type == "?" { if len(param.Type) < 1 || param.Type == "?" {
return ErrMissingParamType return errMissingParamType
} }
// optional type transform // optional type
if param.Type[0] == '?' { if param.Type[0] == '?' {
param.Optional = true param.Optional = true
param.Type = param.Type[1:] param.Type = param.Type[1:]
} }
// assign the datatype // find validator
for _, dtype := range datatypes { for _, dtype := range datatypes {
param.Validator = dtype.Build(param.Type, datatypes...) param.Validator = dtype.Build(param.Type, datatypes...)
param.ExtractType = dtype.Type() param.ExtractType = dtype.Type()
@ -46,8 +42,7 @@ func (param *Parameter) validate(datatypes ...datatype.T) error {
} }
} }
if param.Validator == nil { if param.Validator == nil {
return ErrUnknownDataType return errUnknownDataType
} }
return nil 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"` Input map[string]*Parameter `json:"in"`
Output map[string]*Parameter `json:"out"` Output map[string]*Parameter `json:"out"`
// references to url parameters // Captures contains references to URI parameters from the `Input` map. The format
// format: '/uri/{param}' // of these parameter names is "{paramName}"
Captures []*BraceCapture Captures []*BraceCapture
// references to Query parameters // Query contains references to HTTP Query parameters from the `Input` map.
// format: 'GET@paranName' // Query parameters names are "GET@paramName", this map contains escaped names (e.g. "paramName")
Query map[string]*Parameter 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 Form map[string]*Parameter
} }
@ -43,16 +43,12 @@ type BraceCapture struct {
// Match returns if this service would handle this HTTP request // Match returns if this service would handle this HTTP request
func (svc *Service) Match(req *http.Request) bool { func (svc *Service) Match(req *http.Request) bool {
// method
if req.Method != svc.Method { if req.Method != svc.Method {
return false return false
} }
// check path
if !svc.matchPattern(req.RequestURI) { if !svc.matchPattern(req.RequestURI) {
return false return false
} }
return true return true
} }
@ -61,13 +57,12 @@ func (svc *Service) matchPattern(uri string) bool {
uriparts := SplitURL(uri) uriparts := SplitURL(uri)
parts := SplitURL(svc.Pattern) parts := SplitURL(svc.Pattern)
// fail if size differ
if len(uriparts) != len(parts) { if len(uriparts) != len(parts) {
return false return false
} }
// root url '/' // root url '/'
if len(parts) == 0 { if len(parts) == 0 && len(uriparts) == 0 {
return true return true
} }
@ -118,7 +113,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error {
// check description // check description
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 { 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 // check input parameters
@ -130,7 +125,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error {
// fail if a brace capture remains undefined // fail if a brace capture remains undefined
for _, capture := range svc.Captures { for _, capture := range svc.Captures {
if capture.Ref == nil { 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 nil
} }
} }
return ErrUnknownMethod return errUnknownMethod
} }
func (svc *Service) isPatternValid() error { func (svc *Service) isPatternValid() error {
@ -157,13 +152,13 @@ func (svc *Service) isPatternValid() error {
// empty pattern // empty pattern
if length < 1 { if length < 1 {
return ErrInvalidPattern return errInvalidPattern
} }
if length > 1 { if length > 1 {
// pattern not starting with '/' or ending with '/' // pattern not starting with '/' or ending with '/'
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' { 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) parts := SplitURL(svc.Pattern)
for i, part := range parts { for i, part := range parts {
if len(part) < 1 { if len(part) < 1 {
return ErrInvalidPattern return errInvalidPattern
} }
// if brace capture // if brace capture
@ -192,7 +187,7 @@ func (svc *Service) isPatternValid() error {
// fail on invalid format // fail on invalid format
if strings.ContainsAny(part, "{}") { if strings.ContainsAny(part, "{}") {
return ErrInvalidPatternBraceCapture return errInvalidPatternBraceCapture
} }
} }
@ -211,7 +206,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// for each parameter // for each parameter
for paramName, param := range svc.Input { for paramName, param := range svc.Input {
if len(paramName) < 1 { 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 // fail if brace capture does not exists in pattern
@ -228,7 +223,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
} }
} }
if !found { if !found {
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture) return fmt.Errorf("%s: %w", paramName, errUnspecifiedBraceCapture)
} }
iscapture = true iscapture = true
@ -251,7 +246,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// fail if capture or query without rename // fail if capture or query without rename
if len(param.Rename) < 1 && (iscapture || isquery) { 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 // use param name if no rename
@ -266,7 +261,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
// capture parameter cannot be optional // capture parameter cannot be optional
if iscapture && param.Optional { if iscapture && param.Optional {
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam) return fmt.Errorf("%s: %w", paramName, errIllegalOptionalURIParam)
} }
// fail on name/rename conflict // 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.2. Not-renamed field matches a renamed field
// 3.2.3. Renamed field matches name // 3.2.3. Renamed field matches name
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename { 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 each parameter
for paramName, param := range svc.Output { for paramName, param := range svc.Output {
if len(paramName) < 1 { if len(paramName) < 1 {
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName) return fmt.Errorf("%s: %w", paramName, errIllegalParamName)
} }
// use param name if no rename // use param name if no rename
@ -315,7 +310,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
} }
if param.Optional { if param.Optional {
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption) return fmt.Errorf("%s: %w", paramName, errOptionalOption)
} }
// fail on name/rename conflict // 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.2. Not-renamed field matches a renamed field
// 3.2.3. Renamed field matches name // 3.2.3. Renamed field matches name
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename { 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. // cerr allows you to create constant "const" error with type boxing.
type cerr string type cerr string
// Error implements the error builtin interface.
func (err cerr) Error() string { func (err cerr) Error() string {
return string(err) return string(err)
} }
// ErrHandlerNotFunc - handler is not a func // errHandlerNotFunc - handler is not a func
const ErrHandlerNotFunc = cerr("handler must be a func") const errHandlerNotFunc = cerr("handler must be a func")
// ErrNoServiceForHandler - no service matching this handler // errNoServiceForHandler - no service matching this handler
const ErrNoServiceForHandler = cerr("no service found for this handler") const errNoServiceForHandler = cerr("no service found for this handler")
// ErrMissingHandlerArgumentParam - missing params arguments for handler // errMissingHandlerArgumentParam - missing params arguments for handler
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct") const errMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
// ErrUnexpectedInput - input argument is not expected // errUnexpectedInput - input argument is not expected
const ErrUnexpectedInput = cerr("unexpected input struct") const errUnexpectedInput = cerr("unexpected input struct")
// ErrMissingHandlerOutput - missing output for handler // errMissingHandlerOutput - missing output for handler
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output") const errMissingHandlerOutput = cerr("handler must have at least 1 output")
// ErrMissingHandlerOutputError - missing error output for handler // errMissingHandlerOutputError - missing error output for handler
const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error") const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Error")
// ErrMissingRequestArgument - missing request argument for handler // errMissingRequestArgument - missing request argument for handler
const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request") const errMissingRequestArgument = cerr("handler first argument must be of type api.Request")
// ErrMissingParamArgument - missing parameters argument for handler // errMissingParamArgument - missing parameters argument for handler
const ErrMissingParamArgument = cerr("handler second argument must be a struct") const errMissingParamArgument = cerr("handler second argument must be a struct")
// ErrUnexportedName - argument is unexported in struct // errUnexportedName - argument is unexported in struct
const ErrUnexportedName = cerr("unexported name") const errUnexportedName = cerr("unexported name")
// ErrMissingParamOutput - missing output argument for handler // errMissingParamOutput - missing output argument for handler
const ErrMissingParamOutput = cerr("handler first output must be a *struct") const errMissingParamOutput = cerr("handler first output must be a *struct")
// ErrMissingParamFromConfig - missing a parameter in handler struct // errMissingParamFromConfig - missing a parameter in handler struct
const ErrMissingParamFromConfig = cerr("missing a parameter from configuration") const errMissingParamFromConfig = cerr("missing a parameter from configuration")
// ErrMissingOutputFromConfig - missing a parameter in handler struct // errMissingOutputFromConfig - missing a parameter in handler struct
const ErrMissingOutputFromConfig = cerr("missing a parameter from configuration") const errMissingOutputFromConfig = cerr("missing a parameter from configuration")
// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct // errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
const ErrWrongParamTypeFromConfig = cerr("invalid struct field type") const errWrongParamTypeFromConfig = cerr("invalid struct field type")
// ErrMissingHandlerErrorOutput - missing handler output error // errMissingHandlerErrorOutput - missing handler output error
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.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" "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 // Build a handler from a service configuration and a dynamic function
// //
// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)` // @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) fnv := reflect.ValueOf(fn)
if fnv.Type().Kind() != reflect.Func { if fnv.Type().Kind() != reflect.Func {
return nil, ErrHandlerNotFunc return nil, errHandlerNotFunc
} }
if err := h.spec.checkInput(fnv); err != nil { if err := h.spec.checkInput(fnv); err != nil {

View File

@ -9,6 +9,11 @@ import (
"git.xdrm.io/go/aicra/internal/config" "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 // builds a spec from the configuration service
func makeSpec(service config.Service) spec { func makeSpec(service config.Service) spec {
spec := spec{ spec := spec{
@ -45,34 +50,34 @@ func (s spec) checkInput(fnv reflect.Value) error {
// no input -> ok // no input -> ok
if len(s.Input) == 0 { if len(s.Input) == 0 {
if fnt.NumIn() > 0 { if fnt.NumIn() > 0 {
return ErrUnexpectedInput return errUnexpectedInput
} }
return nil return nil
} }
if fnt.NumIn() != 1 { if fnt.NumIn() != 1 {
return ErrMissingHandlerArgumentParam return errMissingHandlerArgumentParam
} }
// arg must be a struct // arg must be a struct
structArg := fnt.In(0) structArg := fnt.In(0)
if structArg.Kind() != reflect.Struct { if structArg.Kind() != reflect.Struct {
return ErrMissingParamArgument return errMissingParamArgument
} }
// check for invalid param // check for invalid param
for name, ptype := range s.Input { for name, ptype := range s.Input {
if name[0] == strings.ToLower(name)[0] { 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) field, exists := structArg.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig) return fmt.Errorf("%s: %w", name, errMissingParamFromConfig)
} }
if !ptype.AssignableTo(field.Type) { 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 { func (s spec) checkOutput(fnv reflect.Value) error {
fnt := fnv.Type() fnt := fnv.Type()
if fnt.NumOut() < 1 { if fnt.NumOut() < 1 {
return ErrMissingHandlerOutput return errMissingHandlerOutput
} }
// last output must be api.Error // last output must be api.Error
errOutput := fnt.Out(fnt.NumOut() - 1) errOutput := fnt.Out(fnt.NumOut() - 1)
if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) { if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) {
return ErrMissingHandlerErrorOutput return errMissingHandlerErrorOutput
} }
// no output -> ok // no output -> ok
@ -98,29 +103,29 @@ func (s spec) checkOutput(fnv reflect.Value) error {
} }
if fnt.NumOut() != 2 { if fnt.NumOut() != 2 {
return ErrMissingParamOutput return errMissingParamOutput
} }
// fail if first output is not a pointer to struct // fail if first output is not a pointer to struct
structOutputPtr := fnt.Out(0) structOutputPtr := fnt.Out(0)
if structOutputPtr.Kind() != reflect.Ptr { if structOutputPtr.Kind() != reflect.Ptr {
return ErrMissingParamOutput return errMissingParamOutput
} }
structOutput := structOutputPtr.Elem() structOutput := structOutputPtr.Elem()
if structOutput.Kind() != reflect.Struct { if structOutput.Kind() != reflect.Struct {
return ErrMissingParamOutput return errMissingParamOutput
} }
// fail on invalid output // fail on invalid output
for name, ptype := range s.Output { for name, ptype := range s.Output {
if name[0] == strings.ToLower(name)[0] { 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) field, exists := structOutput.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig) return fmt.Errorf("%s: %w", name, errMissingOutputFromConfig)
} }
// ignore types evalutating to nil // ignore types evalutating to nil
@ -129,7 +134,7 @@ func (s spec) checkOutput(fnv reflect.Value) error {
} }
if !field.Type.ConvertibleTo(ptype) { 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{}, Input: map[string]reflect.Type{},
Fn: func(int, string) {}, Fn: func(int, string) {},
Err: ErrUnexpectedInput, Err: errUnexpectedInput,
}, },
// missing input struct in func // missing input struct in func
{ {
@ -33,7 +33,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() {}, Fn: func() {},
Err: ErrMissingHandlerArgumentParam, Err: errMissingHandlerArgumentParam,
}, },
// input not a struct // input not a struct
{ {
@ -41,7 +41,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(int) {}, Fn: func(int) {},
Err: ErrMissingParamArgument, Err: errMissingParamArgument,
}, },
// unexported param name // unexported param name
{ {
@ -49,7 +49,7 @@ func TestInputCheck(t *testing.T) {
"test1": reflect.TypeOf(int(0)), "test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{}) {}, Fn: func(struct{}) {},
Err: ErrUnexportedName, Err: errUnexportedName,
}, },
// input field missing // input field missing
{ {
@ -57,7 +57,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{}) {}, Fn: func(struct{}) {},
Err: ErrMissingParamFromConfig, Err: errMissingParamFromConfig,
}, },
// input field invalid type // input field invalid type
{ {
@ -65,7 +65,7 @@ func TestInputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{ Test1 string }) {}, Fn: func(struct{ Test1 string }) {},
Err: ErrWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
// input field valid type // input field valid type
{ {
@ -115,13 +115,13 @@ func TestOutputCheck(t *testing.T) {
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() {}, Fn: func() {},
Err: ErrMissingHandlerOutput, Err: errMissingHandlerOutput,
}, },
// no input -> with last type not api.Error // no input -> with last type not api.Error
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() bool { return true }, Fn: func() bool { return true },
Err: ErrMissingHandlerErrorOutput, Err: errMissingHandlerErrorOutput,
}, },
// no input -> with api.Error // no input -> with api.Error
{ {
@ -141,7 +141,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() api.Error { return api.ErrorSuccess }, Fn: func() api.Error { return api.ErrorSuccess },
Err: ErrMissingParamOutput, Err: errMissingParamOutput,
}, },
// output not a pointer // output not a pointer
{ {
@ -149,7 +149,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (int, api.Error) { return 0, api.ErrorSuccess }, Fn: func() (int, api.Error) { return 0, api.ErrorSuccess },
Err: ErrMissingParamOutput, Err: errMissingParamOutput,
}, },
// output not a pointer to struct // output not a pointer to struct
{ {
@ -157,7 +157,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess }, Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess },
Err: ErrMissingParamOutput, Err: errMissingParamOutput,
}, },
// unexported param name // unexported param name
{ {
@ -165,7 +165,7 @@ func TestOutputCheck(t *testing.T) {
"test1": reflect.TypeOf(int(0)), "test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
Err: ErrUnexportedName, Err: errUnexportedName,
}, },
// output field missing // output field missing
{ {
@ -173,7 +173,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess },
Err: ErrMissingParamFromConfig, Err: errMissingParamFromConfig,
}, },
// output field invalid type // output field invalid type
{ {
@ -181,7 +181,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess }, Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess },
Err: ErrWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
// output field valid type // 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 // 1. Extract lines
_lines := strings.Split(string(_raw), "\n") _lines := strings.Split(string(_raw), "\n")
if len(_lines) < 2 { if len(_lines) < 2 {
return ErrNoHeader return errNoHeader
} }
// 2. trim each line + remove 'Content-Disposition' prefix // 2. trim each line + remove 'Content-Disposition' prefix
header := strings.Trim(_lines[0], " \t\r") header := strings.Trim(_lines[0], " \t\r")
if !strings.HasPrefix(header, "Content-Disposition: form-data;") { if !strings.HasPrefix(header, "Content-Disposition: form-data;") {
return ErrNoHeader return errNoHeader
} }
header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r") header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r")
if len(header) < 1 { if len(header) < 1 {
return ErrNoHeader return errNoHeader
} }
// 3. Extract each key-value pair // 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. // cerr allows you to create constant "const" error with type boxing.
type cerr string type cerr string
// Error implements the error builtin interface.
func (err cerr) 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 = cerr("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 = cerr("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 = cerr("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

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

View File

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

View File

@ -13,33 +13,29 @@ import (
"strings" "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) // - URI (from the URI)
// - GET (default url data) // - GET (standard url data)
// - POST (from json, form-data, url-encoded) // - POST (from json, form-data, url-encoded)
// - 'application/json' => key-value pair is parsed as json into the map // - 'application/json' => key-value pair is parsed as json into the map
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters // - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
// - 'multipart/form-data' => parse form-data format // - 'multipart/form-data' => parse form-data format
type Set struct { type T struct {
service *config.Service service *config.Service
Data map[string]interface{}
// 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{}
} }
// New creates a new empty store. // New creates a new empty store.
func New(service *config.Service) *Set { func New(service *config.Service) *T {
return &Set{ return &T{
service: service, service: service,
Data: make(map[string]interface{}), Data: map[string]interface{}{},
} }
} }
// ExtractURI fills 'Set' with creating pointers inside 'Url' // GetURI parameters
func (i *Set) ExtractURI(req http.Request) error { func (i *T) GetURI(req http.Request) error {
uriparts := config.SplitURL(req.URL.RequestURI()) uriparts := config.SplitURL(req.URL.RequestURI())
for _, capture := range i.service.Captures { 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) return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
} }
// parse parameter
parsed := parseParameter(value) parsed := parseParameter(value)
// check type
cast, valid := capture.Ref.Validator(parsed) 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'
i.Data[capture.Ref.Rename] = cast i.Data[capture.Ref.Rename] = cast
} }
return nil return nil
} }
// ExtractQuery data from the url query parameters // GetQuery data from the url query parameters
func (i *Set) ExtractQuery(req http.Request) error { func (i *T) GetQuery(req http.Request) error {
query := req.URL.Query() query := req.URL.Query()
for name, param := range i.service.Query { for name, param := range i.service.Query {
value, exist := query[name] value, exist := query[name]
// fail on missing required
if !exist && !param.Optional { if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
} }
// optional
if !exist { if !exist {
continue continue
} }
// parse parameter
parsed := parseParameter(value) parsed := parseParameter(value)
// check type
cast, valid := param.Validator(parsed) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store cast value
i.Data[param.Rename] = cast i.Data[param.Rename] = cast
} }
return nil return nil
} }
// ExtractForm data from request // GetForm parameters the from request
//
// - parse 'form-data' if not supported for non-POST requests // - parse 'form-data' if not supported for non-POST requests
// - parse 'x-www-form-urlencoded' // - parse 'x-www-form-urlencoded'
// - parse 'application/json' // - parse 'application/json'
func (i *Set) ExtractForm(req http.Request) error { func (i *T) GetForm(req http.Request) error {
// ignore GET method
if req.Method == http.MethodGet { if req.Method == http.MethodGet {
return nil return nil
} }
contentType := req.Header.Get("Content-Type") ct := req.Header.Get("Content-Type")
switch {
// parse json case strings.HasPrefix(ct, "application/json"):
if strings.HasPrefix(contentType, "application/json") {
return i.parseJSON(req) return i.parseJSON(req)
}
// parse urlencoded case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
return i.parseUrlencoded(req) return i.parseUrlencoded(req)
}
// parse multipart case strings.HasPrefix(ct, "multipart/form-data; boundary="):
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
return i.parseMultipart(req) return i.parseMultipart(req)
}
// nothing to parse default:
return nil return nil
}
} }
// parseJSON parses JSON from the request body inside 'Form' // parseJSON parses JSON from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *Set) parseJSON(req http.Request) error { func (i *T) parseJSON(req http.Request) error {
var parsed map[string]interface{}
parsed := make(map[string]interface{}, 0)
decoder := json.NewDecoder(req.Body) decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&parsed); err != nil { err := decoder.Decode(&parsed)
if err == io.EOF { if err == io.EOF {
return nil return nil
} }
if err != nil {
return fmt.Errorf("%s: %w", err, ErrInvalidJSON) return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
} }
for name, param := range i.service.Form { for name, param := range i.service.Form {
value, exist := parsed[name] value, exist := parsed[name]
// fail on missing required
if !exist && !param.Optional { if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
} }
// optional
if !exist { if !exist {
continue continue
} }
// fail on invalid type
cast, valid := param.Validator(value) cast, valid := param.Validator(value)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store cast value
i.Data[param.Rename] = cast 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' // parseUrlencoded parses urlencoded from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *Set) parseUrlencoded(req http.Request) error { func (i *T) parseUrlencoded(req http.Request) error {
// use http.Request interface
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
@ -187,26 +157,19 @@ func (i *Set) parseUrlencoded(req http.Request) error {
for name, param := range i.service.Form { for name, param := range i.service.Form {
value, exist := req.PostForm[name] value, exist := req.PostForm[name]
// fail on missing required
if !exist && !param.Optional { if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
} }
// optional
if !exist { if !exist {
continue continue
} }
// parse parameter
parsed := parseParameter(value) parsed := parseParameter(value)
// check type
cast, valid := param.Validator(parsed) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store cast value
i.Data[param.Rename] = cast 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' // parseMultipart parses multi-part from the request body inside 'Form'
// and 'Set' // and 'Set'
func (i *Set) parseMultipart(req http.Request) error { func (i *T) parseMultipart(req http.Request) error {
// 1. create reader
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
mpr, err := multipart.NewReader(req.Body, boundary) mpr, err := multipart.NewReader(req.Body, boundary)
if err == io.EOF {
return nil
}
if err != nil { if err != nil {
if err == io.EOF {
return nil
}
return err return err
} }
// 2. parse multipart err = mpr.Parse()
if err = mpr.Parse(); err != nil { if err != nil {
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart) return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
} }
for name, param := range i.service.Form { for name, param := range i.service.Form {
component, exist := mpr.Data[name] component, exist := mpr.Data[name]
// fail on missing required
if !exist && !param.Optional { if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
} }
// optional
if !exist { if !exist {
continue continue
} }
// parse parameter
parsed := parseParameter(string(component.Data)) parsed := parseParameter(string(component.Data))
// fail on invalid type
cast, valid := param.Validator(parsed) cast, valid := param.Validator(parsed)
if !valid { if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType) return fmt.Errorf("%s: %w", name, ErrInvalidType)
} }
// store cast value
i.Data[param.Rename] = cast 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 array of json elements
// - string : return json if valid, else return raw string // - string : return json if valid, else return raw string
func parseParameter(data interface{}) interface{} { func parseParameter(data interface{}) interface{} {
dtype := reflect.TypeOf(data) rt := reflect.TypeOf(data)
dvalue := reflect.ValueOf(data) rv := reflect.ValueOf(data)
switch dtype.Kind() { switch rt.Kind() {
/* (1) []string -> recursive */ // []string -> recursive
case reflect.Slice: case reflect.Slice:
if rv.Len() == 0 {
// 1. ignore empty
if dvalue.Len() == 0 {
return data return data
} }
// 2. parse each element recursively slice := make([]interface{}, rv.Len())
result := make([]interface{}, dvalue.Len()) for i, l := 0, rv.Len(); i < l; i++ {
element := rv.Index(i)
for i, l := 0, dvalue.Len(); i < l; i++ { slice[i] = parseParameter(element.Interface())
element := dvalue.Index(i)
result[i] = parseParameter(element.Interface())
} }
return result return slice
/* (2) string -> parse */ // string -> parse as json
// keep as string if invalid json
case reflect.String: case reflect.String:
var cast interface{}
// build json wrapper wrapper := fmt.Sprintf("{\"wrapped\":%s}", rv.String())
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) err := json.Unmarshal([]byte(wrapper), &cast)
// try to parse as json
var result interface{}
err := json.Unmarshal([]byte(wrapper), &result)
// return if success
if err != nil { if err != nil {
return dvalue.String() return rv.String()
} }
mapval, ok := result.(map[string]interface{}) mapval, ok := cast.(map[string]interface{})
if !ok { if !ok {
return dvalue.String() return rv.String()
} }
wrapped, ok := mapval["wrapped"] wrapped, ok := mapval["wrapped"]
if !ok { if !ok {
return dvalue.String() return rv.String()
} }
return wrapped 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) store := New(service)
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil) req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
err := store.ExtractURI(*req) err := store.GetURI(*req)
if err != nil { if err != nil {
if test.Err != nil { if test.Err != nil {
if !errors.Is(err, test.Err) { if !errors.Is(err, test.Err) {
@ -242,7 +242,7 @@ func TestExtractQuery(t *testing.T) {
store := New(getServiceWithQuery(test.ServiceParam...)) store := New(getServiceWithQuery(test.ServiceParam...))
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil) 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 err != nil {
if test.Err != nil { if test.Err != nil {
if !errors.Is(err, test.Err) { if !errors.Is(err, test.Err) {
@ -324,7 +324,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
// defer req.Body.Close() // defer req.Body.Close()
store := New(nil) store := New(nil)
err := store.ExtractForm(*req) err := store.GetForm(*req)
if err == nil { if err == nil {
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data)) t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
t.FailNow() t.FailNow()
@ -420,7 +420,7 @@ func TestExtractFormUrlEncoded(t *testing.T) {
defer req.Body.Close() defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...)) store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req) err := store.GetForm(*req)
if err != nil { if err != nil {
if test.Err != nil { if test.Err != nil {
if !errors.Is(err, test.Err) { if !errors.Is(err, test.Err) {
@ -563,7 +563,7 @@ func TestJsonParameters(t *testing.T) {
defer req.Body.Close() defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...)) store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req) err := store.GetForm(*req)
if err != nil { if err != nil {
if test.Err != nil { if test.Err != nil {
if !errors.Is(err, test.Err) { if !errors.Is(err, test.Err) {
@ -720,7 +720,7 @@ x
defer req.Body.Close() defer req.Body.Close()
store := New(getServiceWithForm(test.ServiceParams...)) store := New(getServiceWithForm(test.ServiceParams...))
err := store.ExtractForm(*req) err := store.GetForm(*req)
if err != nil { if err != nil {
if test.Err != nil { if test.Err != nil {
if !errors.Is(err, test.Err) { 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 // 1. find a matching service in the config
service := server.conf.Find(req) service := server.conf.Find(req)
if service == nil { if service == nil {
errorHandler(api.ErrorUnknownService) errorHandler(api.ErrorUnknownService).ServeHTTP(res, req)
return return
} }
// 2. extract request data // 2. extract request data
dataset, err := extractRequestData(service, *req) dataset, err := extractRequestData(service, *req)
if err != nil { if err != nil {
errorHandler(api.ErrorMissingParam) errorHandler(api.ErrorMissingParam).ServeHTTP(res, req)
return return
} }
@ -39,7 +39,7 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// 4. fail if found no handler // 4. fail if found no handler
if handler == nil { if handler == nil {
errorHandler(api.ErrorUncallableService) errorHandler(api.ErrorUncallableService).ServeHTTP(res, req)
return 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) dataset := reqdata.New(service)
// 3. extract URI data // 3. extract URI data
err := dataset.ExtractURI(req) err := dataset.GetURI(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 4. extract query data // 4. extract query data
err = dataset.ExtractQuery(req) err = dataset.GetQuery(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 5. extract form/json data // 5. extract form/json data
err = dataset.ExtractForm(req) err = dataset.GetForm(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }