Compare commits
63 Commits
acdba4121b
...
4877d0ea23
Author | SHA1 | Date |
---|---|---|
Adrien Marquès | 4877d0ea23 | |
Adrien Marquès | 8a0a20294c | |
Adrien Marquès | d7acf771ad | |
Adrien Marquès | a5424d8941 | |
Adrien Marquès | a3daab7de4 | |
Adrien Marquès | 00e2a96c79 | |
Adrien Marquès | e7dd1e7a56 | |
Adrien Marquès | 081e48002f | |
Adrien Marquès | 974f58fb8e | |
Adrien Marquès | ca2be1415d | |
Adrien Marquès | b15bb578ce | |
Adrien Marquès | 76cc2f5279 | |
Adrien Marquès | 8cfa2235d6 | |
Adrien Marquès | cb7f22e03d | |
Adrien Marquès | d3e8d48bc3 | |
Adrien Marquès | af09466013 | |
Adrien Marquès | 5504e4b3ec | |
Adrien Marquès | 2f9534a3b0 | |
Adrien Marquès | 49cf06d5d8 | |
Adrien Marquès | af3ffa7d6a | |
Adrien Marquès | dac9aa4298 | |
Adrien Marquès | 54705b7472 | |
Adrien Marquès | 5f3aa5967d | |
Adrien Marquès | eef94ff998 | |
Adrien Marquès | 97cf19d7b4 | |
Adrien Marquès | d57f60c710 | |
Adrien Marquès | ad6de97979 | |
Adrien Marquès | 2ee48560b6 | |
Adrien Marquès | a15a5c1f7a | |
Adrien Marquès | 5fe983c486 | |
Adrien Marquès | 3017cc5ba9 | |
Adrien Marquès | 9c3166397f | |
Adrien Marquès | e3adbf48ca | |
Adrien Marquès | 0e6dfbe580 | |
Adrien Marquès | d6c22b5ff0 | |
Adrien Marquès | 9a5b0dd6e3 | |
Adrien Marquès | 149ec9a9a0 | |
Adrien Marquès | 3a258400c0 | |
Adrien Marquès | 9475fe4526 | |
Adrien Marquès | 3606f9984d | |
Adrien Marquès | 7b812c6648 | |
Adrien Marquès | dc34d9a81a | |
Adrien Marquès | cdbe4cceac | |
Adrien Marquès | 03d5e87c37 | |
Adrien Marquès | c7aa87c660 | |
Adrien Marquès | 0f62fc25a0 | |
Adrien Marquès | 8c539370aa | |
Adrien Marquès | acd0e73438 | |
Adrien Marquès | b38a9a8111 | |
Adrien Marquès | 93b31b9718 | |
Adrien Marquès | 12417f7f1c | |
Adrien Marquès | e7f10723a6 | |
Adrien Marquès | c32b038da2 | |
Adrien Marquès | 1b4922693b | |
Adrien Marquès | 4e0d669029 | |
Adrien Marquès | 2c1b9cf5ff | |
Adrien Marquès | d1ab4fefb0 | |
Adrien Marquès | 32aff3e07f | |
Adrien Marquès | 6a144a9a93 | |
Adrien Marquès | 511070196b | |
Adrien Marquès | e12c52b88f | |
Adrien Marquès | 003fe4d2e7 | |
Adrien Marquès | a6f5083f0d |
Binary file not shown.
Before Width: | Height: | Size: 63 KiB |
110
README.md
110
README.md
|
@ -10,9 +10,9 @@
|
||||||
**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* **web framework** written in Go that allows you to create a fully featured REST API.
|
||||||
|
|
||||||
The whole management is done for you from a configuration file describing your API, you're left with implementing :
|
The whole management is done for you from a configuration file describing your API, you're left with implementing :
|
||||||
- controllers
|
- handlers
|
||||||
- optionnally middle-wares (_e.g. authentication, csrf_)
|
- optionnally middle-wares (_e.g. authentication, csrf_)
|
||||||
- and optionnally type checkers to check input parameters
|
- and optionnally your custom type checkers to check input parameters
|
||||||
|
|
||||||
|
|
||||||
The aicra server fulfills the `net/http` [Server interface](https://golang.org/pkg/net/http/#Server).
|
The aicra server fulfills the `net/http` [Server interface](https://golang.org/pkg/net/http/#Server).
|
||||||
|
@ -55,31 +55,34 @@ The library should now be available as `git.xdrm.io/go/aicra` in your imports.
|
||||||
|
|
||||||
#### 1) Main executable
|
#### 1) Main executable
|
||||||
|
|
||||||
The main executable will declare and run the aicra server, it might look quite like the code below.
|
Your main executable will declare and run the aicra server, it might look quite like the code below.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra"
|
"git.xdrm.io/go/aicra"
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
// 1. build server
|
// 1. select your datatypes (builtin, custom)
|
||||||
server, err := aicra.New("path/to/your/api/definition.json");
|
var dtypes []datatype.T
|
||||||
if err != nil {
|
dtypes = append(dtypes, builtin.AnyDataType{})
|
||||||
log.Fatalf("Cannot build the aicra server: %v\n", err)
|
dtypes = append(dtypes, builtin.BoolDataType{})
|
||||||
}
|
dtypes = append(dtypes, builtin.UintDataType{})
|
||||||
|
dtypes = append(dtypes, builtin.StringDataType{})
|
||||||
|
|
||||||
// 2. add type checkers
|
// 2. create the server from the configuration file
|
||||||
server.Checkers.Add( builtin.NewAny() );
|
server, err := aicra.New("path/to/your/api/definition.json", dtypes...)
|
||||||
server.Checkers.Add( builtin.NewString() );
|
if err != nil {
|
||||||
server.Checkers.Add( builtin.NewFloat64() );
|
log.Fatalf("cannot built aicra server: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. bind your implementations
|
// 3. bind your implementations
|
||||||
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){
|
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){
|
||||||
|
@ -87,8 +90,14 @@ func main() {
|
||||||
res.SetError(api.ErrorSuccess());
|
res.SetError(api.ErrorSuccess());
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 4. extract to http server
|
||||||
|
httpServer, err := server.ToHTTPServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot get to http server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 4. launch server
|
// 4. launch server
|
||||||
log.Fatal( http.ListenAndServe("localhost:8181", server) )
|
log.Fatal( http.ListenAndServe("localhost:8080", server) )
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -96,33 +105,24 @@ func main() {
|
||||||
|
|
||||||
#### 2) API Configuration
|
#### 2) API Configuration
|
||||||
|
|
||||||
The whole project behavior is described inside a json file (_e.g. usually api.json_) file. 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 :
|
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 :
|
||||||
|
|
||||||
- resource 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 for clients to use which method)
|
- scope permissions (list of permissions needed by clients)
|
||||||
- input policy :
|
- input policy :
|
||||||
- type of argument (_i.e. for type checkers_)
|
- type of argument (_i.e. for data types_)
|
||||||
- required/optional
|
- required/optional
|
||||||
- default value
|
|
||||||
- variable renaming
|
- variable renaming
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###### Definition
|
###### Definition
|
||||||
|
|
||||||
At the root of the json file are available 5 field names :
|
The root of the json file must be an array containing your requests definitions.
|
||||||
|
|
||||||
1. `GET` - to define what to do when receiving a request with a GET HTTP method at the root URI
|
For each, you will have to create fields described in the table above.
|
||||||
2. `POST` - to define what to do when receiving a request with a POST HTTP method at the root URI
|
|
||||||
3. `PUT` - to define what to do when receiving a request with a PUT HTTP method at the root URI
|
|
||||||
4. `DELETE` - to define what to do when receiving a request with a DELETE HTTP method at the root URI
|
|
||||||
5. `/` - to define children URIs ; each will have the same available fields
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
For each method you will have to create fields described in the table above.
|
|
||||||
|
|
||||||
| field path | description | example |
|
| field path | description | example |
|
||||||
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||||
|
@ -138,7 +138,7 @@ For each method you will have to create fields described in the table above.
|
||||||
|
|
||||||
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** - Slash-separated strings right after the resource URI. For instance, if your controller is bound to the `/user` URI, you can use the *URI slot* right after to send the user ID ; Now a client can send requests to the URI `/user/:id` where `:id` is a number sent by the client. This kind of input cannot be extracted by name, but rather by index in the URL (_begins at 0_).
|
- **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.
|
||||||
- **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.
|
||||||
|
@ -150,38 +150,38 @@ Input arguments defines what data from the HTTP request the method needs. Aicra
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
> Variable names must be <u>prefixed</u> when requesting **URI** or **Query** input types.
|
> Variable names from **URI** or **Query** must be named accordingly :
|
||||||
>
|
>
|
||||||
> - The first **URI** data has to be named `URL#0`, the second one `URL#1` and so on...
|
> - the **URI** variable `{id}` from your request route must be named `{id}`.
|
||||||
> - The variable named `somevar` in the **Query** has to be named `GET@somvar` in the configuration.
|
> - the variable `somevar` in the **Query** has to be names `GET@somevar`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
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 (else the controller will not be run). It is renamed `uri-param`, this new name will be sent to the controller.
|
- 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 `int` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `get-param`.
|
- 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 controller with the name `multipart-var`.
|
- 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
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/article/{id}",
|
||||||
|
"scope": [["author"]],
|
||||||
|
"info": "updates an article",
|
||||||
"in": {
|
"in": {
|
||||||
// arg 1
|
"{id}": { "info": "article id", "type": "int", "name": "article_id" },
|
||||||
"URL#0": {
|
"GET@title": { "info": "new article title", "type": "?string", "name": "title" },
|
||||||
"info": "some integer in the URI",
|
"content": { "info": "new article content", "type": "string" }
|
||||||
"type": "int",
|
|
||||||
"name": "uri-param"
|
|
||||||
},
|
},
|
||||||
// arg 2
|
"out": {
|
||||||
"GET@get-var": {
|
"id": { "info": "updated article id", "type": "uint" },
|
||||||
"info": "some Query OPTIONAL variable",
|
"title": { "info": "updated article title", "type": "string" },
|
||||||
"type": "?int",
|
"content": { "info": "updated article content", "type": "string" }
|
||||||
"name": "get-param"
|
|
||||||
},
|
|
||||||
// arg 3
|
|
||||||
"multipart-var": { /* ... */ }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ In this example we want 3 arguments :
|
||||||
|
|
||||||
- [x] human-readable json configuration
|
- [x] human-readable json configuration
|
||||||
- [x] nested routes (*i.e. `/user/:id:` and `/user/post/:id:`*)
|
- [x] nested routes (*i.e. `/user/:id:` and `/user/post/:id:`*)
|
||||||
- [ ] nested URL arguments (*i.e. `/user/:id:` and `/user/:id:/post/:id:`*)
|
- [x] nested URL arguments (*i.e. `/user/:id:` and `/user/:id:/post/:id:`*)
|
||||||
- [x] useful http methods: GET, POST, PUT, DELETE
|
- [x] useful http methods: GET, POST, PUT, DELETE
|
||||||
- [x] manage URL, query and body arguments:
|
- [x] manage URL, query and body arguments:
|
||||||
- [x] multipart/form-data (variables and file uploads)
|
- [x] multipart/form-data (variables and file uploads)
|
||||||
|
@ -211,5 +211,5 @@ In this example we want 3 arguments :
|
||||||
- [x] generic controllers implementation (shared objects)
|
- [x] generic controllers implementation (shared objects)
|
||||||
- [x] response interface
|
- [x] response interface
|
||||||
- [x] log bound resources when building the aicra server
|
- [x] log bound resources when building the aicra server
|
||||||
- [ ] fail on check for unimplemented resources at server boot.
|
- [x] fail on check for unimplemented resources at server boot.
|
||||||
- [ ] fail on check for unavailable types in api.json at server boot.
|
- [x] fail on check for unavailable types in api.json at server boot.
|
||||||
|
|
|
@ -1,78 +1,94 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrorSuccess represents a generic successful service execution
|
|
||||||
ErrorSuccess = func() Error { return Error{0, "all right", nil} }
|
|
||||||
|
|
||||||
// ErrorFailure is the most generic error
|
|
||||||
ErrorFailure = func() Error { return Error{1, "it failed", nil} }
|
|
||||||
|
|
||||||
// ErrorUnknown represents any error which cause is unknown.
|
// 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
|
||||||
// has to be used the less possible
|
// has to be used the less possible
|
||||||
ErrorUnknown = func() Error { return Error{-1, "", nil} }
|
ErrorUnknown Error = -1
|
||||||
|
|
||||||
|
// ErrorSuccess represents a generic successful service execution
|
||||||
|
ErrorSuccess Error = 0
|
||||||
|
|
||||||
|
// ErrorFailure is the most generic error
|
||||||
|
ErrorFailure Error = 1
|
||||||
|
|
||||||
// ErrorNoMatchFound has to be set when trying to fetch data and there is no result
|
// ErrorNoMatchFound has to be set when trying to fetch data and there is no result
|
||||||
ErrorNoMatchFound = func() Error { return Error{2, "no resource found", nil} }
|
ErrorNoMatchFound Error = 2
|
||||||
|
|
||||||
// ErrorAlreadyExists has to be set when trying to insert data, but identifiers or
|
// ErrorAlreadyExists has to be set when trying to insert data, but identifiers or
|
||||||
// unique fields already exists
|
// unique fields already exists
|
||||||
ErrorAlreadyExists = func() Error { return Error{3, "resource already exists", nil} }
|
ErrorAlreadyExists Error = 3
|
||||||
|
|
||||||
// ErrorConfig has to be set when there is a configuration error
|
// ErrorConfig has to be set when there is a configuration error
|
||||||
ErrorConfig = func() Error { return Error{4, "configuration error", nil} }
|
ErrorConfig Error = 4
|
||||||
|
|
||||||
// ErrorUpload has to be set when a file upload failed
|
// ErrorUpload has to be set when a file upload failed
|
||||||
ErrorUpload = func() Error { return Error{100, "upload failed", nil} }
|
ErrorUpload Error = 100
|
||||||
|
|
||||||
// ErrorDownload has to be set when a file download failed
|
// ErrorDownload has to be set when a file download failed
|
||||||
ErrorDownload = func() Error { return Error{101, "download failed", nil} }
|
ErrorDownload Error = 101
|
||||||
|
|
||||||
// MissingDownloadHeaders has to be set when the implementation
|
// MissingDownloadHeaders has to be set when the implementation
|
||||||
// of a service of type 'download' (which returns a file instead of
|
// of a service of type 'download' (which returns a file instead of
|
||||||
// a set or output fields) is missing its HEADER field
|
// a set or output fields) is missing its HEADER field
|
||||||
MissingDownloadHeaders = func() Error { return Error{102, "download headers are missing", nil} }
|
MissingDownloadHeaders Error = 102
|
||||||
|
|
||||||
// ErrorMissingDownloadBody has to be set when the implementation
|
// ErrorMissingDownloadBody has to be set when the implementation
|
||||||
// of a service of type 'download' (which returns a file instead of
|
// of a service of type 'download' (which returns a file instead of
|
||||||
// a set or output fields) is missing its BODY field
|
// a set or output fields) is missing its BODY field
|
||||||
ErrorMissingDownloadBody = func() Error { return Error{103, "download body is missing", nil} }
|
ErrorMissingDownloadBody Error = 103
|
||||||
|
|
||||||
// ErrorUnknownService is set when there is no service matching
|
// ErrorUnknownService is set when there is no service matching
|
||||||
// the http request URI.
|
// the http request URI.
|
||||||
ErrorUnknownService = func() Error { return Error{200, "unknown service", nil} }
|
ErrorUnknownService Error = 200
|
||||||
|
|
||||||
// ErrorUnknownMethod is set when there is no method matching the
|
|
||||||
// request's http method
|
|
||||||
ErrorUnknownMethod = func() Error { return Error{201, "unknown method", nil} }
|
|
||||||
|
|
||||||
// ErrorUncallableService is set when there the requested service's
|
// ErrorUncallableService is set when there the requested service's
|
||||||
// implementation (plugin file) is not found/callable
|
// implementation (plugin file) is not found/callable
|
||||||
ErrorUncallableService = func() Error { return Error{202, "uncallable service", nil} }
|
ErrorUncallableService Error = 202
|
||||||
|
|
||||||
// ErrorUncallableMethod is set when there the requested service's
|
// ErrorNotImplemented is set when a handler is not implemented yet
|
||||||
// implementation does not features the requested method
|
ErrorNotImplemented Error = 203
|
||||||
ErrorUncallableMethod = func() Error { return Error{203, "uncallable method", nil} }
|
|
||||||
|
|
||||||
// ErrorPermission is set when there is a permission error by default
|
// ErrorPermission is set when there is a permission error by default
|
||||||
// the api returns a permission error when the current scope (built
|
// the api returns a permission error when the current scope (built
|
||||||
// by middlewares) does not match the scope required in the config.
|
// by middlewares) does not match the scope required in the config.
|
||||||
// You can add your own permission policy and use this error
|
// You can add your own permission policy and use this error
|
||||||
ErrorPermission = func() Error { return Error{300, "permission error", nil} }
|
ErrorPermission Error = 300
|
||||||
|
|
||||||
// ErrorToken has to be set (usually in authentication middleware) to tell
|
// ErrorToken has to be set (usually in authentication middleware) to tell
|
||||||
// the user that this authentication token is expired or invalid
|
// the user that this authentication token is expired or invalid
|
||||||
ErrorToken = func() Error { return Error{301, "token error", nil} }
|
ErrorToken Error = 301
|
||||||
|
|
||||||
// ErrorMissingParam is set when a *required* parameter is missing from the
|
// ErrorMissingParam is set when a *required* parameter is missing from the
|
||||||
// http request
|
// http request
|
||||||
ErrorMissingParam = func() Error { return Error{400, "missing parameter", nil} }
|
ErrorMissingParam Error = 400
|
||||||
|
|
||||||
// ErrorInvalidParam is set when a given parameter fails its type check as
|
// ErrorInvalidParam is set when a given parameter fails its type check as
|
||||||
// defined in the config file.
|
// defined in the config file.
|
||||||
ErrorInvalidParam = func() Error { return Error{401, "invalid parameter", nil} }
|
ErrorInvalidParam Error = 401
|
||||||
|
|
||||||
// ErrorInvalidDefaultParam is set when an optional parameter's default value
|
// ErrorInvalidDefaultParam is set when an optional parameter's default value
|
||||||
// does not match its type.
|
// does not match its type.
|
||||||
ErrorInvalidDefaultParam = func() Error { return Error{402, "invalid default param", nil} }
|
ErrorInvalidDefaultParam Error = 402
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errorReasons = map[Error]string{
|
||||||
|
ErrorUnknown: "unknown error",
|
||||||
|
ErrorSuccess: "all right",
|
||||||
|
ErrorFailure: "it failed",
|
||||||
|
ErrorNoMatchFound: "resource not found",
|
||||||
|
ErrorAlreadyExists: "already exists",
|
||||||
|
ErrorConfig: "configuration error",
|
||||||
|
ErrorUpload: "upload failed",
|
||||||
|
ErrorDownload: "download failed",
|
||||||
|
MissingDownloadHeaders: "download headers are missing",
|
||||||
|
ErrorMissingDownloadBody: "download body is missing",
|
||||||
|
ErrorUnknownService: "unknown service",
|
||||||
|
ErrorUncallableService: "uncallable service",
|
||||||
|
ErrorNotImplemented: "not implemented",
|
||||||
|
ErrorPermission: "permission error",
|
||||||
|
ErrorToken: "token error",
|
||||||
|
ErrorMissingParam: "missing parameter",
|
||||||
|
ErrorInvalidParam: "invalid parameter",
|
||||||
|
ErrorInvalidDefaultParam: "invalid default param",
|
||||||
|
}
|
||||||
|
|
54
api/error.go
54
api/error.go
|
@ -1,40 +1,42 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents an http response error following the api format.
|
// Error represents an http response error following the api format.
|
||||||
// These are used by the services to set the *execution status*
|
// These are used by the services to set the *execution status*
|
||||||
// directly into the response as JSON alongside response output fields.
|
// directly into the response as JSON alongside response output fields.
|
||||||
type Error struct {
|
type Error int
|
||||||
|
|
||||||
|
// Error implements the error interface
|
||||||
|
func (e Error) Error() string {
|
||||||
|
// use unknown error if no reason
|
||||||
|
reason, ok := errorReasons[e]
|
||||||
|
if !ok {
|
||||||
|
return ErrorUnknown.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%d] %s", e, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements encoding/json.Marshaler interface
|
||||||
|
func (e Error) MarshalJSON() ([]byte, error) {
|
||||||
|
// use unknown error if no reason
|
||||||
|
reason, ok := errorReasons[e]
|
||||||
|
if !ok {
|
||||||
|
return ErrorUnknown.MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// format to proper struct
|
||||||
|
formatted := struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Arguments []interface{} `json:"arguments"`
|
}{
|
||||||
|
Code: int(e),
|
||||||
|
Reason: reason,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetArguments set one or multiple arguments to the error
|
return json.Marshal(formatted)
|
||||||
// to be displayed back to API caller
|
|
||||||
func (e *Error) SetArguments(arg0 interface{}, args ...interface{}) {
|
|
||||||
|
|
||||||
// 1. clear arguments */
|
|
||||||
e.Arguments = make([]interface{}, 0)
|
|
||||||
|
|
||||||
// 2. add arg[0]
|
|
||||||
e.Arguments = append(e.Arguments, arg0)
|
|
||||||
|
|
||||||
// 3. add optional other arguments
|
|
||||||
for _, arg := range args {
|
|
||||||
e.Arguments = append(e.Arguments, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements 'error'
|
|
||||||
func (e Error) Error() string {
|
|
||||||
if e.Arguments == nil || len(e.Arguments) < 1 {
|
|
||||||
return fmt.Sprintf("[%d] %s", e.Code, e.Reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[%d] %s (%v)", e.Code, e.Reason, e.Arguments)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandlerFunc manages an API request
|
|
||||||
type HandlerFunc func(Request, *Response)
|
|
||||||
|
|
||||||
// Handler is an API handler ready to be bound
|
|
||||||
type Handler struct {
|
|
||||||
path string
|
|
||||||
method string
|
|
||||||
handle HandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler builds a handler from its http method and path
|
|
||||||
func NewHandler(method, path string, handlerFunc HandlerFunc) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
path: path,
|
|
||||||
method: strings.ToUpper(method),
|
|
||||||
handle: handlerFunc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle fires a handler
|
|
||||||
func (h *Handler) Handle(req Request, res *Response) {
|
|
||||||
h.handle(req, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMethod returns the handler's HTTP method
|
|
||||||
func (h *Handler) GetMethod() string {
|
|
||||||
return h.method
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPath returns the handler's path
|
|
||||||
func (h *Handler) GetPath() string {
|
|
||||||
return h.path
|
|
||||||
}
|
|
|
@ -2,15 +2,21 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
// ErrReqParamNotFound is thrown when a request parameter is not found
|
||||||
const ErrReqParamNotFound = cerr.Error("request parameter not found")
|
const ErrReqParamNotFound = cerr("request parameter not found")
|
||||||
|
|
||||||
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
||||||
const ErrReqParamNotType = cerr.Error("request parameter does not fulfills type")
|
const ErrReqParamNotType = cerr("request parameter does not fulfills type")
|
||||||
|
|
||||||
// RequestParam defines input parameters of an api request
|
// RequestParam defines input parameters of an api request
|
||||||
type RequestParam map[string]interface{}
|
type RequestParam map[string]interface{}
|
||||||
|
|
|
@ -16,31 +16,22 @@ type Response struct {
|
||||||
err Error
|
err Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResponse creates an empty response. An optional error can be passed as its first argument.
|
// EmptyResponse creates an empty response.
|
||||||
func NewResponse(errors ...Error) *Response {
|
func EmptyResponse() *Response {
|
||||||
res := &Response{
|
return &Response{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Data: make(ResponseData),
|
Data: make(ResponseData),
|
||||||
err: ErrorFailure(),
|
err: ErrorFailure,
|
||||||
Headers: make(http.Header),
|
Headers: make(http.Header),
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional error
|
|
||||||
if len(errors) == 1 {
|
|
||||||
res.err = errors[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithError sets the error from a base error with error arguments.
|
||||||
|
func (res *Response) WithError(err Error) *Response {
|
||||||
|
res.err = err
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetError sets the error from a base error with error arguments.
|
|
||||||
func (res *Response) SetError(baseError Error, arguments ...interface{}) {
|
|
||||||
if len(arguments) > 0 {
|
|
||||||
baseError.SetArguments(arguments[0], arguments[1:]...)
|
|
||||||
}
|
|
||||||
res.err = baseError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the error interface and dispatches to internal error.
|
// 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()
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnyDataType is what its name tells
|
||||||
|
type AnyDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (AnyDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(interface{}(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (AnyDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "any" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,26 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAny_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewAny())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.Any:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.Any{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAny_AvailableTypes(t *testing.T) {
|
func TestAny_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewAny()
|
dt := builtin.AnyDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,9 +26,9 @@ func TestAny_AvailableTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
}
|
}
|
||||||
|
@ -60,8 +47,8 @@ func TestAny_AlwaysTrue(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "any"
|
const typeName = "any"
|
||||||
|
|
||||||
checker := builtin.NewAny().Checker(typeName)
|
validator := builtin.AnyDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -76,7 +63,7 @@ func TestAny_AlwaysTrue(t *testing.T) {
|
||||||
|
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if !checker(value) {
|
if _, isValid := validator(value); !isValid {
|
||||||
t.Errorf("expect value to be valid")
|
t.Errorf("expect value to be valid")
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoolDataType is what its name tells
|
||||||
|
type BoolDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (BoolDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (BoolDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "bool" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
switch cast := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return cast, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
strVal := string(cast)
|
||||||
|
return strVal == "true", strVal == "true" || strVal == "false"
|
||||||
|
case []byte:
|
||||||
|
strVal := string(cast)
|
||||||
|
return strVal == "true", strVal == "true" || strVal == "false"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,26 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBool_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewBool())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.Bool:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.Bool{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBool_AvailableTypes(t *testing.T) {
|
func TestBool_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewBool()
|
dt := builtin.BoolDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,8 +26,8 @@ func TestBool_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -62,8 +49,8 @@ func TestBool_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "bool"
|
const typeName = "bool"
|
||||||
|
|
||||||
checker := builtin.NewBool().Checker(typeName)
|
validator := builtin.BoolDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -98,7 +85,7 @@ func TestBool_Values(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -0,0 +1,53 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FloatDataType is what its name tells
|
||||||
|
type FloatDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (FloatDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(float64(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (FloatDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "float64" && typeName != "float" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
switch cast := value.(type) {
|
||||||
|
|
||||||
|
case int:
|
||||||
|
return float64(cast), true
|
||||||
|
|
||||||
|
case uint:
|
||||||
|
return float64(cast), true
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
return cast, true
|
||||||
|
|
||||||
|
// serialized string -> try to convert to float
|
||||||
|
case []byte:
|
||||||
|
num := json.Number(cast)
|
||||||
|
floatVal, err := num.Float64()
|
||||||
|
return floatVal, err == nil
|
||||||
|
|
||||||
|
case string:
|
||||||
|
num := json.Number(cast)
|
||||||
|
floatVal, err := num.Float64()
|
||||||
|
return floatVal, err == nil
|
||||||
|
|
||||||
|
// unknown type
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,26 +5,13 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFloat64_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewFloat64())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.Float64:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.Float64{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFloat64_AvailableTypes(t *testing.T) {
|
func TestFloat64_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewFloat64()
|
dt := builtin.FloatDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -46,8 +33,8 @@ func TestFloat64_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -69,8 +56,8 @@ func TestFloat64_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "float"
|
const typeName = "float"
|
||||||
|
|
||||||
checker := builtin.NewFloat64().Checker(typeName)
|
validator := builtin.FloatDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -110,7 +97,7 @@ func TestFloat64_Values(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -0,0 +1,58 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IntDataType is what its name tells
|
||||||
|
type IntDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (IntDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(int(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (IntDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "int" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
switch cast := value.(type) {
|
||||||
|
|
||||||
|
case int:
|
||||||
|
return cast, true
|
||||||
|
|
||||||
|
case uint:
|
||||||
|
overflows := cast > math.MaxInt64
|
||||||
|
return int(cast), !overflows
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
intVal := int(cast)
|
||||||
|
overflows := cast < float64(math.MinInt64) || cast > float64(math.MaxInt64)
|
||||||
|
return intVal, cast == float64(intVal) && !overflows
|
||||||
|
|
||||||
|
// serialized string -> try to convert to float
|
||||||
|
case string:
|
||||||
|
num := json.Number(cast)
|
||||||
|
intVal, err := num.Int64()
|
||||||
|
return int(intVal), err == nil
|
||||||
|
// serialized string -> try to convert to float
|
||||||
|
|
||||||
|
case []byte:
|
||||||
|
num := json.Number(cast)
|
||||||
|
intVal, err := num.Int64()
|
||||||
|
return int(intVal), err == nil
|
||||||
|
|
||||||
|
// unknown type
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,26 +5,13 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInt_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewInt())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.Int:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.Int{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInt_AvailableTypes(t *testing.T) {
|
func TestInt_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewInt()
|
dt := builtin.IntDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,8 +27,8 @@ func TestInt_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -63,8 +50,8 @@ func TestInt_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "int"
|
const typeName = "int"
|
||||||
|
|
||||||
checker := builtin.NewInt().Checker(typeName)
|
validator := builtin.IntDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -110,7 +97,7 @@ func TestInt_Values(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,31 +1,33 @@
|
||||||
package builtin
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
||||||
var variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
var variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
||||||
|
|
||||||
// String checks if a value is a string
|
// StringDataType is what its name tells
|
||||||
type String struct{}
|
type StringDataType struct{}
|
||||||
|
|
||||||
// NewString returns a bare string type checker
|
// Type returns the type of data
|
||||||
func NewString() *String {
|
func (StringDataType) Type() reflect.Type {
|
||||||
return &String{}
|
return reflect.TypeOf(string(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checker returns the checker function. Availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`.
|
// Build returns the validator.
|
||||||
func (s String) Checker(typeName string) typecheck.CheckerFunc {
|
// availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`.
|
||||||
isSimpleString := typeName == "string"
|
func (s StringDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
simple := typeName == "string"
|
||||||
fixedLengthMatches := fixedLengthRegex.FindStringSubmatch(typeName)
|
fixedLengthMatches := fixedLengthRegex.FindStringSubmatch(typeName)
|
||||||
variableLengthMatches := variableLengthRegex.FindStringSubmatch(typeName)
|
variableLengthMatches := variableLengthRegex.FindStringSubmatch(typeName)
|
||||||
|
|
||||||
// nothing if type not handled
|
// nothing if type not handled
|
||||||
if !isSimpleString && fixedLengthMatches == nil && variableLengthMatches == nil {
|
if !simple && fixedLengthMatches == nil && variableLengthMatches == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +55,10 @@ func (s String) Checker(typeName string) typecheck.CheckerFunc {
|
||||||
max = exMax
|
max = exMax
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(value interface{}) bool {
|
return func(value interface{}) (interface{}, bool) {
|
||||||
// preprocessing error
|
// preprocessing error
|
||||||
if mustFail {
|
if mustFail {
|
||||||
return false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check type
|
// check type
|
||||||
|
@ -68,21 +70,21 @@ func (s String) Checker(typeName string) typecheck.CheckerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isString {
|
if !isString {
|
||||||
return false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSimpleString {
|
if simple {
|
||||||
return true
|
return strValue, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// check length against previously extracted length
|
// check length against previously extracted length
|
||||||
l := len(strValue)
|
l := len(strValue)
|
||||||
return l >= min && l <= max
|
return strValue, l >= min && l <= max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFixedLength returns the fixed length from regex matches and a success state.
|
// getFixedLength returns the fixed length from regex matches and a success state.
|
||||||
func (String) getFixedLength(regexMatches []string) (int, bool) {
|
func (StringDataType) getFixedLength(regexMatches []string) (int, bool) {
|
||||||
// incoherence error
|
// incoherence error
|
||||||
if regexMatches == nil || len(regexMatches) < 2 {
|
if regexMatches == nil || len(regexMatches) < 2 {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
@ -98,7 +100,7 @@ func (String) getFixedLength(regexMatches []string) (int, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVariableLength returns the length min and max from regex matches and a success state.
|
// getVariableLength returns the length min and max from regex matches and a success state.
|
||||||
func (String) getVariableLength(regexMatches []string) (int, int, bool) {
|
func (StringDataType) getVariableLength(regexMatches []string) (int, int, bool) {
|
||||||
// incoherence error
|
// incoherence error
|
||||||
if regexMatches == nil || len(regexMatches) < 3 {
|
if regexMatches == nil || len(regexMatches) < 3 {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
|
@ -4,26 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestString_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewString())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.String:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.String{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString_AvailableTypes(t *testing.T) {
|
func TestString_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewString()
|
dt := builtin.StringDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -66,9 +53,9 @@ func TestString_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
}
|
}
|
||||||
|
@ -88,8 +75,8 @@ func TestString_AnyLength(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "string"
|
const typeName = "string"
|
||||||
|
|
||||||
checker := builtin.NewString().Checker(typeName)
|
validator := builtin.StringDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -107,7 +94,7 @@ func TestString_AnyLength(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -146,14 +133,14 @@ func TestString_FixedLength(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
checker := builtin.NewString().Checker(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -207,14 +194,14 @@ func TestString_VariableLength(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
checker := builtin.NewString().Checker(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -0,0 +1,64 @@
|
||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UintDataType is what its name tells
|
||||||
|
type UintDataType struct{}
|
||||||
|
|
||||||
|
// Type returns the type of data
|
||||||
|
func (UintDataType) Type() reflect.Type {
|
||||||
|
return reflect.TypeOf(uint(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the validator
|
||||||
|
func (UintDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
|
// nothing if type not handled
|
||||||
|
if typeName != "uint" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(value interface{}) (interface{}, bool) {
|
||||||
|
switch cast := value.(type) {
|
||||||
|
|
||||||
|
case int:
|
||||||
|
return uint(cast), cast >= 0
|
||||||
|
|
||||||
|
case uint:
|
||||||
|
return cast, true
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
uintVal := uint(cast)
|
||||||
|
overflows := cast < 0 || cast > math.MaxUint64
|
||||||
|
return uintVal, cast == float64(uintVal) && !overflows
|
||||||
|
|
||||||
|
// serialized string -> try to convert to float
|
||||||
|
case string:
|
||||||
|
num := json.Number(cast)
|
||||||
|
floatVal, err := num.Float64()
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
overflows := floatVal < 0 || floatVal > math.MaxUint64
|
||||||
|
return uint(floatVal), !overflows
|
||||||
|
|
||||||
|
case []byte:
|
||||||
|
num := json.Number(cast)
|
||||||
|
floatVal, err := num.Float64()
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
overflows := floatVal < 0 || floatVal > math.MaxUint64
|
||||||
|
return uint(floatVal), !overflows
|
||||||
|
|
||||||
|
// unknown type
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,26 +5,13 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUint_New(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inst := interface{}(builtin.NewUint())
|
|
||||||
|
|
||||||
switch cast := inst.(type) {
|
|
||||||
case *builtin.Uint:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Errorf("expect %T ; got %T", &builtin.Uint{}, cast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUint_AvailableTypes(t *testing.T) {
|
func TestUint_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
inst := builtin.NewUint()
|
dt := builtin.UintDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,8 +27,8 @@ func TestUint_AvailableTypes(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.Type, func(t *testing.T) {
|
t.Run(test.Type, func(t *testing.T) {
|
||||||
checker := inst.Checker(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
t.Errorf("expect %q to be handled", test.Type)
|
t.Errorf("expect %q to be handled", test.Type)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -63,8 +50,8 @@ func TestUint_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "uint"
|
const typeName = "uint"
|
||||||
|
|
||||||
checker := builtin.NewUint().Checker(typeName)
|
validator := builtin.UintDataType{}.Build(typeName)
|
||||||
if checker == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
@ -110,7 +97,7 @@ func TestUint_Values(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
if checker(test.Value) {
|
if _, isValid := validator(test.Value); isValid {
|
||||||
if !test.Valid {
|
if !test.Valid {
|
||||||
t.Errorf("expect value to be invalid")
|
t.Errorf("expect value to be invalid")
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrHandlerNotFunc - handler is not a func
|
||||||
|
const ErrHandlerNotFunc = cerr("handler must be a func")
|
||||||
|
|
||||||
|
// ErrNoServiceForHandler - no service matching this handler
|
||||||
|
const ErrNoServiceForHandler = cerr("no service found for this handler")
|
||||||
|
|
||||||
|
// ErrMissingHandlerArgumentParam - missing params arguments for handler
|
||||||
|
const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct")
|
||||||
|
|
||||||
|
// ErrMissingHandlerOutput - missing output for handler
|
||||||
|
const ErrMissingHandlerOutput = cerr("handler must have at least 1 output")
|
||||||
|
|
||||||
|
// ErrMissingHandlerOutputError - missing error output for handler
|
||||||
|
const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error")
|
||||||
|
|
||||||
|
// ErrMissingRequestArgument - missing request argument for handler
|
||||||
|
const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request")
|
||||||
|
|
||||||
|
// ErrMissingParamArgument - missing parameters argument for handler
|
||||||
|
const ErrMissingParamArgument = cerr("handler second argument must be a struct")
|
||||||
|
|
||||||
|
// ErrMissingParamOutput - missing output argument for handler
|
||||||
|
const ErrMissingParamOutput = cerr("handler first output must be a *struct")
|
||||||
|
|
||||||
|
// ErrMissingParamFromConfig - missing a parameter in handler struct
|
||||||
|
const ErrMissingParamFromConfig = cerr("missing a parameter from configuration")
|
||||||
|
|
||||||
|
// ErrMissingOutputFromConfig - missing a parameter in handler struct
|
||||||
|
const ErrMissingOutputFromConfig = cerr("missing a parameter from configuration")
|
||||||
|
|
||||||
|
// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
|
||||||
|
const ErrWrongParamTypeFromConfig = cerr("invalid struct field type")
|
||||||
|
|
||||||
|
// ErrWrongOutputTypeFromConfig - a configuration output type is invalid in the handler output struct
|
||||||
|
const ErrWrongOutputTypeFromConfig = cerr("invalid struct field type")
|
||||||
|
|
||||||
|
// ErrMissingHandlerErrorOutput - missing handler output error
|
||||||
|
const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error")
|
|
@ -0,0 +1,90 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/api"
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build a handler from a service configuration and a HandlerFn
|
||||||
|
//
|
||||||
|
// a HandlerFn must have as a signature : `func(api.Request, inputStruct) (outputStruct, api.Error)`
|
||||||
|
// - `inputStruct` is a struct{} containing a field for each service input (with valid reflect.Type)
|
||||||
|
// - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type)
|
||||||
|
//
|
||||||
|
// Special cases:
|
||||||
|
// - it there is no input, `inputStruct` can be omitted
|
||||||
|
// - it there is no output, `outputStruct` can be omitted
|
||||||
|
func Build(fn HandlerFn, service config.Service) (*Handler, error) {
|
||||||
|
h := &Handler{
|
||||||
|
spec: makeSpec(service),
|
||||||
|
fn: fn,
|
||||||
|
}
|
||||||
|
|
||||||
|
fnv := reflect.ValueOf(fn)
|
||||||
|
|
||||||
|
if fnv.Type().Kind() != reflect.Func {
|
||||||
|
return nil, ErrHandlerNotFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.spec.checkInput(fnv); err != nil {
|
||||||
|
return nil, fmt.Errorf("input: %w", err)
|
||||||
|
}
|
||||||
|
if err := h.spec.checkOutput(fnv); err != nil {
|
||||||
|
return nil, fmt.Errorf("output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle binds input @data into HandleFn and returns map output
|
||||||
|
func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Error) {
|
||||||
|
fnv := reflect.ValueOf(h.fn)
|
||||||
|
|
||||||
|
callArgs := []reflect.Value{}
|
||||||
|
|
||||||
|
// bind input data
|
||||||
|
if fnv.Type().NumIn() > 0 {
|
||||||
|
// create zero value struct
|
||||||
|
callStructPtr := reflect.New(fnv.Type().In(0))
|
||||||
|
callStruct := callStructPtr.Elem()
|
||||||
|
|
||||||
|
// set each field
|
||||||
|
for name := range h.spec.Input {
|
||||||
|
field := callStruct.FieldByName(name)
|
||||||
|
if !field.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// get value from @data
|
||||||
|
value, inData := data[name]
|
||||||
|
if !inData {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(value).Convert(field.Type()))
|
||||||
|
}
|
||||||
|
callArgs = append(callArgs, callStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the HandlerFn
|
||||||
|
output := fnv.Call(callArgs)
|
||||||
|
|
||||||
|
// no output OR pointer to output struct is nil
|
||||||
|
outdata := make(map[string]interface{})
|
||||||
|
if len(h.spec.Output) < 1 || output[0].IsNil() {
|
||||||
|
return outdata, api.Error(output[len(output)-1].Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract struct from pointer
|
||||||
|
returnStruct := output[0].Elem()
|
||||||
|
|
||||||
|
for name := range h.spec.Output {
|
||||||
|
field := returnStruct.FieldByName(name)
|
||||||
|
outdata[name] = field.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract api.Error
|
||||||
|
return outdata, api.Error(output[len(output)-1].Int())
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/api"
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// builds a spec from the configuration service
|
||||||
|
func makeSpec(service config.Service) spec {
|
||||||
|
spec := spec{
|
||||||
|
Input: make(map[string]reflect.Type),
|
||||||
|
Output: make(map[string]reflect.Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, param := range service.Input {
|
||||||
|
// make a pointer if optional
|
||||||
|
if param.Optional {
|
||||||
|
spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spec.Input[param.Rename] = param.ExtractType
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, param := range service.Output {
|
||||||
|
spec.Output[param.Rename] = param.ExtractType
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for HandlerFn input arguments
|
||||||
|
func (s spec) checkInput(fnv reflect.Value) error {
|
||||||
|
fnt := fnv.Type()
|
||||||
|
|
||||||
|
// no input -> ok
|
||||||
|
if len(s.Input) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fnt.NumIn() != 1 {
|
||||||
|
return ErrMissingHandlerArgumentParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// arg must be a struct
|
||||||
|
structArg := fnt.In(0)
|
||||||
|
if structArg.Kind() != reflect.Struct {
|
||||||
|
return ErrMissingParamArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for invlaid param
|
||||||
|
for name, ptype := range s.Input {
|
||||||
|
field, exists := structArg.FieldByName(name)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ptype.AssignableTo(field.Type) {
|
||||||
|
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for HandlerFn output arguments
|
||||||
|
func (s spec) checkOutput(fnv reflect.Value) error {
|
||||||
|
fnt := fnv.Type()
|
||||||
|
if fnt.NumOut() < 1 {
|
||||||
|
return ErrMissingHandlerOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// last output must be api.Error
|
||||||
|
errOutput := fnt.Out(fnt.NumOut() - 1)
|
||||||
|
if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) {
|
||||||
|
return ErrMissingHandlerErrorOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// no output -> ok
|
||||||
|
if len(s.Output) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fnt.NumOut() != 2 {
|
||||||
|
return ErrMissingParamOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if first output is not a pointer to struct
|
||||||
|
structOutputPtr := fnt.Out(0)
|
||||||
|
if structOutputPtr.Kind() != reflect.Ptr {
|
||||||
|
return ErrMissingParamOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
structOutput := structOutputPtr.Elem()
|
||||||
|
if structOutput.Kind() != reflect.Struct {
|
||||||
|
return ErrMissingParamOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on invalid output
|
||||||
|
for name, ptype := range s.Output {
|
||||||
|
field, exists := structOutput.FieldByName(name)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore types evalutating to nil
|
||||||
|
if ptype == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ptype.ConvertibleTo(field.Type) {
|
||||||
|
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongOutputTypeFromConfig, field.Type, ptype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dynamic
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// HandlerFn defines a dynamic handler function
|
||||||
|
type HandlerFn interface{}
|
||||||
|
|
||||||
|
// Handler represents a dynamic api handler
|
||||||
|
type Handler struct {
|
||||||
|
spec spec
|
||||||
|
fn HandlerFn
|
||||||
|
}
|
||||||
|
|
||||||
|
type spec struct {
|
||||||
|
Input map[string]reflect.Type
|
||||||
|
Output map[string]reflect.Type
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNoServiceForHandler - no service matching this handler
|
||||||
|
const ErrNoServiceForHandler = cerr("no service found for this handler")
|
||||||
|
|
||||||
|
// ErrNoHandlerForService - no handler matching this service
|
||||||
|
const ErrNoHandlerForService = cerr("no handler found for this service")
|
|
@ -0,0 +1,32 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/dynamic"
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
dynHandler *dynamic.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHandler builds a handler from its http method and path
|
||||||
|
// also it checks whether the function signature is valid
|
||||||
|
func createHandler(method, path string, service config.Service, fn dynamic.HandlerFn) (*handler, error) {
|
||||||
|
method = strings.ToUpper(method)
|
||||||
|
|
||||||
|
dynHandler, err := dynamic.Build(fn, service)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s '%s' handler: %w", method, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &handler{
|
||||||
|
Path: path,
|
||||||
|
Method: method,
|
||||||
|
dynHandler: dynHandler,
|
||||||
|
}, nil
|
||||||
|
}
|
171
http.go
171
http.go
|
@ -3,7 +3,6 @@ package aicra
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/api"
|
||||||
"git.xdrm.io/go/aicra/internal/reqdata"
|
"git.xdrm.io/go/aicra/internal/reqdata"
|
||||||
|
@ -13,103 +12,105 @@ import (
|
||||||
type httpServer Server
|
type httpServer Server
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and has to be called on each request
|
// ServeHTTP implements http.Handler and has to be called on each request
|
||||||
func (server httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||||
defer r.Body.Close()
|
defer req.Body.Close()
|
||||||
|
|
||||||
/* (1) create api.Request from http.Request
|
// 1. find a matching service in the config
|
||||||
---------------------------------------------------------*/
|
service := server.config.Find(req)
|
||||||
request, err := api.NewRequest(r)
|
if service == nil {
|
||||||
|
response := api.EmptyResponse().WithError(api.ErrorUnknownService)
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
logError(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. build input parameter receiver
|
||||||
|
dataset := reqdata.New(service)
|
||||||
|
|
||||||
|
// 3. extract URI data
|
||||||
|
err := dataset.ExtractURI(req)
|
||||||
|
if err != nil {
|
||||||
|
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
logError(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. extract query data
|
||||||
|
err = dataset.ExtractQuery(req)
|
||||||
|
if err != nil {
|
||||||
|
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
logError(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. extract form/json data
|
||||||
|
err = dataset.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
response := api.EmptyResponse().WithError(api.ErrorMissingParam)
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
logError(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. find a matching handler
|
||||||
|
var foundHandler *handler
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for _, handler := range server.handlers {
|
||||||
|
if handler.Method == service.Method && handler.Path == service.Pattern {
|
||||||
|
foundHandler = handler
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. fail if found no handler
|
||||||
|
if foundHandler == nil {
|
||||||
|
if found {
|
||||||
|
r := api.EmptyResponse().WithError(api.ErrorUncallableService)
|
||||||
|
r.ServeHTTP(res, req)
|
||||||
|
logError(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r := api.EmptyResponse().WithError(api.ErrorUnknownService)
|
||||||
|
r.ServeHTTP(res, req)
|
||||||
|
logError(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. build api.Request from http.Request
|
||||||
|
apireq, err := api.NewRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. find a matching service for this path in the config
|
// 9. feed request with scope & parameters
|
||||||
serviceConf, pathIndex := server.config.Browse(request.URI)
|
apireq.Scope = service.Scope
|
||||||
if serviceConf == nil {
|
apireq.Param = dataset.Data
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. extract the service path from request URI
|
// 10. execute
|
||||||
servicePath := strings.Join(request.URI[:pathIndex], "/")
|
returned, apiErr := foundHandler.dynHandler.Handle(dataset.Data)
|
||||||
if !strings.HasPrefix(servicePath, "/") {
|
response := api.EmptyResponse().WithError(apiErr)
|
||||||
servicePath = "/" + servicePath
|
for key, value := range returned {
|
||||||
}
|
|
||||||
|
|
||||||
// 4. find method configuration from http method */
|
// find original name from rename
|
||||||
var methodConf = serviceConf.Method(r.Method)
|
for name, param := range service.Output {
|
||||||
if methodConf == nil {
|
if param.Rename == key {
|
||||||
res := api.NewResponse(api.ErrorUnknownMethod())
|
response.SetData(name, value)
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
logError(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. parse data from the request (uri, query, form, json)
|
|
||||||
data := reqdata.New(request.URI[pathIndex:], r)
|
|
||||||
|
|
||||||
/* (2) check parameters
|
|
||||||
---------------------------------------------------------*/
|
|
||||||
parameters, paramError := server.extractParameters(data, methodConf.Parameters)
|
|
||||||
|
|
||||||
// Fail if argument check failed
|
|
||||||
if paramError.Code != api.ErrorSuccess().Code {
|
|
||||||
res := api.NewResponse(paramError)
|
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
logError(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Param = parameters
|
|
||||||
|
|
||||||
/* (3) search for the handler
|
|
||||||
---------------------------------------------------------*/
|
|
||||||
var foundHandler *api.Handler
|
|
||||||
var found bool
|
|
||||||
|
|
||||||
for _, handler := range server.handlers {
|
|
||||||
if handler.GetPath() == servicePath {
|
|
||||||
found = true
|
|
||||||
if handler.GetMethod() == r.Method {
|
|
||||||
foundHandler = handler
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if found no handler
|
// 11. apply headers
|
||||||
if foundHandler == nil {
|
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
if found {
|
for key, values := range response.Headers {
|
||||||
res := api.NewResponse()
|
|
||||||
res.SetError(api.ErrorUncallableMethod(), servicePath, r.Method)
|
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
logError(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := api.NewResponse()
|
|
||||||
res.SetError(api.ErrorUncallableService(), servicePath)
|
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
logError(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (4) execute handler and return response
|
|
||||||
---------------------------------------------------------*/
|
|
||||||
// 1. feed request with configuration scope
|
|
||||||
request.Scope = methodConf.Scope
|
|
||||||
|
|
||||||
// 2. execute
|
|
||||||
res := api.NewResponse()
|
|
||||||
foundHandler.Handle(*request, res)
|
|
||||||
|
|
||||||
// 3. apply headers
|
|
||||||
for key, values := range res.Headers {
|
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
w.Header().Add(key, value)
|
res.Header().Add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. write to response
|
// 12. write to response
|
||||||
res.ServeHTTP(w, r)
|
response.ServeHTTP(res, req)
|
||||||
return
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package cerr
|
|
||||||
|
|
||||||
// Error allows you to create constant "const" error with type boxing.
|
|
||||||
type Error string
|
|
||||||
|
|
||||||
// Error implements the error builtin interface.
|
|
||||||
func (err Error) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap returns a new error which wraps a new error into itself.
|
|
||||||
func (err Error) Wrap(e error) *WrapError {
|
|
||||||
return &WrapError{
|
|
||||||
base: err,
|
|
||||||
wrap: e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapString returns a new error which wraps a new error created from a string.
|
|
||||||
func (err Error) WrapString(e string) *WrapError {
|
|
||||||
return &WrapError{
|
|
||||||
base: err,
|
|
||||||
wrap: Error(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapError is way to wrap errors recursively.
|
|
||||||
type WrapError struct {
|
|
||||||
base error
|
|
||||||
wrap error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the error builtin interface recursively.
|
|
||||||
func (err *WrapError) Error() string {
|
|
||||||
return err.base.Error() + ": " + err.wrap.Error()
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package cerr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConstError(t *testing.T) {
|
|
||||||
const cerr1 = Error("some-string")
|
|
||||||
const cerr2 = Error("some-other-string")
|
|
||||||
const cerr3 = Error("some-string") // same const value as @cerr1
|
|
||||||
|
|
||||||
if cerr1.Error() == cerr2.Error() {
|
|
||||||
t.Errorf("cerr1 should not be equal to cerr2 ('%s', '%s')", cerr1.Error(), cerr2.Error())
|
|
||||||
}
|
|
||||||
if cerr2.Error() == cerr3.Error() {
|
|
||||||
t.Errorf("cerr2 should not be equal to cerr3 ('%s', '%s')", cerr2.Error(), cerr3.Error())
|
|
||||||
}
|
|
||||||
if cerr1.Error() != cerr3.Error() {
|
|
||||||
t.Errorf("cerr1 should be equal to cerr3 ('%s', '%s')", cerr1.Error(), cerr3.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrappedConstError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
const readErrorConst = Error("cannot read file")
|
|
||||||
var wrappedReadError = parent.Wrap(readErrorConst)
|
|
||||||
|
|
||||||
expectedWrappedReadError := fmt.Sprintf("%s: %s", parent.Error(), readErrorConst.Error())
|
|
||||||
if wrappedReadError.Error() != expectedWrappedReadError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedReadError.Error(), expectedWrappedReadError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestWrappedStandardError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
var writeErrorStandard error = errors.New("cannot write file")
|
|
||||||
var wrappedWriteError = parent.Wrap(writeErrorStandard)
|
|
||||||
|
|
||||||
expectedWrappedWriteError := fmt.Sprintf("%s: %s", parent.Error(), writeErrorStandard.Error())
|
|
||||||
if wrappedWriteError.Error() != expectedWrappedWriteError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedWriteError.Error(), expectedWrappedWriteError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestWrappedStringError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
var closeErrorString string = "cannot close file"
|
|
||||||
var wrappedCloseError = parent.WrapString(closeErrorString)
|
|
||||||
|
|
||||||
expectedWrappedCloseError := fmt.Sprintf("%s: %s", parent.Error(), closeErrorString)
|
|
||||||
if wrappedCloseError.Error() != expectedWrappedCloseError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedCloseError.Error(), expectedWrappedCloseError)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
|
|
||||||
// Service represents a service definition (from api.json)
|
|
||||||
type Service struct {
|
|
||||||
GET *Method `json:"GET"`
|
|
||||||
POST *Method `json:"POST"`
|
|
||||||
PUT *Method `json:"PUT"`
|
|
||||||
DELETE *Method `json:"DELETE"`
|
|
||||||
|
|
||||||
Children map[string]*Service `json:"/"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter represents a parameter definition (from api.json)
|
|
||||||
type Parameter struct {
|
|
||||||
Description string `json:"info"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Rename string `json:"name,omitempty"`
|
|
||||||
Optional bool
|
|
||||||
Default *interface{} `json:"default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method represents a method definition (from api.json)
|
|
||||||
type Method struct {
|
|
||||||
Description string `json:"info"`
|
|
||||||
Scope [][]string `json:"scope"`
|
|
||||||
Parameters map[string]*Parameter `json:"in"`
|
|
||||||
Download *bool `json:"download"`
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,27 +1,60 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/internal/cerr"
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrRead - a problem ocurred when trying to read the configuration file
|
// ErrRead - a problem ocurred when trying to read the configuration file
|
||||||
const ErrRead = cerr.Error("cannot read config")
|
const ErrRead = cerr("cannot read config")
|
||||||
|
|
||||||
|
// ErrUnknownMethod - invalid http method
|
||||||
|
const ErrUnknownMethod = cerr("unknown HTTP method")
|
||||||
|
|
||||||
// ErrFormat - a invalid format has been detected
|
// ErrFormat - a invalid format has been detected
|
||||||
const ErrFormat = cerr.Error("invalid config format")
|
const ErrFormat = cerr("invalid config format")
|
||||||
|
|
||||||
// ErrIllegalServiceName - an illegal character has been found in a service name
|
// ErrPatternCollision - there is a collision between 2 services' patterns (same method)
|
||||||
const ErrIllegalServiceName = cerr.Error("service must not contain any slash '/' nor '-' symbols")
|
const ErrPatternCollision = cerr("pattern collision")
|
||||||
|
|
||||||
// ErrMissingMethodDesc - a method is missing its description
|
// ErrInvalidPattern - a service pattern is malformed
|
||||||
const ErrMissingMethodDesc = cerr.Error("missing method description")
|
const ErrInvalidPattern = cerr("must begin with a '/' and not end with")
|
||||||
|
|
||||||
|
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
|
||||||
|
const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces")
|
||||||
|
|
||||||
|
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
|
||||||
|
const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path")
|
||||||
|
|
||||||
|
// ErrMandatoryRename - capture/query parameters must have a rename
|
||||||
|
const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'")
|
||||||
|
|
||||||
|
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
|
||||||
|
const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition")
|
||||||
|
|
||||||
|
// ErrMissingDescription - a service is missing its description
|
||||||
|
const ErrMissingDescription = cerr("missing description")
|
||||||
|
|
||||||
|
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
|
||||||
|
const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
|
||||||
|
|
||||||
|
// ErrOptionalOption - an output is optional
|
||||||
|
const ErrOptionalOption = cerr("output cannot be optional")
|
||||||
|
|
||||||
// ErrMissingParamDesc - a parameter is missing its description
|
// ErrMissingParamDesc - a parameter is missing its description
|
||||||
const ErrMissingParamDesc = cerr.Error("missing parameter description")
|
const ErrMissingParamDesc = cerr("missing parameter description")
|
||||||
|
|
||||||
|
// ErrUnknownDataType - a parameter has an unknown datatype name
|
||||||
|
const ErrUnknownDataType = cerr("unknown data type")
|
||||||
|
|
||||||
// ErrIllegalParamName - a parameter has an illegal name
|
// ErrIllegalParamName - a parameter has an illegal name
|
||||||
const ErrIllegalParamName = cerr.Error("illegal parameter name (must not begin/end with '_')")
|
const ErrIllegalParamName = cerr("illegal parameter name")
|
||||||
|
|
||||||
// ErrMissingParamType - a parameter has an illegal type
|
// ErrMissingParamType - a parameter has an illegal type
|
||||||
const ErrMissingParamType = cerr.Error("missing parameter type")
|
const ErrMissingParamType = cerr("missing parameter type")
|
||||||
|
|
||||||
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
||||||
const ErrParamNameConflict = cerr.Error("name conflict for parameter")
|
const ErrParamNameConflict = cerr("name conflict for parameter")
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
|
||||||
func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) error {
|
|
||||||
|
|
||||||
// 1. fail on missing description
|
|
||||||
if len(methodDef.Description) < 1 {
|
|
||||||
return ErrMissingMethodDesc.WrapString(httpMethod + " " + servicePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. stop if no parameter
|
|
||||||
if methodDef.Parameters == nil || len(methodDef.Parameters) < 1 {
|
|
||||||
methodDef.Parameters = make(map[string]*Parameter, 0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. for each parameter
|
|
||||||
for pName, pData := range methodDef.Parameters {
|
|
||||||
|
|
||||||
// 3.1. check name
|
|
||||||
if strings.Trim(pName, "_") != pName {
|
|
||||||
return ErrIllegalParamName.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pData.Rename) < 1 {
|
|
||||||
pData.Rename = pName
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Check for name/rename conflict
|
|
||||||
for paramName, param := range methodDef.Parameters {
|
|
||||||
|
|
||||||
// ignore self
|
|
||||||
if pName == paramName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2.1. Same rename field
|
|
||||||
// 3.2.2. Not-renamed field matches a renamed field
|
|
||||||
// 3.2.3. Renamed field matches name
|
|
||||||
if pData.Rename == param.Rename || pName == param.Rename || pData.Rename == paramName {
|
|
||||||
return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.3. Fail on missing description
|
|
||||||
if len(pData.Description) < 1 {
|
|
||||||
return ErrMissingParamDesc.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.4. Manage invalid type
|
|
||||||
if len(pData.Type) < 1 || pData.Type == "?" {
|
|
||||||
return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.5. Set optional + type
|
|
||||||
if pData.Type[0] == '?' {
|
|
||||||
pData.Optional = true
|
|
||||||
pData.Type = pData.Type[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate implements the validator interface
|
||||||
|
func (param *Parameter) Validate(datatypes ...datatype.T) error {
|
||||||
|
// missing description
|
||||||
|
if len(param.Description) < 1 {
|
||||||
|
return ErrMissingParamDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid type
|
||||||
|
if len(param.Type) < 1 || param.Type == "?" {
|
||||||
|
return ErrMissingParamType
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional type transform
|
||||||
|
if param.Type[0] == '?' {
|
||||||
|
param.Optional = true
|
||||||
|
param.Type = param.Type[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign the datatype
|
||||||
|
for _, dtype := range datatypes {
|
||||||
|
param.Validator = dtype.Build(param.Type, datatypes...)
|
||||||
|
param.ExtractType = dtype.Type()
|
||||||
|
if param.Validator != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if param.Validator == nil {
|
||||||
|
return ErrUnknownDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse builds a server configuration from a json reader and checks for most format errors.
|
||||||
|
// you can provide additional DataTypes as variadic arguments
|
||||||
|
func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
|
||||||
|
server := &Server{
|
||||||
|
Types: make([]datatype.T, 0),
|
||||||
|
Services: make([]*Service, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// add data types
|
||||||
|
for _, dtype := range dtypes {
|
||||||
|
server.Types = append(server.Types, dtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return server, 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
|
||||||
|
}
|
|
@ -1,104 +1,314 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse builds a service from a json reader and checks for most format errors.
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
func Parse(r io.Reader) (*Service, error) {
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
receiver := &Service{}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r).Decode(receiver)
|
// Match returns if this service would handle this HTTP request
|
||||||
if err != nil {
|
func (svc *Service) Match(req *http.Request) bool {
|
||||||
return nil, ErrRead.Wrap(err)
|
// method
|
||||||
|
if req.Method != svc.Method {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = receiver.checkAndFormat("/")
|
// check path
|
||||||
if err != nil {
|
if !svc.matchPattern(req.RequestURI) {
|
||||||
return nil, ErrFormat.Wrap(err)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return receiver, nil
|
// check and extract input
|
||||||
|
// todo: check if input match and extract models
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method returns the actual method from the http method.
|
// checks if an uri matches the service's pattern
|
||||||
func (svc *Service) Method(httpMethod string) *Method {
|
func (svc *Service) matchPattern(uri string) bool {
|
||||||
httpMethod = strings.ToUpper(httpMethod)
|
uriparts := SplitURL(uri)
|
||||||
|
parts := SplitURL(svc.Pattern)
|
||||||
|
|
||||||
switch httpMethod {
|
// fail if size differ
|
||||||
case http.MethodGet:
|
if len(uriparts) != len(parts) {
|
||||||
return svc.GET
|
return false
|
||||||
case http.MethodPost:
|
|
||||||
return svc.POST
|
|
||||||
case http.MethodPut:
|
|
||||||
return svc.PUT
|
|
||||||
case http.MethodDelete:
|
|
||||||
return svc.DELETE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// root url '/'
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse the service childtree and returns the deepest matching child. The `path` is a formatted URL split by '/'
|
// check part by part
|
||||||
func (svc *Service) Browse(path []string) (*Service, int) {
|
for i, part := range parts {
|
||||||
currentService := svc
|
uripart := uriparts[i]
|
||||||
var depth int
|
|
||||||
|
|
||||||
// for each URI depth
|
isCapture := len(part) > 0 && part[0] == '{'
|
||||||
for depth = 0; depth < len(path); depth++ {
|
|
||||||
currentPath := path[depth]
|
|
||||||
|
|
||||||
child, exists := currentService.Children[currentPath]
|
// if no capture -> check equality
|
||||||
if !exists {
|
if !isCapture {
|
||||||
break
|
if part != uripart {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
currentService = child
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentService, depth
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
|
||||||
func (svc *Service) checkAndFormat(servicePath string) error {
|
|
||||||
|
|
||||||
// 1. check and format every method
|
|
||||||
for _, httpMethod := range availableHTTPMethods {
|
|
||||||
methodDef := svc.Method(httpMethod)
|
|
||||||
if methodDef == nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err := methodDef.checkAndFormat(servicePath, httpMethod)
|
param, exists := svc.Input[part]
|
||||||
if err != nil {
|
|
||||||
return err
|
// fail if no validator
|
||||||
|
if !exists || param.Validator == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if not type-valid
|
||||||
|
if _, valid := param.Validator(uripart); !valid {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. stop if no child */
|
return true
|
||||||
if svc.Children == nil || len(svc.Children) < 1 {
|
}
|
||||||
|
|
||||||
|
// Validate implements the validator interface
|
||||||
|
func (svc *Service) Validate(datatypes ...datatype.T) error {
|
||||||
|
// check method
|
||||||
|
err := svc.isMethodAvailable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field 'method': %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check pattern
|
||||||
|
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
|
||||||
|
err = svc.isPatternValid()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field 'path': %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check description
|
||||||
|
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
|
||||||
|
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check input parameters
|
||||||
|
err = svc.validateInput(datatypes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field 'in': %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if a brace capture remains undefined
|
||||||
|
for _, capture := range svc.Captures {
|
||||||
|
if capture.Ref == nil {
|
||||||
|
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check output
|
||||||
|
err = svc.validateOutput(datatypes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field 'out': %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. for each service */
|
func (svc *Service) isMethodAvailable() error {
|
||||||
for childService, ctl := range svc.Children {
|
for _, available := range availableHTTPMethods {
|
||||||
|
if svc.Method == available {
|
||||||
// 3.1. invalid name */
|
return nil
|
||||||
if strings.ContainsAny(childService, "/-") {
|
}
|
||||||
return ErrIllegalServiceName.WrapString(childService)
|
}
|
||||||
|
return ErrUnknownMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.2. check recursively */
|
func (svc *Service) isPatternValid() error {
|
||||||
err := ctl.checkAndFormat(childService)
|
length := len(svc.Pattern)
|
||||||
if err != nil {
|
|
||||||
return err
|
// empty pattern
|
||||||
|
if length < 1 {
|
||||||
|
return ErrInvalidPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > 1 {
|
||||||
|
// pattern not starting with '/' or ending with '/'
|
||||||
|
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' {
|
||||||
|
return ErrInvalidPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each slash-separated chunk
|
||||||
|
parts := SplitURL(svc.Pattern)
|
||||||
|
for i, part := range parts {
|
||||||
|
if len(part) < 1 {
|
||||||
|
return ErrInvalidPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// if brace capture
|
||||||
|
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
braceName := matches[0][1]
|
||||||
|
|
||||||
|
// append
|
||||||
|
if svc.Captures == nil {
|
||||||
|
svc.Captures = make([]*BraceCapture, 0)
|
||||||
|
}
|
||||||
|
svc.Captures = append(svc.Captures, &BraceCapture{
|
||||||
|
Index: i,
|
||||||
|
Name: braceName,
|
||||||
|
Ref: nil,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on invalid format
|
||||||
|
if strings.ContainsAny(part, "{}") {
|
||||||
|
return ErrInvalidPatternBraceCapture
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) validateInput(types []datatype.T) error {
|
||||||
|
|
||||||
|
// ignore no parameter
|
||||||
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
|
svc.Input = make(map[string]*Parameter, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each parameter
|
||||||
|
for paramName, param := range svc.Input {
|
||||||
|
if len(paramName) < 1 {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if brace capture does not exists in pattern
|
||||||
|
var iscapture, isquery bool
|
||||||
|
if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
braceName := matches[0][1]
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, capture := range svc.Captures {
|
||||||
|
if capture.Name == braceName {
|
||||||
|
capture.Ref = param
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture)
|
||||||
|
}
|
||||||
|
iscapture = true
|
||||||
|
|
||||||
|
} else if matches := queryRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
|
||||||
|
queryName := matches[0][1]
|
||||||
|
|
||||||
|
// init map
|
||||||
|
if svc.Query == nil {
|
||||||
|
svc.Query = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Query[queryName] = param
|
||||||
|
isquery = true
|
||||||
|
} else {
|
||||||
|
if svc.Form == nil {
|
||||||
|
svc.Form = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Form[paramName] = param
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if capture or query without rename
|
||||||
|
if len(param.Rename) < 1 && (iscapture || isquery) {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrMandatoryRename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use param name if no rename
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
param.Rename = paramName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := param.Validate(types...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture parameter cannot be optional
|
||||||
|
if iscapture && param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on name/rename conflict
|
||||||
|
for paramName2, param2 := range svc.Input {
|
||||||
|
// ignore self
|
||||||
|
if paramName == paramName2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.1. Same rename field
|
||||||
|
// 3.2.2. Not-renamed field matches a renamed field
|
||||||
|
// 3.2.3. Renamed field matches name
|
||||||
|
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) validateOutput(types []datatype.T) error {
|
||||||
|
|
||||||
|
// ignore no parameter
|
||||||
|
if svc.Output == nil || len(svc.Output) < 1 {
|
||||||
|
svc.Output = make(map[string]*Parameter, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each parameter
|
||||||
|
for paramName, param := range svc.Output {
|
||||||
|
if len(paramName) < 1 {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use param name if no rename
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
param.Rename = paramName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := param.Validate(types...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on name/rename conflict
|
||||||
|
for paramName2, param2 := range svc.Output {
|
||||||
|
// ignore self
|
||||||
|
if paramName == paramName2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.1. Same rename field
|
||||||
|
// 3.2.2. Not-renamed field matches a renamed field
|
||||||
|
// 3.2.3. Renamed field matches name
|
||||||
|
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
|
// validator unifies the check and format routine
|
||||||
|
type validator interface {
|
||||||
|
Validate(...datatype.T) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server represents a full server configuration
|
||||||
|
type Server struct {
|
||||||
|
Types []datatype.T
|
||||||
|
Services []*Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service represents a service definition (from api.json)
|
||||||
|
type Service struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Pattern string `json:"path"`
|
||||||
|
Scope [][]string `json:"scope"`
|
||||||
|
Description string `json:"info"`
|
||||||
|
Input map[string]*Parameter `json:"in"`
|
||||||
|
Output map[string]*Parameter `json:"out"`
|
||||||
|
|
||||||
|
// references to url parameters
|
||||||
|
// format: '/uri/{param}'
|
||||||
|
Captures []*BraceCapture
|
||||||
|
|
||||||
|
// references to Query parameters
|
||||||
|
// format: 'GET@paranName'
|
||||||
|
Query map[string]*Parameter
|
||||||
|
|
||||||
|
// references for form parameters (all but Captures and Query)
|
||||||
|
Form map[string]*Parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter represents a parameter definition (from api.json)
|
||||||
|
type Parameter struct {
|
||||||
|
Description string `json:"info"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Rename string `json:"name,omitempty"`
|
||||||
|
// ExtractType is the type of data the datatype returns
|
||||||
|
ExtractType reflect.Type
|
||||||
|
// Optional is set to true when the type is prefixed with '?'
|
||||||
|
Optional bool
|
||||||
|
|
||||||
|
// Validator is inferred from @Type
|
||||||
|
Validator datatype.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
// BraceCapture links to the related URI parameter
|
||||||
|
type BraceCapture struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
Ref *Parameter
|
||||||
|
}
|
|
@ -1,15 +1,21 @@
|
||||||
package multipart
|
package multipart
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/internal/cerr"
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
||||||
const ErrMissingDataName = cerr.Error("data has no name")
|
const ErrMissingDataName = cerr("data has no name")
|
||||||
|
|
||||||
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
||||||
const ErrDataNameConflict = cerr.Error("data name conflict")
|
const ErrDataNameConflict = cerr("data name conflict")
|
||||||
|
|
||||||
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
||||||
const ErrNoHeader = cerr.Error("data has no header")
|
const ErrNoHeader = cerr("data has no header")
|
||||||
|
|
||||||
// Component represents a multipart variable/file
|
// Component represents a multipart variable/file
|
||||||
type Component struct {
|
type Component struct {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnknownType is returned when encountering an unknown type
|
||||||
|
const ErrUnknownType = cerr("unknown type")
|
||||||
|
|
||||||
|
// ErrInvalidMultipart is returned when multipart parse failed
|
||||||
|
const ErrInvalidMultipart = cerr("invalid multipart")
|
||||||
|
|
||||||
|
// ErrParseParameter is returned when a parameter fails when parsing
|
||||||
|
const ErrParseParameter = cerr("cannot parse parameter")
|
||||||
|
|
||||||
|
// ErrInvalidJSON is returned when json parse failed
|
||||||
|
const ErrInvalidJSON = cerr("invalid json")
|
||||||
|
|
||||||
|
// ErrMissingRequiredParam - required param is missing
|
||||||
|
const ErrMissingRequiredParam = cerr("missing required param")
|
||||||
|
|
||||||
|
// ErrInvalidType - parameter value does not satisfy its type
|
||||||
|
const ErrInvalidType = cerr("invalid type")
|
||||||
|
|
||||||
|
// ErrMissingURIParameter - missing an URI parameter
|
||||||
|
const ErrMissingURIParameter = cerr("missing URI parameter")
|
|
@ -1,128 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrUnknownType is returned when encountering an unknown type
|
|
||||||
const ErrUnknownType = cerr.Error("unknown type")
|
|
||||||
|
|
||||||
// ErrInvalidJSON is returned when json parse failed
|
|
||||||
const ErrInvalidJSON = cerr.Error("invalid json")
|
|
||||||
|
|
||||||
// ErrInvalidRootType is returned when json is a map
|
|
||||||
const ErrInvalidRootType = cerr.Error("invalid json root type")
|
|
||||||
|
|
||||||
// Parameter represents an http request parameter
|
|
||||||
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
|
||||||
type Parameter struct {
|
|
||||||
// whether the value has been json-parsed
|
|
||||||
// for optimisation purpose, parameters are only parsed
|
|
||||||
// if they are required by the current service
|
|
||||||
Parsed bool
|
|
||||||
|
|
||||||
// whether the value is a file
|
|
||||||
File bool
|
|
||||||
|
|
||||||
// the actual parameter value
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parameter (json-like) if not already done
|
|
||||||
func (i *Parameter) Parse() error {
|
|
||||||
|
|
||||||
/* (1) Stop if already parsed or nil*/
|
|
||||||
if i.Parsed || i.Value == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2) Try to parse value */
|
|
||||||
parsed, err := parseParameter(i.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
i.Parsed = true
|
|
||||||
i.Value = parsed
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseParameter parses http GET/POST data
|
|
||||||
// - []string
|
|
||||||
// - size = 1 : return json of first element
|
|
||||||
// - size > 1 : return array of json elements
|
|
||||||
// - string : return json if valid, else return raw string
|
|
||||||
func parseParameter(data interface{}) (interface{}, error) {
|
|
||||||
dtype := reflect.TypeOf(data)
|
|
||||||
dvalue := reflect.ValueOf(data)
|
|
||||||
|
|
||||||
switch dtype.Kind() {
|
|
||||||
|
|
||||||
/* (1) []string -> recursive */
|
|
||||||
case reflect.Slice:
|
|
||||||
|
|
||||||
// 1. ignore empty
|
|
||||||
if dvalue.Len() == 0 {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. parse each element recursively
|
|
||||||
result := make([]interface{}, dvalue.Len())
|
|
||||||
|
|
||||||
for i, l := 0, dvalue.Len(); i < l; i++ {
|
|
||||||
element := dvalue.Index(i)
|
|
||||||
|
|
||||||
// ignore non-string
|
|
||||||
if element.Kind() != reflect.String {
|
|
||||||
result[i] = element.Interface()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := parseParameter(element.String())
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
result[i] = parsed
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
/* (2) string -> parse */
|
|
||||||
case reflect.String:
|
|
||||||
|
|
||||||
// build json wrapper
|
|
||||||
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
|
|
||||||
|
|
||||||
// try to parse as json
|
|
||||||
var result interface{}
|
|
||||||
err := json.Unmarshal([]byte(wrapper), &result)
|
|
||||||
|
|
||||||
// return if success
|
|
||||||
if err == nil {
|
|
||||||
|
|
||||||
mapval, ok := result.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return dvalue.String(), ErrInvalidRootType
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapped, ok := mapval["wrapped"]
|
|
||||||
if !ok {
|
|
||||||
return dvalue.String(), ErrInvalidJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapped, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// else return as string
|
|
||||||
return dvalue.String(), nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (3) NIL if unknown type */
|
|
||||||
return dvalue.Interface(), nil
|
|
||||||
|
|
||||||
}
|
|
|
@ -6,21 +6,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSimpleString(t *testing.T) {
|
func TestSimpleString(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: "some-string"}
|
p := parseParameter("some-string")
|
||||||
|
|
||||||
err := p.Parse()
|
cast, canCast := p.(string)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := p.Value.(string)
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a string")
|
t.Errorf("expected parameter to be a string")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -37,19 +25,9 @@ func TestSimpleFloat(t *testing.T) {
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run("case "+string(i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: tcase}
|
p := parseParameter(tcase)
|
||||||
|
|
||||||
if err := p.Parse(); err != nil {
|
cast, canCast := p.(float64)
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := p.Value.(float64)
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a float64")
|
t.Errorf("expected parameter to be a float64")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -68,19 +46,9 @@ func TestSimpleBool(t *testing.T) {
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run("case "+string(i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: tcase}
|
p := parseParameter(tcase)
|
||||||
|
|
||||||
if err := p.Parse(); err != nil {
|
cast, canCast := p.(bool)
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := p.Value.(bool)
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a bool")
|
t.Errorf("expected parameter to be a bool")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -95,21 +63,9 @@ func TestSimpleBool(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonStringSlice(t *testing.T) {
|
func TestJsonStringSlice(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`}
|
p := parseParameter(`["str1", "str2"]`)
|
||||||
|
|
||||||
err := p.Parse()
|
slice, canCast := p.([]interface{})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
slice, canCast := p.Value.([]interface{})
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a []interface{}")
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -139,21 +95,9 @@ func TestJsonStringSlice(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStringSlice(t *testing.T) {
|
func TestStringSlice(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: []string{"str1", "str2"}}
|
p := parseParameter([]string{"str1", "str2"})
|
||||||
|
|
||||||
err := p.Parse()
|
slice, canCast := p.([]interface{})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
slice, canCast := p.Value.([]interface{})
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a []interface{}")
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -193,20 +137,9 @@ func TestJsonPrimitiveBool(t *testing.T) {
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run("case "+string(i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
|
p := parseParameter(tcase.Raw)
|
||||||
|
|
||||||
err := p.Parse()
|
cast, canCast := p.(bool)
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := p.Value.(bool)
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a bool")
|
t.Errorf("expected parameter to be a bool")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -241,20 +174,9 @@ func TestJsonPrimitiveFloat(t *testing.T) {
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run("case "+string(i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
|
p := parseParameter(tcase.Raw)
|
||||||
|
|
||||||
err := p.Parse()
|
cast, canCast := p.(float64)
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := p.Value.(float64)
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a float64")
|
t.Errorf("expected parameter to be a float64")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -270,21 +192,9 @@ func TestJsonPrimitiveFloat(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonBoolSlice(t *testing.T) {
|
func TestJsonBoolSlice(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: []string{"true", "false"}}
|
p := parseParameter([]string{"true", "false"})
|
||||||
|
|
||||||
err := p.Parse()
|
slice, canCast := p.([]interface{})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
slice, canCast := p.Value.([]interface{})
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a []interface{}")
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -314,21 +224,9 @@ func TestJsonBoolSlice(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoolSlice(t *testing.T) {
|
func TestBoolSlice(t *testing.T) {
|
||||||
p := Parameter{Parsed: false, File: false, Value: []bool{true, false}}
|
p := parseParameter([]bool{true, false})
|
||||||
|
|
||||||
err := p.Parse()
|
slice, canCast := p.([]interface{})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: <%s>", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.Parsed {
|
|
||||||
t.Errorf("expected parameter to be parsed")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
slice, canCast := p.Value.([]interface{})
|
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Errorf("expected parameter to be a []interface{}")
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
t.FailNow()
|
t.FailNow()
|
|
@ -0,0 +1,323 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
"git.xdrm.io/go/aicra/internal/multipart"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set represents all data that can be caught:
|
||||||
|
// - URI (from the URI)
|
||||||
|
// - GET (default url data)
|
||||||
|
// - POST (from json, form-data, url-encoded)
|
||||||
|
// - 'application/json' => key-value pair is parsed as json into the map
|
||||||
|
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
||||||
|
// - 'multipart/form-data' => parse form-data format
|
||||||
|
type Set struct {
|
||||||
|
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{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new empty store.
|
||||||
|
func New(service *config.Service) *Set {
|
||||||
|
return &Set{
|
||||||
|
service: service,
|
||||||
|
Data: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
||||||
|
func (i *Set) ExtractURI(req *http.Request) error {
|
||||||
|
uriparts := config.SplitURL(req.URL.RequestURI())
|
||||||
|
|
||||||
|
for _, capture := range i.service.Captures {
|
||||||
|
// out of range
|
||||||
|
if capture.Index > len(uriparts)-1 {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrMissingURIParameter)
|
||||||
|
}
|
||||||
|
value := uriparts[capture.Index]
|
||||||
|
|
||||||
|
// should not happen
|
||||||
|
if capture.Ref == nil {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := capture.Ref.Validator(parsed)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value in 'Set'
|
||||||
|
i.Data[capture.Ref.Rename] = cast
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractQuery data from the url query parameters
|
||||||
|
func (i *Set) ExtractQuery(req *http.Request) error {
|
||||||
|
query := req.URL.Query()
|
||||||
|
|
||||||
|
for name, param := range i.service.Query {
|
||||||
|
value, exist := query[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := param.Validator(parsed)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
|
i.Data[param.Rename] = cast
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractForm data from request
|
||||||
|
//
|
||||||
|
// - parse 'form-data' if not supported for non-POST requests
|
||||||
|
// - parse 'x-www-form-urlencoded'
|
||||||
|
// - parse 'application/json'
|
||||||
|
func (i *Set) ExtractForm(req *http.Request) error {
|
||||||
|
|
||||||
|
// ignore GET method
|
||||||
|
if req.Method == http.MethodGet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
// parse json
|
||||||
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
|
return i.parseJSON(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse urlencoded
|
||||||
|
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||||
|
return i.parseUrlencoded(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse multipart
|
||||||
|
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||||
|
return i.parseMultipart(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to parse
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJSON parses JSON from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseJSON(req *http.Request) error {
|
||||||
|
|
||||||
|
parsed := make(map[string]interface{}, 0)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(req.Body)
|
||||||
|
if err := decoder.Decode(&parsed); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
value, exist := parsed[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
|
cast, valid := param.Validator(value)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
|
i.Data[param.Rename] = cast
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseUrlencoded(req *http.Request) error {
|
||||||
|
// use http.Request interface
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
value, exist := req.PostForm[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := param.Validator(parsed)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
|
i.Data[param.Rename] = cast
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseMultipart(req *http.Request) error {
|
||||||
|
|
||||||
|
// 1. create reader
|
||||||
|
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||||
|
mpr, err := multipart.NewReader(req.Body, boundary)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. parse multipart
|
||||||
|
if err = mpr.Parse(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
component, exist := mpr.Data[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
|
parsed := parseParameter(string(component.Data))
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
|
cast, valid := param.Validator(parsed)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
|
i.Data[param.Rename] = cast
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParameter parses http URI/GET/POST data
|
||||||
|
// - []string : return array of json elements
|
||||||
|
// - string : return json if valid, else return raw string
|
||||||
|
func parseParameter(data interface{}) interface{} {
|
||||||
|
dtype := reflect.TypeOf(data)
|
||||||
|
dvalue := reflect.ValueOf(data)
|
||||||
|
|
||||||
|
switch dtype.Kind() {
|
||||||
|
|
||||||
|
/* (1) []string -> recursive */
|
||||||
|
case reflect.Slice:
|
||||||
|
|
||||||
|
// 1. ignore empty
|
||||||
|
if dvalue.Len() == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. parse each element recursively
|
||||||
|
result := make([]interface{}, dvalue.Len())
|
||||||
|
|
||||||
|
for i, l := 0, dvalue.Len(); i < l; i++ {
|
||||||
|
element := dvalue.Index(i)
|
||||||
|
result[i] = parseParameter(element.Interface())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
/* (2) string -> parse */
|
||||||
|
case reflect.String:
|
||||||
|
|
||||||
|
// build json wrapper
|
||||||
|
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
|
||||||
|
|
||||||
|
// try to parse as json
|
||||||
|
var result interface{}
|
||||||
|
err := json.Unmarshal([]byte(wrapper), &result)
|
||||||
|
|
||||||
|
// return if success
|
||||||
|
if err != nil {
|
||||||
|
return dvalue.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
mapval, ok := result.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return dvalue.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped, ok := mapval["wrapped"]
|
||||||
|
if !ok {
|
||||||
|
return dvalue.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (3) NIL if unknown type */
|
||||||
|
return dvalue.Interface()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,784 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEmptyService() *config.Service {
|
||||||
|
return &config.Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceWithURI(capturingBraces ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
|
||||||
|
for _, capture := range capturingBraces {
|
||||||
|
if len(capture) == 0 {
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id := fmt.Sprintf("{%s}", capture)
|
||||||
|
service.Input[id] = &config.Parameter{
|
||||||
|
Rename: capture,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Captures = append(service.Captures, &config.BraceCapture{
|
||||||
|
Name: capture,
|
||||||
|
Index: index,
|
||||||
|
Ref: service.Input[id],
|
||||||
|
})
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
func getServiceWithQuery(params ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
Query: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range params {
|
||||||
|
id := fmt.Sprintf("GET@%s", name)
|
||||||
|
service.Input[id] = &config.Parameter{
|
||||||
|
Rename: name,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Query[name] = service.Input[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
func getServiceWithForm(params ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
Form: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range params {
|
||||||
|
service.Input[name] = &config.Parameter{
|
||||||
|
Rename: name,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Form[name] = service.Input[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreWithUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
URI string
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]string{},
|
||||||
|
"/non-captured/uri",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"missing"},
|
||||||
|
"/",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "missing"},
|
||||||
|
"/gotme",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "gotittoo"},
|
||||||
|
"/gotme/andme",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "gotittoo"},
|
||||||
|
"/gotme/andme/ignored",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"first", "", "second"},
|
||||||
|
"/gotme/ignored/gotmetoo",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"first", "", "second"},
|
||||||
|
"/gotme/ignored",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("test.%d", i), func(t *testing.T) {
|
||||||
|
service := getServiceWithURI(test.ServiceParams...)
|
||||||
|
store := New(service)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
||||||
|
err := store.ExtractURI(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(store.Data) != len(service.Input) {
|
||||||
|
t.Errorf("store should contain %d elements, got %d", len(service.Input), len(store.Data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractQuery(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParam []string
|
||||||
|
Query string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues [][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ServiceParam: []string{},
|
||||||
|
Query: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"missing"},
|
||||||
|
Query: "",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Query: "a",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "missing"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "b"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=",
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "b"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=&b=x",
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=b&c=d",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=b&c=d&a=x",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
|
||||||
|
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||||
|
err := store.ExtractQuery(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, pName := range test.ParamNames {
|
||||||
|
values := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(pName, func(t *testing.T) {
|
||||||
|
param, isset := store.Data[pName]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := param.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("should return a []string (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cast) != len(values) {
|
||||||
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
|
if value != cast[vi] {
|
||||||
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||||
|
// http.Request.ParseForm() fails when:
|
||||||
|
// - http.Request.Method is one of [POST,PUT,PATCH]
|
||||||
|
// - http.Request.Form is not nil (created manually)
|
||||||
|
// - http.Request.PostForm is nil (deleted manually)
|
||||||
|
// - http.Request.Body is nil (deleted manually)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
// break everything
|
||||||
|
req.Body = nil
|
||||||
|
req.Form = make(url.Values)
|
||||||
|
req.PostForm = nil
|
||||||
|
|
||||||
|
// defer req.Body.Close()
|
||||||
|
store := New(nil)
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
URLEncoded string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues [][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
URLEncoded: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"missing"},
|
||||||
|
URLEncoded: "",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
URLEncoded: "a",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "missing"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "b"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=",
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "b"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=&b=x",
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=b&c=d",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=b&c=d&a=x",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.URLEncoded)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, key := range test.ParamNames {
|
||||||
|
values := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := param.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("should return a []interface{} (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cast) != len(values) {
|
||||||
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
|
if value != cast[vi] {
|
||||||
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
Raw string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues []interface{}
|
||||||
|
}{
|
||||||
|
// no need to fully check json because it is parsed with the standard library
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: "{}",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: `{ "a": "b" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": "b" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": "b", "ignored": "d" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Raw: `{ "a": "b", "c": "d" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: []interface{}{"b", "d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": null }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{nil},
|
||||||
|
},
|
||||||
|
// json parse error
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: `{ "a": "b", }`,
|
||||||
|
Err: ErrInvalidJSON,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.Raw)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
defer req.Body.Close()
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, pName := range test.ParamNames {
|
||||||
|
key := pName
|
||||||
|
value := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType := reflect.TypeOf(value)
|
||||||
|
|
||||||
|
paramValue := param
|
||||||
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
|
if valueType != paramValueType {
|
||||||
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramValue != value {
|
||||||
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipartParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
RawMultipart string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues []interface{}
|
||||||
|
}{
|
||||||
|
// no need to fully check json because it is parsed with the standard library
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: ``,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
`,
|
||||||
|
Err: ErrInvalidMultipart,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
--xxx--`,
|
||||||
|
Err: ErrInvalidMultipart,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx--`,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx--`,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="c"
|
||||||
|
|
||||||
|
d
|
||||||
|
--xxx--`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: []interface{}{"b", "d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="ignored"
|
||||||
|
|
||||||
|
x
|
||||||
|
--xxx--`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.RawMultipart)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
||||||
|
defer req.Body.Close()
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, key := range test.ParamNames {
|
||||||
|
value := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType := reflect.TypeOf(value)
|
||||||
|
|
||||||
|
paramValue := param
|
||||||
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
|
if valueType != paramValueType {
|
||||||
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramValue != value {
|
||||||
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,301 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/multipart"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store represents all data that can be caught:
|
|
||||||
// - URI (guessed from the URI by removing the service path)
|
|
||||||
// - GET (default url data)
|
|
||||||
// - POST (from json, form-data, url-encoded)
|
|
||||||
type Store struct {
|
|
||||||
|
|
||||||
// ordered values from the URI
|
|
||||||
// catches all after the service path
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
URI []*Parameter
|
|
||||||
|
|
||||||
// uri parameters following the QUERY format
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
Get map[string]*Parameter
|
|
||||||
|
|
||||||
// form data depending on the Content-Type:
|
|
||||||
// 'application/json' => key-value pair is parsed as json into the map
|
|
||||||
// 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
|
||||||
// 'multipart/form-data' => parse form-data format
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
Form map[string]*Parameter
|
|
||||||
|
|
||||||
// contains URL+GET+FORM data with prefixes:
|
|
||||||
// - FORM: no prefix
|
|
||||||
// - URL: 'URL#' followed by the index in Uri
|
|
||||||
// - GET: 'GET@' followed by the key in GET
|
|
||||||
Set map[string]*Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new store from an http request.
|
|
||||||
// URI params is required because it only takes into account after service path
|
|
||||||
// we do not know in this scope.
|
|
||||||
func New(uriParams []string, req *http.Request) *Store {
|
|
||||||
ds := &Store{
|
|
||||||
URI: make([]*Parameter, 0),
|
|
||||||
Get: make(map[string]*Parameter),
|
|
||||||
Form: make(map[string]*Parameter),
|
|
||||||
Set: make(map[string]*Parameter),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. set URI parameters
|
|
||||||
ds.setURIParams(uriParams)
|
|
||||||
|
|
||||||
// ignore nil requests
|
|
||||||
if req == nil {
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. GET (query) data
|
|
||||||
ds.readQuery(req)
|
|
||||||
|
|
||||||
// 3. We are done if GET method
|
|
||||||
if req.Method == http.MethodGet {
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. POST (body) data
|
|
||||||
ds.readForm(req)
|
|
||||||
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// setURIParameters fills 'Set' with creating pointers inside 'Url'
|
|
||||||
func (i *Store) setURIParams(orderedUParams []string) {
|
|
||||||
|
|
||||||
for index, value := range orderedUParams {
|
|
||||||
|
|
||||||
// create set index
|
|
||||||
setindex := fmt.Sprintf("URL#%d", index)
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[setindex] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Url'
|
|
||||||
i.URI = append(i.URI, i.Set[setindex])
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// readQuery stores data from the QUERY (in url parameters)
|
|
||||||
func (i *Store) readQuery(req *http.Request) {
|
|
||||||
|
|
||||||
for name, value := range req.URL.Query() {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("get.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// create set index
|
|
||||||
setindex := fmt.Sprintf("GET@%s", name)
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[setindex] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Get'
|
|
||||||
i.Get[name] = i.Set[setindex]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// readForm stores FORM data
|
|
||||||
//
|
|
||||||
// - parse 'form-data' if not supported (not POST requests)
|
|
||||||
// - parse 'x-www-form-urlencoded'
|
|
||||||
// - parse 'application/json'
|
|
||||||
func (i *Store) readForm(req *http.Request) {
|
|
||||||
|
|
||||||
contentType := req.Header.Get("Content-Type")
|
|
||||||
|
|
||||||
// parse json
|
|
||||||
if strings.HasPrefix(contentType, "application/json") {
|
|
||||||
i.parseJSON(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse urlencoded
|
|
||||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
|
||||||
i.parseUrlencoded(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse multipart
|
|
||||||
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
|
||||||
i.parseMultipart(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if unknown type store nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseJSON parses JSON from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseJSON(req *http.Request) {
|
|
||||||
|
|
||||||
parsed := make(map[string]interface{}, 0)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
|
|
||||||
// if parse error: do nothing
|
|
||||||
if err := decoder.Decode(&parsed); err != nil {
|
|
||||||
log.Printf("json.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// else store values 'parsed' values
|
|
||||||
for name, value := range parsed {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: true,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseUrlencoded(req *http.Request) {
|
|
||||||
|
|
||||||
// use http.Request interface
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
log.Printf("urlencoded.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range req.PostForm {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseMultipart(req *http.Request) {
|
|
||||||
|
|
||||||
/* (1) Create reader */
|
|
||||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
|
||||||
mpr, err := multipart.NewReader(req.Body, boundary)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2) Parse multipart */
|
|
||||||
if err = mpr.Parse(); err != nil {
|
|
||||||
log.Printf("multipart.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (3) Store data into 'Form' and 'Set */
|
|
||||||
for name, data := range mpr.Data {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
File: len(data.GetHeader("filename")) > 0,
|
|
||||||
Value: string(data.Data),
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasNameInjection returns whether there is
|
|
||||||
// a parameter name injection:
|
|
||||||
// - inferred GET parameters
|
|
||||||
// - inferred URL parameters
|
|
||||||
func hasNameInjection(pName string) bool {
|
|
||||||
return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNameValid returns whether a parameter name (without the GET@ or URL# prefix) is valid
|
|
||||||
// if fails if the name begins/ends with underscores
|
|
||||||
func isNameValid(pName string) bool {
|
|
||||||
return strings.Trim(pName, "_") == pName
|
|
||||||
}
|
|
|
@ -1,804 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEmptyStore(t *testing.T) {
|
|
||||||
store := New(nil, nil)
|
|
||||||
|
|
||||||
if store.URI == nil {
|
|
||||||
t.Errorf("store 'URI' list should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if len(store.URI) != 0 {
|
|
||||||
t.Errorf("store 'URI' list should be empty")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if store.Get == nil {
|
|
||||||
t.Errorf("store 'Get' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if store.Form == nil {
|
|
||||||
t.Errorf("store 'Form' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if store.Set == nil {
|
|
||||||
t.Errorf("store 'Set' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreWithUri(t *testing.T) {
|
|
||||||
urilist := []string{"abc", "def"}
|
|
||||||
store := New(urilist, nil)
|
|
||||||
|
|
||||||
if len(store.URI) != len(urilist) {
|
|
||||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.URI))
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if len(store.Set) != len(urilist) {
|
|
||||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.Set))
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, value := range urilist {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("URL#%d='%s'", i, value), func(t *testing.T) {
|
|
||||||
key := fmt.Sprintf("URL#%d", i)
|
|
||||||
element, isset := store.Set[key]
|
|
||||||
|
|
||||||
if !isset {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if element.Value != value {
|
|
||||||
t.Errorf("store[%s] should return '%s' (got '%s')", key, value, element.Value)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreWithGet(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Query string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues [][]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Query: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: [][]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a&b",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=&b=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&c=d",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&c=d&a=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&_invalid=x",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&invalid_=x",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&GET@injection=x",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{ // not really useful as all after '#' should be ignored by http clients
|
|
||||||
Query: "a=b&URL#injection=x",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no GET parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := fmt.Sprintf("GET@%s", pName)
|
|
||||||
values := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := param.Value.([]string)
|
|
||||||
|
|
||||||
if !canCast {
|
|
||||||
t.Errorf("should return a []string (got '%v')", cast)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
|
||||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for vi, value := range values {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
|
||||||
if value != cast[vi] {
|
|
||||||
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
|
||||||
// http.Request.ParseForm() fails when:
|
|
||||||
// - http.Request.Method is one of [POST,PUT,PATCH]
|
|
||||||
// - http.Request.Form is not nil (created manually)
|
|
||||||
// - http.Request.PostForm is nil (deleted manually)
|
|
||||||
// - http.Request.Body is nil (deleted manually)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
|
|
||||||
// break everything
|
|
||||||
req.Body = nil
|
|
||||||
req.Form = make(url.Values)
|
|
||||||
req.PostForm = nil
|
|
||||||
|
|
||||||
// defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
if len(store.Form) > 0 {
|
|
||||||
t.Errorf("expected malformed urlencoded to have failed being parsed (got %d elements)", len(store.Form))
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestStoreWithUrlEncodedForm(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
URLEncoded string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues [][]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
URLEncoded: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: [][]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a&b",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=&b=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&c=d",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&c=d&a=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&_invalid=x",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&invalid_=x",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&GET@injection=x",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&URL#injection=x",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.URLEncoded)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no FORM parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
values := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := param.Value.([]string)
|
|
||||||
|
|
||||||
if !canCast {
|
|
||||||
t.Errorf("should return a []string (got '%v')", cast)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
|
||||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for vi, value := range values {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
|
||||||
if value != cast[vi] {
|
|
||||||
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJsonParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
RawJson string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues []interface{}
|
|
||||||
}{
|
|
||||||
// no need to fully check json because it is parsed with the standard library
|
|
||||||
{
|
|
||||||
RawJson: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{}",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\" }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: []interface{}{"b"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"c\": \"d\" }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: []interface{}{"b", "d"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"_invalid\": \"x\" }",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"_invalid"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"_invalid\": \"x\" }",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"invalid_\": \"x\" }",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"invalid_"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"invalid_\": \"x\" }",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"GET@injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"GET@injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"GET@injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"URL#injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"URL#injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"URL#injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
// json parse error
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.RawJson)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
value := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
valueType := reflect.TypeOf(value)
|
|
||||||
|
|
||||||
paramValue := param.Value
|
|
||||||
paramValueType := reflect.TypeOf(param.Value)
|
|
||||||
|
|
||||||
if valueType != paramValueType {
|
|
||||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if paramValue != value {
|
|
||||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipartParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
RawMultipart string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues []interface{}
|
|
||||||
}{
|
|
||||||
// no need to fully check json because it is parsed with the standard library
|
|
||||||
{
|
|
||||||
RawMultipart: ``,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: []interface{}{"b"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="c"
|
|
||||||
|
|
||||||
d
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: []interface{}{"b", "d"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="_invalid"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"_invalid"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="_invalid"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="invalid_"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"invalid_"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="invalid_"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="GET@injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"GET@injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="GET@injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="URL#injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"URL#injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="URL#injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
// json parse error
|
|
||||||
{
|
|
||||||
RawMultipart: "{ \"a\": \"b\", }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.RawMultipart)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
value := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
valueType := reflect.TypeOf(value)
|
|
||||||
|
|
||||||
paramValue := param.Value
|
|
||||||
paramValueType := reflect.TypeOf(param.Value)
|
|
||||||
|
|
||||||
if valueType != paramValueType {
|
|
||||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if paramValue != value {
|
|
||||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
69
server.go
69
server.go
|
@ -1,25 +1,23 @@
|
||||||
package aicra
|
package aicra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
"git.xdrm.io/go/aicra/dynamic"
|
||||||
"git.xdrm.io/go/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
checker "git.xdrm.io/go/aicra/typecheck"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server represents an AICRA instance featuring: type checkers, services
|
// Server represents an AICRA instance featuring: type checkers, services
|
||||||
type Server struct {
|
type Server struct {
|
||||||
config *config.Service
|
config *config.Server
|
||||||
Checkers *checker.Set
|
handlers []*handler
|
||||||
handlers []*api.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a framework instance from a configuration file
|
// New creates a framework instance from a configuration file
|
||||||
func New(configPath string) (*Server, error) {
|
func New(configPath string, dtypes ...datatype.T) (*Server, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
configFile io.ReadCloser
|
configFile io.ReadCloser
|
||||||
|
@ -28,8 +26,7 @@ func New(configPath string) (*Server, error) {
|
||||||
// 1. init instance
|
// 1. init instance
|
||||||
var i = &Server{
|
var i = &Server{
|
||||||
config: nil,
|
config: nil,
|
||||||
Checkers: checker.New(),
|
handlers: make([]*handler, 0),
|
||||||
handlers: make([]*api.Handler, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. open config file
|
// 2. open config file
|
||||||
|
@ -40,39 +37,55 @@ func New(configPath string) (*Server, error) {
|
||||||
defer configFile.Close()
|
defer configFile.Close()
|
||||||
|
|
||||||
// 3. load configuration
|
// 3. load configuration
|
||||||
i.config, err = config.Parse(configFile)
|
i.config, err = config.Parse(configFile, dtypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. log configuration services
|
|
||||||
log.Printf("🔧 Reading configuration '%s'\n", configPath)
|
|
||||||
logService(*i.config, "")
|
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleFunc sets a new handler for an HTTP method to a path
|
// Handle sets a new handler for an HTTP method to a path
|
||||||
func (s *Server) HandleFunc(httpMethod, path string, handlerFunc api.HandlerFunc) {
|
func (s *Server) Handle(method, path string, fn dynamic.HandlerFn) error {
|
||||||
handler := api.NewHandler(httpMethod, path, handlerFunc)
|
// find associated service
|
||||||
s.handlers = append(s.handlers, handler)
|
var found *config.Service = nil
|
||||||
|
for _, service := range s.config.Services {
|
||||||
|
if method == service.Method && path == service.Pattern {
|
||||||
|
found = service
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
return fmt.Errorf("%s '%s': %w", method, path, ErrNoServiceForHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sets a new handler
|
handler, err := createHandler(method, path, *found, fn)
|
||||||
func (s *Server) Handle(handler *api.Handler) {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s.handlers = append(s.handlers, handler)
|
s.handlers = append(s.handlers, handler)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP converts the server to a http server
|
// ToHTTPServer converts the server to a http server
|
||||||
func (s Server) HTTP() httpServer {
|
func (s Server) ToHTTPServer() (*httpServer, error) {
|
||||||
|
|
||||||
// 1. log available handlers
|
// check if handlers are missing
|
||||||
log.Printf("🔗 Mapping handlers\n")
|
for _, service := range s.config.Services {
|
||||||
for i := 0; i < len(s.handlers); i++ {
|
found := false
|
||||||
log.Printf(" ->\t%s\t'%s'\n", s.handlers[i].GetMethod(), s.handlers[i].GetPath())
|
for _, handler := range s.handlers {
|
||||||
|
if handler.Method == service.Method && handler.Path == service.Pattern {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrNoHandlerForService)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. cast to http server
|
// 2. cast to http server
|
||||||
return httpServer(s)
|
httpServer := httpServer(s)
|
||||||
|
return &httpServer, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/typecheck"
|
|
||||||
|
|
||||||
// Any is a permissive type checker
|
|
||||||
type Any struct{}
|
|
||||||
|
|
||||||
// NewAny returns a bare any type checker
|
|
||||||
func NewAny() *Any {
|
|
||||||
return &Any{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker returns the checker function
|
|
||||||
func (Any) Checker(typeName string) typecheck.CheckerFunc {
|
|
||||||
// nothing if type not handled
|
|
||||||
if typeName != "any" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(interface{}) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/typecheck"
|
|
||||||
|
|
||||||
// Bool checks if a value is a boolean
|
|
||||||
type Bool struct{}
|
|
||||||
|
|
||||||
// NewBool returns a bare boolean type checker
|
|
||||||
func NewBool() *Bool {
|
|
||||||
return &Bool{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker returns the checker function
|
|
||||||
func (Bool) Checker(typeName string) typecheck.CheckerFunc {
|
|
||||||
// nothing if type not handled
|
|
||||||
if typeName != "bool" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) bool {
|
|
||||||
_, isBool := readBool(value)
|
|
||||||
return isBool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBool tries to read a serialized boolean and returns whether it succeeded.
|
|
||||||
func readBool(value interface{}) (bool, bool) {
|
|
||||||
switch cast := value.(type) {
|
|
||||||
case bool:
|
|
||||||
return cast, true
|
|
||||||
|
|
||||||
case string:
|
|
||||||
strVal := string(cast)
|
|
||||||
return strVal == "true", strVal == "true" || strVal == "false"
|
|
||||||
|
|
||||||
case []byte:
|
|
||||||
strVal := string(cast)
|
|
||||||
return strVal == "true", strVal == "true" || strVal == "false"
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, false
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Float64 checks if a value is a float64
|
|
||||||
type Float64 struct{}
|
|
||||||
|
|
||||||
// NewFloat64 returns a bare number type checker
|
|
||||||
func NewFloat64() *Float64 {
|
|
||||||
return &Float64{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker returns the checker function
|
|
||||||
func (Float64) Checker(typeName string) typecheck.CheckerFunc {
|
|
||||||
// nothing if type not handled
|
|
||||||
if typeName != "float64" && typeName != "float" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) bool {
|
|
||||||
_, isFloat := readFloat(value)
|
|
||||||
return isFloat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readFloat tries to read a serialized float and returns whether it succeeded.
|
|
||||||
func readFloat(value interface{}) (float64, bool) {
|
|
||||||
switch cast := value.(type) {
|
|
||||||
|
|
||||||
case int:
|
|
||||||
return float64(cast), true
|
|
||||||
|
|
||||||
case uint:
|
|
||||||
return float64(cast), true
|
|
||||||
|
|
||||||
case float64:
|
|
||||||
return cast, true
|
|
||||||
|
|
||||||
// serialized string -> try to convert to float
|
|
||||||
case string:
|
|
||||||
num := json.Number(cast)
|
|
||||||
floatVal, err := num.Float64()
|
|
||||||
return floatVal, err == nil
|
|
||||||
|
|
||||||
case []byte:
|
|
||||||
num := json.Number(cast)
|
|
||||||
floatVal, err := num.Float64()
|
|
||||||
return floatVal, err == nil
|
|
||||||
|
|
||||||
// unknown type
|
|
||||||
default:
|
|
||||||
return 0, false
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Int checks if a value is an int
|
|
||||||
type Int struct{}
|
|
||||||
|
|
||||||
// NewInt returns a bare number type checker
|
|
||||||
func NewInt() *Int {
|
|
||||||
return &Int{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker returns the checker function
|
|
||||||
func (Int) Checker(typeName string) typecheck.CheckerFunc {
|
|
||||||
// nothing if type not handled
|
|
||||||
if typeName != "int" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) bool {
|
|
||||||
_, isInt := readInt(value)
|
|
||||||
|
|
||||||
return isInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readInt tries to read a serialized int and returns whether it succeeded.
|
|
||||||
func readInt(value interface{}) (int, bool) {
|
|
||||||
switch cast := value.(type) {
|
|
||||||
|
|
||||||
case int:
|
|
||||||
return cast, true
|
|
||||||
|
|
||||||
case uint:
|
|
||||||
overflows := cast > math.MaxInt64
|
|
||||||
return int(cast), !overflows
|
|
||||||
|
|
||||||
case float64:
|
|
||||||
intVal := int(cast)
|
|
||||||
overflows := cast < float64(math.MinInt64) || cast > float64(math.MaxInt64)
|
|
||||||
return intVal, cast == float64(intVal) && !overflows
|
|
||||||
|
|
||||||
// serialized string -> try to convert to float
|
|
||||||
case string:
|
|
||||||
num := json.Number(cast)
|
|
||||||
intVal, err := num.Int64()
|
|
||||||
return int(intVal), err == nil
|
|
||||||
// serialized string -> try to convert to float
|
|
||||||
|
|
||||||
case []byte:
|
|
||||||
num := json.Number(cast)
|
|
||||||
intVal, err := num.Int64()
|
|
||||||
return int(intVal), err == nil
|
|
||||||
|
|
||||||
// unknown type
|
|
||||||
default:
|
|
||||||
return 0, false
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/typecheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Uint checks if a value is an uint
|
|
||||||
type Uint struct{}
|
|
||||||
|
|
||||||
// NewUint returns a bare number type checker
|
|
||||||
func NewUint() *Uint {
|
|
||||||
return &Uint{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker returns the checker function
|
|
||||||
func (Uint) Checker(typeName string) typecheck.CheckerFunc {
|
|
||||||
// nothing if type not handled
|
|
||||||
if typeName != "uint" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) bool {
|
|
||||||
_, isInt := readUint(value)
|
|
||||||
|
|
||||||
return isInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readUint tries to read a serialized uint and returns whether it succeeded.
|
|
||||||
func readUint(value interface{}) (uint, bool) {
|
|
||||||
switch cast := value.(type) {
|
|
||||||
|
|
||||||
case int:
|
|
||||||
return uint(cast), cast >= 0
|
|
||||||
|
|
||||||
case uint:
|
|
||||||
return cast, true
|
|
||||||
|
|
||||||
case float64:
|
|
||||||
uintVal := uint(cast)
|
|
||||||
overflows := cast < 0 || cast > math.MaxUint64
|
|
||||||
return uintVal, cast == float64(uintVal) && !overflows
|
|
||||||
|
|
||||||
// serialized string -> try to convert to float
|
|
||||||
case string:
|
|
||||||
num := json.Number(cast)
|
|
||||||
floatVal, err := num.Float64()
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
overflows := floatVal < 0 || floatVal > math.MaxUint64
|
|
||||||
return uint(floatVal), !overflows
|
|
||||||
|
|
||||||
case []byte:
|
|
||||||
num := json.Number(cast)
|
|
||||||
floatVal, err := num.Float64()
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
overflows := floatVal < 0 || floatVal > math.MaxUint64
|
|
||||||
return uint(floatVal), !overflows
|
|
||||||
|
|
||||||
// unknown type
|
|
||||||
default:
|
|
||||||
return 0, false
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package typecheck
|
|
||||||
|
|
||||||
// Set of type checkers
|
|
||||||
type Set struct {
|
|
||||||
types []Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new set of type checkers
|
|
||||||
func New() *Set {
|
|
||||||
return &Set{types: make([]Type, 0)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a new type checker
|
|
||||||
func (s *Set) Add(typeChecker Type) {
|
|
||||||
s.types = append(s.types, typeChecker)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run finds a type checker from the registry matching the type `typeName`
|
|
||||||
// and uses this checker to check the `value`. If no type checker matches
|
|
||||||
// the `type`, error is returned by default.
|
|
||||||
func (s *Set) Run(typeName string, value interface{}) error {
|
|
||||||
|
|
||||||
// find matching type (take first)
|
|
||||||
for _, typeChecker := range s.types {
|
|
||||||
if typeChecker == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// found
|
|
||||||
checkerFunc := typeChecker.Checker(typeName)
|
|
||||||
if checkerFunc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check value
|
|
||||||
if checkerFunc(value) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ErrDoesNotMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrNoMatchingType
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package typecheck
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// ErrNoMatchingType when no available type checker matches the type
|
|
||||||
var ErrNoMatchingType = errors.New("no matching type")
|
|
||||||
|
|
||||||
// ErrDoesNotMatch when the value is invalid
|
|
||||||
var ErrDoesNotMatch = errors.New("does not match")
|
|
||||||
|
|
||||||
// CheckerFunc returns whether a given value fulfills a type
|
|
||||||
type CheckerFunc func(interface{}) bool
|
|
||||||
|
|
||||||
// Type represents a type checker
|
|
||||||
type Type interface {
|
|
||||||
// given a type name, returns the checker function or NIL if the type is not handled here
|
|
||||||
Checker(string) CheckerFunc
|
|
||||||
}
|
|
90
util.go
90
util.go
|
@ -5,101 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/api"
|
||||||
"git.xdrm.io/go/aicra/internal/config"
|
|
||||||
"git.xdrm.io/go/aicra/internal/reqdata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// extractParameters extracts parameters for the request and checks
|
|
||||||
// every single one according to configuration options
|
|
||||||
func (s *httpServer) extractParameters(store *reqdata.Store, methodParam map[string]*config.Parameter) (map[string]interface{}, api.Error) {
|
|
||||||
|
|
||||||
// init vars
|
|
||||||
parameters := make(map[string]interface{})
|
|
||||||
|
|
||||||
// for each param of the config
|
|
||||||
for name, param := range methodParam {
|
|
||||||
|
|
||||||
// 1. extract value
|
|
||||||
p, isset := store.Set[name]
|
|
||||||
|
|
||||||
// 2. fail if required & missing
|
|
||||||
if !isset && !param.Optional {
|
|
||||||
apiErr := api.ErrorMissingParam()
|
|
||||||
apiErr.SetArguments(name)
|
|
||||||
return nil, apiErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. optional & missing: set default value
|
|
||||||
if !isset {
|
|
||||||
p = &reqdata.Parameter{
|
|
||||||
Parsed: true,
|
|
||||||
File: param.Type == "FILE",
|
|
||||||
Value: nil,
|
|
||||||
}
|
|
||||||
if param.Default != nil {
|
|
||||||
p.Value = *param.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
// we are done
|
|
||||||
parameters[param.Rename] = p.Value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. parse parameter if not file
|
|
||||||
if !p.File {
|
|
||||||
p.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. fail on unexpected multipart file
|
|
||||||
waitFile, gotFile := param.Type == "FILE", p.File
|
|
||||||
if gotFile && !waitFile || !gotFile && waitFile {
|
|
||||||
apiErr := api.ErrorInvalidParam()
|
|
||||||
apiErr.SetArguments(param.Rename, "FILE")
|
|
||||||
return nil, apiErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. ignore type check if file
|
|
||||||
if gotFile {
|
|
||||||
parameters[param.Rename] = p.Value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. check type
|
|
||||||
if s.Checkers.Run(param.Type, p.Value) != nil {
|
|
||||||
apiErr := api.ErrorInvalidParam()
|
|
||||||
apiErr.SetArguments(param.Rename, param.Type, p.Value)
|
|
||||||
return nil, apiErr
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters[param.Rename] = p.Value
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return parameters, api.ErrorSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
// Prints an error as HTTP response
|
// Prints an error as HTTP response
|
||||||
func logError(res *api.Response) {
|
func logError(res *api.Response) {
|
||||||
log.Printf("[http.fail] %v\n", res)
|
log.Printf("[http.fail] %v\n", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// logService logs a service details
|
|
||||||
func logService(s config.Service, path string) {
|
|
||||||
for _, method := range handledMethods {
|
|
||||||
if m := s.Method(method); m != nil {
|
|
||||||
if path == "" {
|
|
||||||
log.Printf(" ->\t%s\t'/'\n", method)
|
|
||||||
} else {
|
|
||||||
log.Printf(" ->\t%s\t'%s'\n", method, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Children != nil {
|
|
||||||
for subPath, child := range s.Children {
|
|
||||||
logService(*child, path+"/"+subPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue