Compare commits
13 Commits
6319761731
...
fb69dbb903
Author | SHA1 | Date |
---|---|---|
Adrien Marquès | fb69dbb903 | |
Adrien Marquès | 658c66d2db | |
Adrien Marquès | 3c453e7f89 | |
Adrien Marquès | d198086dd4 | |
Adrien Marquès | 30862195a1 | |
Adrien Marquès | 990bb86919 | |
Adrien Marquès | 35ede5e266 | |
Adrien Marquès | 90472b8bf7 | |
Adrien Marquès | df56496a16 | |
Adrien Marquès | caa57889b4 | |
Adrien Marquès | 4ba62e19c7 | |
Adrien Marquès | 5cadfcf78b | |
Adrien Marquès | e0ea0c97c5 |
155
README.md
155
README.md
|
@ -7,28 +7,22 @@
|
||||||
[![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)
|
||||||
|
@ -38,9 +32,9 @@ The aicra server fulfills the `net/http` [Server interface](https://golang.org/p
|
||||||
|
|
||||||
<!-- 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,12 +44,12 @@ 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
|
||||||
|
@ -63,82 +57,99 @@ 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.
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
13
api/error.go
13
api/error.go
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
10
builder.go
10
builder.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
21
errors.go
21
errors.go
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
ExtractType reflect.Type
|
|
||||||
// Optional is set to true when the type is prefixed with '?'
|
|
||||||
Optional bool
|
Optional bool
|
||||||
|
// ExtractType is the type the Validator will cast into
|
||||||
// Validator is inferred from @Type
|
ExtractType reflect.Type
|
||||||
|
// Validator is inferred from the "type" property
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
// contains URL+GET+FORM data with prefixes:
|
|
||||||
// - FORM: no prefix
|
|
||||||
// - URL: '{uri_var}'
|
|
||||||
// - GET: 'GET@' followed by the key in GET
|
|
||||||
Data map[string]interface{}
|
Data map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new empty store.
|
// 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 != nil {
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if err != 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()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
14
server.go
14
server.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue