Compare commits
No commits in common. "0.4.0" and "master" have entirely different histories.
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: golang:1.13
|
||||||
|
commands:
|
||||||
|
- go get ./...
|
||||||
|
- go test -v -race -cover -coverprofile ./coverage.out ./...
|
|
@ -1,27 +0,0 @@
|
||||||
name: Go
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: 1.16
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: go build -v ./...
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: go test -race -v ./... -cover
|
|
438
README.md
438
README.md
|
@ -1,339 +1,215 @@
|
||||||
<p align="center">
|
# | aicra |
|
||||||
<a href="https://github.com/xdrm-io/aicra">
|
|
||||||
<img src="https://github.com/xdrm-io/aicra/raw/0.4.0/readme.assets/logo.png" alt="aicra logo" width="200" height="200">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 align="center">aicra</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Fast, intuitive, and powerful configuration-driven engine for faster and easier <em>REST</em> development.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
[![Go version](https://img.shields.io/badge/go_version-1.10.3-blue.svg)](https://golang.org/doc/go1.10)
|
||||||
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
||||||
[![Go version](https://img.shields.io/badge/go_version-1.16-blue.svg)](https://golang.org/doc/go1.16)
|
[![Go Report Card](https://goreportcard.com/badge/git.xdrm.io/go/aicra)](https://goreportcard.com/report/git.xdrm.io/go/aicra)
|
||||||
[![Go doc](https://pkg.go.dev/badge/github.com/xdrm-io/aicra)](https://pkg.go.dev/github.com/xdrm-io/aicra)
|
[![Go doc](https://godoc.org/git.xdrm.io/go/aicra?status.svg)](https://godoc.org/git.xdrm.io/go/aicra)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/xdrm-io/aicra)](https://goreportcard.com/report/github.com/xdrm-io/aicra)
|
[![Build Status](https://drone.xdrm.io/api/badges/go/aicra/status.svg)](https://drone.xdrm.io/go/aicra)
|
||||||
[![Build status](https://github.com/xdrm-io/aicra/actions/workflows/go.yml/badge.svg)](https://github.com/xdrm-io/aicra/actions/workflows/go.yml)
|
|
||||||
|
|
||||||
## Presentation
|
|
||||||
|
|
||||||
`aicra` is a lightweight and idiomatic configuration-driven engine for building REST services. It's especially good at helping you write large APIs that remain maintainable as your project grows.
|
**Aicra** is a *configuration-driven* **web framework** written in Go that allows you to create a fully featured REST API.
|
||||||
|
|
||||||
The focus of the project is to allow you to build a fully-featured REST API in an elegant, comfortable and inexpensive way. This is achieved by using a single configuration file to drive the server. This one file describes your entire API: methods, uris, input data, expected output, permissions, etc.
|
The whole management is done for you from a configuration file describing your API, you're left with implementing :
|
||||||
|
- handlers
|
||||||
|
- optionnally middle-wares (_e.g. authentication, csrf_)
|
||||||
|
- and optionnally your custom type checkers to check input parameters
|
||||||
|
|
||||||
Repetitive tasks are automatically processed by `aicra` based on your configuration, you're left with implementing your handlers (_usually business logic_).
|
|
||||||
|
|
||||||
## Table of contents
|
The aicra server fulfills the `net/http` [Server interface](https://golang.org/pkg/net/http/#Server).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> A example project is available [here](https://git.xdrm.io/go/tiny-url-ex)
|
||||||
|
|
||||||
|
|
||||||
|
### Table of contents
|
||||||
|
|
||||||
<!-- toc -->
|
<!-- toc -->
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [I/ Installation](#i-installation)
|
||||||
- [What's automated](#whats-automated)
|
- [II/ Development](#ii-development)
|
||||||
- [Getting started](#getting-started)
|
* [1) Main executable](#1-main-executable)
|
||||||
- [Configuration file](#configuration-file)
|
* [2) API Configuration](#2-api-configuration)
|
||||||
* [Services](#services)
|
- [Definition](#definition)
|
||||||
* [Input and output parameters](#input-and-output-parameters)
|
+ [Input Arguments](#input-arguments)
|
||||||
* [Example](#example)
|
- [1. Input types](#1-input-types)
|
||||||
- [Writing your code](#writing-your-code)
|
- [2. Global Format](#2-global-format)
|
||||||
- [Changelog](#changelog)
|
- [III/ Change Log](#iii-change-log)
|
||||||
|
|
||||||
<!-- tocstop -->
|
<!-- tocstop -->
|
||||||
|
|
||||||
## Installation
|
### I/ Installation
|
||||||
|
|
||||||
|
You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.10**.
|
||||||
|
|
||||||
To install the aicra package, you need to install Go and set your Go workspace first.
|
|
||||||
> not tested under Go 1.14
|
|
||||||
|
|
||||||
1. you can use the below Go command to install aicra.
|
|
||||||
```bash
|
```bash
|
||||||
$ go get -u github.com/xdrm-io/aicra
|
go get -u git.xdrm.io/go/aicra/cmd/aicra
|
||||||
```
|
|
||||||
2. Import it in your code:
|
|
||||||
```go
|
|
||||||
import "github.com/xdrm-io/aicra"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's automated
|
The library should now be available as `git.xdrm.io/go/aicra` in your imports.
|
||||||
|
|
||||||
As the configuration file is here to make your life easier, let's take a quick look at what you do not have to do ; or in other words, what does `aicra` automates.
|
|
||||||
|
|
||||||
Http requests are only accepted when they have the permissions you have defined. If unauthorized, the request is rejected with an error response.
|
### II/ Development
|
||||||
|
|
||||||
Request data is automatically extracted and validated before it reaches your code. If a request has missing or invalid data an automatic error response is sent.
|
|
||||||
|
|
||||||
When launching the server, it ensures everything is ok and won't start until fixed. You will get errors for:
|
#### 1) Main executable
|
||||||
- handler signature does not match the configuration
|
|
||||||
- a configuration service has no handler
|
|
||||||
- a handler does not match any service
|
|
||||||
|
|
||||||
The same applies if your configuration is invalid:
|
Your main executable will declare and run the aicra server, it might look quite like the code below.
|
||||||
- unknown HTTP method
|
|
||||||
- invalid uri
|
|
||||||
- uri collision between 2 services
|
|
||||||
- missing fields
|
|
||||||
- unknown data type
|
|
||||||
- input name collision
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Here is the minimal code to launch your aicra server assuming your configuration file is `api.json`.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra"
|
"git.xdrm.io/go/aicra"
|
||||||
"github.com/xdrm-io/aicra/api"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
"github.com/xdrm-io/aicra/validator/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
builder := &aicra.Builder{}
|
|
||||||
|
|
||||||
// add custom type validators
|
// 1. select your datatypes (builtin, custom)
|
||||||
builder.Validate(validator.BoolDataType{})
|
var dtypes []datatype.T
|
||||||
builder.Validate(validator.UintDataType{})
|
dtypes = append(dtypes, builtin.AnyDataType{})
|
||||||
builder.Validate(validator.StringDataType{})
|
dtypes = append(dtypes, builtin.BoolDataType{})
|
||||||
|
dtypes = append(dtypes, builtin.UintDataType{})
|
||||||
|
dtypes = append(dtypes, builtin.StringDataType{})
|
||||||
|
|
||||||
// load your configuration
|
// 2. create the server from the configuration file
|
||||||
config, err := os.Open("api.json")
|
server, err := aicra.New("path/to/your/api/definition.json", dtypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("cannot open config: %s", err)
|
log.Fatalf("cannot built aicra server: %s\n", err)
|
||||||
}
|
|
||||||
err = builder.Setup(config)
|
|
||||||
config.Close() // free config file
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid config: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add http middlewares (logger)
|
|
||||||
builder.With(func(next http.Handler) http.Handler{ /* ... */ })
|
|
||||||
|
|
||||||
// add contextual middlewares (authentication)
|
|
||||||
builder.WithContext(func(next http.Handler) http.Handler{ /* ... */ })
|
|
||||||
|
|
||||||
// bind handlers
|
|
||||||
err = builder.Bind(http.MethodGet, "/user/{id}", getUserById)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("cannog bind GET /user/{id}: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build your services
|
|
||||||
handler, err := builder.Build()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("cannot build handler: %s", err)
|
|
||||||
}
|
|
||||||
http.ListenAndServe("localhost:8080", handler)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to use HTTPS, you can configure your own `http.Server`.
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: "localhost:8080",
|
|
||||||
TLSConfig: tls.Config{},
|
|
||||||
// ...
|
|
||||||
Handler: AICRAHandler,
|
|
||||||
}
|
}
|
||||||
server.ListenAndServe()
|
|
||||||
|
// 3. bind your implementations
|
||||||
|
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){
|
||||||
|
// ... process stuff ...
|
||||||
|
res.SetError(api.ErrorSuccess());
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. extract to http server
|
||||||
|
httpServer, err := server.ToHTTPServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot get to http server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. launch server
|
||||||
|
log.Fatal( http.ListenAndServe("localhost:8080", server) )
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration file
|
|
||||||
|
|
||||||
First of all, the configuration uses `json`.
|
|
||||||
|
|
||||||
> Quick note if you thought: "I hate JSON, I would have preferred yaml, or even xml !"
|
#### 2) API Configuration
|
||||||
|
|
||||||
|
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 :
|
||||||
|
|
||||||
|
- routes and their methods
|
||||||
|
- every input for each method (called *argument*)
|
||||||
|
- every output for each method
|
||||||
|
- scope permissions (list of permissions needed by clients)
|
||||||
|
- input policy :
|
||||||
|
- type of argument (_i.e. for data types_)
|
||||||
|
- required/optional
|
||||||
|
- variable renaming
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###### Definition
|
||||||
|
|
||||||
|
The root of the json file must be an array containing your requests definitions.
|
||||||
|
|
||||||
|
For each, you will have to create fields described in the table above.
|
||||||
|
|
||||||
|
| field path | description | example |
|
||||||
|
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||||
|
| `info` | A short human-readable description of what the method does | `create a new user` |
|
||||||
|
| `scope` | A 2-dimensional array of permissions. The first dimension can be translated to a **or** operator, the second dimension as a **and**. It allows you to combine permissions in complex ways. | `[["A", "B"], ["C", "D"]]` can be translated to : this method needs users to have permissions (A **and** B) **or** (C **and** D) |
|
||||||
|
| `in` | The list of arguments that the clients will have to provide. See [here](#input-arguments) for details. | |
|
||||||
|
| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but is only use for readability purpose and documentation. | |
|
||||||
|
|
||||||
|
|
||||||
|
##### Input Arguments
|
||||||
|
|
||||||
|
###### 1. Input types
|
||||||
|
|
||||||
|
Input arguments defines what data from the HTTP request the method needs. Aicra is able to extract 3 types of data :
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **JSON** - data send inside the body as a json object ; each key being a variable name, each value its content. Note that the HTTP header '**Content-Type**' must be set to `application/json` for the API to use it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###### 2. Global Format
|
||||||
|
|
||||||
|
The `in` field in each method contains as list of arguments where the key is the argument name, and the value defines how to manage the variable.
|
||||||
|
|
||||||
|
> Variable names from **URI** or **Query** must be named accordingly :
|
||||||
>
|
>
|
||||||
> I've had a hard time deciding and testing different formats including yaml and xml.
|
> - the **URI** variable `{id}` from your request route must be named `{id}`.
|
||||||
> But as it describes our entire api and is crucial for our server to keep working over updates; xml would have been too verbose with growth and yaml on the other side would have been too difficult to read. Json sits in the right spot for this.
|
> - the variable `somevar` in the **Query** has to be names `GET@somevar`.
|
||||||
|
|
||||||
Let's take a quick look at the configuration format !
|
**Example**
|
||||||
|
|
||||||
> if you don't like boring explanations and prefer a working example, see [here](https://git.xdrm.io/go/articles-api/src/master/api.json)
|
In this example we want 3 arguments :
|
||||||
|
|
||||||
### Services
|
- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler.
|
||||||
|
- the 2^nd^ one is send in the query (_e.g. [http://host/uri?get-var=value](http://host/uri?get-var=value)_). It must be a valid `string` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `title`.
|
||||||
|
- the 3^rd^ can be send with a **JSON** body, in **multipart** or **URL encoded** it makes no difference and only give clients a choice over the technology to use. If not renamed, the variable will be given to the handler with the name `content`.
|
||||||
|
|
||||||
To begin with, the configuration file defines a list of services. Each one is defined by:
|
|
||||||
- `method` an HTTP method
|
|
||||||
- `path` an uri pattern (can contain variables)
|
|
||||||
- `info` a short description of what it does
|
|
||||||
- `scope` a list of the required permissions
|
|
||||||
- `in` a list of input arguments
|
|
||||||
- `out` a list of output arguments
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "PUT",
|
||||||
"path": "/article",
|
"path": "/article/{id}",
|
||||||
"scope": [["author", "reader"], ["admin"]],
|
"scope": [["author"]],
|
||||||
"info": "returns all available articles",
|
"info": "updates an article",
|
||||||
"in": {},
|
"in": {
|
||||||
"out": {}
|
"{id}": { "info": "article id", "type": "int", "name": "article_id" },
|
||||||
}
|
"GET@title": { "info": "new article title", "type": "?string", "name": "title" },
|
||||||
|
"content": { "info": "new article content", "type": "string" }
|
||||||
|
},
|
||||||
|
"out": {
|
||||||
|
"id": { "info": "updated article id", "type": "uint" },
|
||||||
|
"title": { "info": "updated article title", "type": "string" },
|
||||||
|
"content": { "info": "updated article content", "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
The `scope` is a 2-dimensional list of permissions. The first list means **or**, the second means **and**, it allows for complex permission combinations. The example above can be translated to: this method requires users to have permissions (author **and** reader) **or** (admin)
|
|
||||||
|
|
||||||
### Input and output parameters
|
|
||||||
|
|
||||||
Input and output parameters share the same format, featuring:
|
|
||||||
- `info` a short description of what it is
|
|
||||||
- `type` its data type (_c.f. validation_)
|
|
||||||
- `?` whether it is mandatory or optional
|
|
||||||
- `name` a custom name for easy access in code
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/article/{id}",
|
|
||||||
"scope": [["author"]],
|
|
||||||
"info": "updates an article",
|
|
||||||
"in": {
|
|
||||||
"{id}": { "info": "...", "type": "int", "name": "id" },
|
|
||||||
"GET@title": { "info": "...", "type": "?string", "name": "title" },
|
|
||||||
"content": { "info": "...", "type": "string" }
|
|
||||||
},
|
|
||||||
"out": {
|
|
||||||
"title": { "info": "updated article title", "type": "string" },
|
|
||||||
"content": { "info": "updated article content", "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
If a parameter is optional you just have to prefix its type with a question mark, by default all parameters are mandatory.
|
|
||||||
|
|
||||||
The format of the key of input arguments defines where it comes from:
|
|
||||||
1. `{param}` is an URI parameter that is extracted from the `"path"`
|
|
||||||
2. `GET@param` is an URL parameter that is extracted from the [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax.
|
|
||||||
3. `param` is a body parameter that can be extracted from 3 formats independently:
|
|
||||||
- _url encoded_: data send in the body following the [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax.
|
|
||||||
- _multipart_: data send in the body with a dedicated [format](https://tools.ietf.org/html/rfc2388#section-3). This format can be quite heavy but allows to transmit data as well as files.
|
|
||||||
- _JSON_: data sent in the body as a json object ; The _Content-Type_ header must be `application/json` for it to work.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/article/{id}",
|
|
||||||
"scope": [["author"]],
|
|
||||||
"info": "updates an article",
|
|
||||||
"in": {
|
|
||||||
"{id}": { "info": "...", "type": "int", "name": "id" },
|
|
||||||
"GET@title": { "info": "...", "type": "?string", "name": "title" },
|
|
||||||
"content": { "info": "...", "type": "string" }
|
|
||||||
},
|
|
||||||
"out": {
|
|
||||||
"id": { "info": "updated article id", "type": "uint" },
|
|
||||||
"title": { "info": "updated article title", "type": "string" },
|
|
||||||
"content": { "info": "updated article content", "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
1. `{id}` is extracted from the end of the URI and is a number compliant with the `int` type checker. It is renamed `ID`, this new name will be sent to the handler.
|
|
||||||
2. `GET@title` is extracted from 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`.
|
|
||||||
3. `content` can be extracted from json, multipart or url-encoded data; 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 its original name `content`.
|
|
||||||
|
|
||||||
|
|
||||||
|
### III/ Change Log
|
||||||
## Writing your code
|
|
||||||
|
|
||||||
Besides your main package where you launch your server, you will need to create handlers matching services from the configuration.
|
|
||||||
|
|
||||||
The code below implements a simple handler.
|
|
||||||
```go
|
|
||||||
// "in": {
|
|
||||||
// "Input1": { "info": "...", "type": "int" },
|
|
||||||
// "Input2": { "info": "...", "type": "?string" }
|
|
||||||
// },
|
|
||||||
type req struct{
|
|
||||||
Input1 int
|
|
||||||
Input2 *string // optional are pointers
|
|
||||||
}
|
|
||||||
// "out": {
|
|
||||||
// "Output1": { "info": "...", "type": "string" },
|
|
||||||
// "Output2": { "info": "...", "type": "bool" }
|
|
||||||
// }
|
|
||||||
type res struct{
|
|
||||||
Output1 string
|
|
||||||
Output2 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func myHandler(ctx context.Context, r req) (*res, api.Err) {
|
|
||||||
err := doSomething()
|
|
||||||
if err != nil {
|
|
||||||
return nil, api.ErrFailure
|
|
||||||
}
|
|
||||||
return &res{"out1", true}, api.ErrSuccess
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If your handler signature does not match the configuration exactly, the server will print out the error and won't start.
|
|
||||||
|
|
||||||
The `api.Err` type automatically maps to HTTP status codes and error descriptions that will be sent to the client as json; clients have to manage the same format for every response.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"reason": "all right"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
- [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:`*)
|
||||||
- [x] nested URL arguments (*i.e. `/user/{id}` and `/user/{uid}/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
|
||||||
- [ ] add support for PATCH method
|
- [x] manage URL, query and body arguments:
|
||||||
- [ ] add support for OPTIONS method
|
- [x] multipart/form-data (variables and file uploads)
|
||||||
- [ ] it might be interesting to generate the list of allowed methods from the configuration
|
- [x] application/x-www-form-urlencoded
|
||||||
- [ ] add CORS support
|
- [x] application/json
|
||||||
- [x] manage request data extraction:
|
|
||||||
- [x] URL slash-separated strings
|
|
||||||
- [x] HTTP Query named parameters
|
|
||||||
- [x] manage array format
|
|
||||||
- [x] body parameters
|
|
||||||
- [x] multipart/form-data (variables and file uploads)
|
|
||||||
- [x] application/x-www-form-urlencoded
|
|
||||||
- [x] application/json
|
|
||||||
- [x] required vs. optional parameters with a default value
|
- [x] required vs. optional parameters with a default value
|
||||||
- [x] parameter renaming
|
- [x] parameter renaming
|
||||||
- [x] generic type check (*i.e. you can add custom types alongside built-in ones*)
|
- [x] generic type check (*i.e. implement custom types alongside built-in ones*)
|
||||||
- [x] built-in types
|
- [ ] built-in types
|
||||||
- [x] `any` - matches any value
|
- [x] `any` - wildcard matching all values
|
||||||
- [x] `int` - see go types
|
- [x] `int` - see go types
|
||||||
- [x] `uint` - see go types
|
- [x] `uint` - see go types
|
||||||
- [x] `float` - see go types
|
- [x] `float` - see go types
|
||||||
- [x] `string` - any text
|
- [x] `string` - any text
|
||||||
- [x] `string(len)` - any string with a length of exactly `len` characters
|
- [x] `string(min, max)` - any string with a length between `min` and `max`
|
||||||
- [x] `string(min, max)` - any string with a length between `min` and `max`
|
- [ ] `[a]` - array containing **only** elements matching `a` type
|
||||||
- [ ] `[]a` - array containing **only** elements matching `a` type
|
- [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
|
||||||
- [ ] `a[b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
|
- [x] generic controllers implementation (shared objects)
|
||||||
- [x] generic handler implementation
|
|
||||||
- [x] response interface
|
- [x] response interface
|
||||||
- [x] generic errors that automatically formats into response
|
- [x] log bound resources when building the aicra server
|
||||||
- [x] builtin errors
|
- [x] fail on check for unimplemented resources at server boot.
|
||||||
- [x] possibility to add custom errors
|
- [x] fail on check for unavailable types in api.json at server boot.
|
||||||
- [x] check for missing handlers when building the handler
|
|
||||||
- [x] check handlers not matching a route in the configuration at server boot
|
|
||||||
- [x] specific configuration format errors qt server boot
|
|
||||||
- [x] statically typed handlers - avoids having to check every input and its type (_which is used by context.Context for instance_)
|
|
||||||
- [x] using reflection to use structs as input and output arguments to match the configuration
|
|
||||||
- [x] check for input and output arguments structs at server boot
|
|
||||||
- [x] check for unavailable types in configuration at server boot
|
|
||||||
- [x] recover panics from handlers
|
|
||||||
- [ ] improve tests and coverage
|
|
||||||
|
|
||||||
|
|
62
api/auth.go
62
api/auth.go
|
@ -1,62 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
// Auth can be used by http middleware to
|
|
||||||
// 1) consult required roles in @Auth.Required
|
|
||||||
// 2) update active roles in @Auth.Active
|
|
||||||
type Auth struct {
|
|
||||||
// required roles for this request
|
|
||||||
// - the first dimension of the array reads as a OR
|
|
||||||
// - the second dimension reads as a AND
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
// [ [A, B], [C, D] ] reads: roles (A and B) or (C and D) are required
|
|
||||||
//
|
|
||||||
// Warning: must not be mutated
|
|
||||||
Required [][]string
|
|
||||||
|
|
||||||
// active roles to be updated by authentication
|
|
||||||
// procedures (e.g. jwt)
|
|
||||||
Active []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Granted returns whether the authorization is granted
|
|
||||||
// i.e. Auth.Active fulfills Auth.Required
|
|
||||||
func (a *Auth) Granted() bool {
|
|
||||||
var nothingRequired = true
|
|
||||||
|
|
||||||
// first dimension: OR ; at least one is valid
|
|
||||||
for _, required := range a.Required {
|
|
||||||
// empty list
|
|
||||||
if len(required) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
nothingRequired = false
|
|
||||||
|
|
||||||
// second dimension: AND ; all required must be fulfilled
|
|
||||||
if a.fulfills(required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nothingRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns whether Auth.Active fulfills (contains) all @required roles
|
|
||||||
func (a *Auth) fulfills(required []string) bool {
|
|
||||||
for _, requiredRole := range required {
|
|
||||||
var found = false
|
|
||||||
for _, activeRole := range a.Active {
|
|
||||||
if activeRole == requiredRole {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// missing role -> fail
|
|
||||||
if !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// all @required are fulfilled
|
|
||||||
return true
|
|
||||||
}
|
|
114
api/auth_test.go
114
api/auth_test.go
|
@ -1,114 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCombination(t *testing.T) {
|
|
||||||
tcases := []struct {
|
|
||||||
Name string
|
|
||||||
Required [][]string
|
|
||||||
Active []string
|
|
||||||
Granted bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "no requirement none given",
|
|
||||||
Required: [][]string{},
|
|
||||||
Active: []string{},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "empty requirements none given",
|
|
||||||
Required: [][]string{{}},
|
|
||||||
Active: []string{},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "no requirement 1 given",
|
|
||||||
Required: [][]string{},
|
|
||||||
Active: []string{"a"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "no requirement some given",
|
|
||||||
Required: [][]string{},
|
|
||||||
Active: []string{"a", "b"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
Name: "1 required none given",
|
|
||||||
Required: [][]string{{"a"}},
|
|
||||||
Active: []string{},
|
|
||||||
Granted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required fulfilled",
|
|
||||||
Required: [][]string{{"a"}},
|
|
||||||
Active: []string{"a"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required mismatch",
|
|
||||||
Required: [][]string{{"a"}},
|
|
||||||
Active: []string{"b"},
|
|
||||||
Granted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 required none gien",
|
|
||||||
Required: [][]string{{"a", "b"}},
|
|
||||||
Active: []string{},
|
|
||||||
Granted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 required other given",
|
|
||||||
Required: [][]string{{"a", "b"}},
|
|
||||||
Active: []string{"c"},
|
|
||||||
Granted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 required one given",
|
|
||||||
Required: [][]string{{"a", "b"}},
|
|
||||||
Active: []string{"a"},
|
|
||||||
Granted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 required fulfilled",
|
|
||||||
Required: [][]string{{"a", "b"}},
|
|
||||||
Active: []string{"a", "b"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 or 2 required first fulfilled",
|
|
||||||
Required: [][]string{{"a", "b"}, {"c", "d"}},
|
|
||||||
Active: []string{"a", "b"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 or 2 required second fulfilled",
|
|
||||||
Required: [][]string{{"a", "b"}, {"c", "d"}},
|
|
||||||
Active: []string{"c", "d"},
|
|
||||||
Granted: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tcase := range tcases {
|
|
||||||
t.Run(tcase.Name, func(t *testing.T) {
|
|
||||||
|
|
||||||
auth := Auth{
|
|
||||||
Required: tcase.Required,
|
|
||||||
Active: tcase.Active,
|
|
||||||
}
|
|
||||||
|
|
||||||
// all right
|
|
||||||
if tcase.Granted == auth.Granted() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tcase.Granted && !auth.Granted() {
|
|
||||||
t.Fatalf("expected granted authorization")
|
|
||||||
}
|
|
||||||
t.Fatalf("unexpected granted authorization")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/internal/ctx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetRequest extracts the current request from a context.Context
|
|
||||||
func GetRequest(c context.Context) *http.Request {
|
|
||||||
var (
|
|
||||||
raw = c.Value(ctx.Request)
|
|
||||||
cast, ok = raw.(*http.Request)
|
|
||||||
)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cast
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResponseWriter extracts the response writer from a context.Context
|
|
||||||
func GetResponseWriter(c context.Context) http.ResponseWriter {
|
|
||||||
var (
|
|
||||||
raw = c.Value(ctx.Response)
|
|
||||||
cast, ok = raw.(http.ResponseWriter)
|
|
||||||
)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cast
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuth returns the api.Auth associated with this request from a context.Context
|
|
||||||
func GetAuth(c context.Context) *Auth {
|
|
||||||
var (
|
|
||||||
raw = c.Value(ctx.Auth)
|
|
||||||
cast, ok = raw.(*Auth)
|
|
||||||
)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cast
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package api_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/ctx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContextGetRequest(t *testing.T) {
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "/random", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot create http request: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Request, req)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetRequest(c)
|
|
||||||
if fetched != req {
|
|
||||||
t.Fatalf("fetched http request %v ; expected %v", fetched, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestContextGetNilRequest(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetRequest(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched http request %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetResponseWriter(t *testing.T) {
|
|
||||||
res := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Response, res)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetResponseWriter(c)
|
|
||||||
if fetched != res {
|
|
||||||
t.Fatalf("fetched http response writer %v ; expected %v", fetched, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetNilResponseWriter(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetResponseWriter(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched http response writer %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetAuth(t *testing.T) {
|
|
||||||
auth := &api.Auth{}
|
|
||||||
|
|
||||||
// store in bare context
|
|
||||||
c := context.Background()
|
|
||||||
c = context.WithValue(c, ctx.Auth, auth)
|
|
||||||
|
|
||||||
// fetch from context
|
|
||||||
fetched := api.GetAuth(c)
|
|
||||||
if fetched != auth {
|
|
||||||
t.Fatalf("fetched api auth %v ; expected %v", fetched, auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContextGetNilAuth(t *testing.T) {
|
|
||||||
// fetch from bare context
|
|
||||||
fetched := api.GetAuth(context.Background())
|
|
||||||
if fetched != nil {
|
|
||||||
t.Fatalf("fetched api auth %v from empty context; expected nil", fetched)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +1,94 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrUnknown 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
|
||||||
ErrUnknown = Err{-1, "unknown error", http.StatusOK}
|
ErrorUnknown Error = -1
|
||||||
|
|
||||||
// ErrSuccess represents a generic successful service execution
|
// ErrorSuccess represents a generic successful service execution
|
||||||
ErrSuccess = Err{0, "all right", http.StatusOK}
|
ErrorSuccess Error = 0
|
||||||
|
|
||||||
// ErrFailure is the most generic error
|
// ErrorFailure is the most generic error
|
||||||
ErrFailure = Err{1, "it failed", http.StatusInternalServerError}
|
ErrorFailure Error = 1
|
||||||
|
|
||||||
// ErrNoMatchFound is 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
|
||||||
ErrNoMatchFound = Err{2, "resource not found", http.StatusOK}
|
ErrorNoMatchFound Error = 2
|
||||||
|
|
||||||
// ErrAlreadyExists is 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
|
||||||
ErrAlreadyExists = Err{3, "already exists", http.StatusOK}
|
ErrorAlreadyExists Error = 3
|
||||||
|
|
||||||
// ErrCreation is set when there is a creation/insert error
|
// ErrorConfig has to be set when there is a configuration error
|
||||||
ErrCreation = Err{4, "create error", http.StatusOK}
|
ErrorConfig Error = 4
|
||||||
|
|
||||||
// ErrModification is set when there is an update/modification error
|
// ErrorUpload has to be set when a file upload failed
|
||||||
ErrModification = Err{5, "update error", http.StatusOK}
|
ErrorUpload Error = 100
|
||||||
|
|
||||||
// ErrDeletion is set when there is a deletion/removal error
|
// ErrorDownload has to be set when a file download failed
|
||||||
ErrDeletion = Err{6, "delete error", http.StatusOK}
|
ErrorDownload Error = 101
|
||||||
|
|
||||||
// ErrTransaction is set when there is a transactional error
|
// MissingDownloadHeaders has to be set when the implementation
|
||||||
ErrTransaction = Err{7, "transactional error", http.StatusOK}
|
|
||||||
|
|
||||||
// ErrUpload is set when a file upload failed
|
|
||||||
ErrUpload = Err{100, "upload failed", http.StatusInternalServerError}
|
|
||||||
|
|
||||||
// ErrDownload is set when a file download failed
|
|
||||||
ErrDownload = Err{101, "download failed", http.StatusInternalServerError}
|
|
||||||
|
|
||||||
// MissingDownloadHeaders is 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 = Err{102, "download headers are missing", http.StatusBadRequest}
|
MissingDownloadHeaders Error = 102
|
||||||
|
|
||||||
// ErrMissingDownloadBody is 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
|
||||||
ErrMissingDownloadBody = Err{103, "download body is missing", http.StatusBadRequest}
|
ErrorMissingDownloadBody Error = 103
|
||||||
|
|
||||||
// ErrUnknownService 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.
|
||||||
ErrUnknownService = Err{200, "unknown service", http.StatusServiceUnavailable}
|
ErrorUnknownService Error = 200
|
||||||
|
|
||||||
// ErrUncallableService 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
|
||||||
ErrUncallableService = Err{202, "uncallable service", http.StatusServiceUnavailable}
|
ErrorUncallableService Error = 202
|
||||||
|
|
||||||
// ErrNotImplemented is set when a handler is not implemented yet
|
// ErrorNotImplemented is set when a handler is not implemented yet
|
||||||
ErrNotImplemented = Err{203, "not implemented", http.StatusNotImplemented}
|
ErrorNotImplemented Error = 203
|
||||||
|
|
||||||
// ErrPermission 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
|
||||||
ErrPermission = Err{300, "permission error", http.StatusUnauthorized}
|
ErrorPermission Error = 300
|
||||||
|
|
||||||
// ErrToken is 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
|
||||||
ErrToken = Err{301, "token error", http.StatusForbidden}
|
ErrorToken Error = 301
|
||||||
|
|
||||||
// ErrMissingParam 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
|
||||||
ErrMissingParam = Err{400, "missing parameter", http.StatusBadRequest}
|
ErrorMissingParam Error = 400
|
||||||
|
|
||||||
// ErrInvalidParam 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.
|
||||||
ErrInvalidParam = Err{401, "invalid parameter", http.StatusBadRequest}
|
ErrorInvalidParam Error = 401
|
||||||
|
|
||||||
// ErrInvalidDefaultParam 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.
|
||||||
ErrInvalidDefaultParam = Err{402, "invalid default param", http.StatusBadRequest}
|
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",
|
||||||
|
}
|
||||||
|
|
41
api/error.go
41
api/error.go
|
@ -1,21 +1,42 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Err 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 Err struct {
|
type Error int
|
||||||
// error code (unique)
|
|
||||||
Code int `json:"code"`
|
// Error implements the error interface
|
||||||
// error small description
|
func (e Error) Error() string {
|
||||||
Reason string `json:"reason"`
|
// use unknown error if no reason
|
||||||
// associated HTTP status
|
reason, ok := errorReasons[e]
|
||||||
Status int `json:"-"`
|
if !ok {
|
||||||
|
return ErrorUnknown.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%d] %s", e, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Err) Error() string {
|
// MarshalJSON implements encoding/json.Marshaler interface
|
||||||
return fmt.Sprintf("[%d] %s", e.Code, e.Reason)
|
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"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}{
|
||||||
|
Code: int(e),
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(formatted)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request represents an API request i.e. HTTP
|
||||||
|
type Request struct {
|
||||||
|
// corresponds to the list of uri components
|
||||||
|
// featured in the request URI
|
||||||
|
URI []string
|
||||||
|
|
||||||
|
// Scope from the configuration file of the current service
|
||||||
|
Scope [][]string
|
||||||
|
|
||||||
|
// original HTTP request
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
// input parameters
|
||||||
|
Param RequestParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest builds an interface request from a http.Request
|
||||||
|
func NewRequest(req *http.Request) (*Request, error) {
|
||||||
|
|
||||||
|
// 1. get useful data
|
||||||
|
uri := normaliseURI(req.URL.Path)
|
||||||
|
uriparts := strings.Split(uri, "/")
|
||||||
|
|
||||||
|
// 3. Init request
|
||||||
|
inst := &Request{
|
||||||
|
URI: uriparts,
|
||||||
|
Scope: nil,
|
||||||
|
Request: req,
|
||||||
|
Param: make(RequestParam),
|
||||||
|
}
|
||||||
|
|
||||||
|
return inst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaliseURI removes the trailing '/' to always
|
||||||
|
// have the same Uri format for later processing
|
||||||
|
func normaliseURI(uri string) string {
|
||||||
|
|
||||||
|
if len(uri) < 1 {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
if uri[0] == '/' {
|
||||||
|
uri = uri[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uri) > 1 && uri[len(uri)-1] == '/' {
|
||||||
|
uri = uri[0 : len(uri)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrReqParamNotFound is thrown when a request parameter is not found
|
||||||
|
const ErrReqParamNotFound = cerr("request parameter not found")
|
||||||
|
|
||||||
|
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
||||||
|
const ErrReqParamNotType = cerr("request parameter does not fulfills type")
|
||||||
|
|
||||||
|
// RequestParam defines input parameters of an api request
|
||||||
|
type RequestParam map[string]interface{}
|
||||||
|
|
||||||
|
// Get returns the raw value (not typed) and an error if not found
|
||||||
|
func (rp RequestParam) Get(key string) (interface{}, error) {
|
||||||
|
rawValue, found := rp[key]
|
||||||
|
if !found {
|
||||||
|
return "", ErrReqParamNotFound
|
||||||
|
}
|
||||||
|
return rawValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns a string and an error if not found or invalid type
|
||||||
|
func (rp RequestParam) GetString(key string) (string, error) {
|
||||||
|
rawValue, err := rp.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cast := rawValue.(type) {
|
||||||
|
case fmt.Stringer:
|
||||||
|
return cast.String(), nil
|
||||||
|
case []byte:
|
||||||
|
return string(cast), nil
|
||||||
|
case string:
|
||||||
|
return cast, nil
|
||||||
|
default:
|
||||||
|
return "", ErrReqParamNotType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat returns a float64 and an error if not found or invalid type
|
||||||
|
func (rp RequestParam) GetFloat(key string) (float64, error) {
|
||||||
|
rawValue, err := rp.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cast := rawValue.(type) {
|
||||||
|
case float32:
|
||||||
|
return float64(cast), nil
|
||||||
|
case float64:
|
||||||
|
return cast, nil
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
intVal, ok := cast.(int)
|
||||||
|
if !ok || intVal != int(float64(intVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return float64(intVal), nil
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
uintVal, ok := cast.(uint)
|
||||||
|
if !ok || uintVal != uint(float64(uintVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return float64(uintVal), nil
|
||||||
|
default:
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt returns an int and an error if not found or invalid type
|
||||||
|
func (rp RequestParam) GetInt(key string) (int, error) {
|
||||||
|
rawValue, err := rp.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cast := rawValue.(type) {
|
||||||
|
case float32, float64:
|
||||||
|
floatVal, ok := cast.(float64)
|
||||||
|
if !ok || floatVal < 0 || floatVal != float64(int(floatVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return int(floatVal), nil
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
intVal, ok := cast.(int)
|
||||||
|
if !ok || intVal != int(int(intVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return int(intVal), nil
|
||||||
|
default:
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUint returns an uint and an error if not found or invalid type
|
||||||
|
func (rp RequestParam) GetUint(key string) (uint, error) {
|
||||||
|
rawValue, err := rp.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cast := rawValue.(type) {
|
||||||
|
case float32, float64:
|
||||||
|
floatVal, ok := cast.(float64)
|
||||||
|
if !ok || floatVal < 0 || floatVal != float64(uint(floatVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return uint(floatVal), nil
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
intVal, ok := cast.(int)
|
||||||
|
if !ok || intVal != int(uint(intVal)) {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return uint(intVal), nil
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
uintVal, ok := cast.(uint)
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
return uintVal, nil
|
||||||
|
default:
|
||||||
|
return 0, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrings returns an []slice and an error if not found or invalid type
|
||||||
|
func (rp RequestParam) GetStrings(key string) ([]string, error) {
|
||||||
|
rawValue, err := rp.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cast := rawValue.(type) {
|
||||||
|
case []fmt.Stringer:
|
||||||
|
strings := make([]string, len(cast))
|
||||||
|
for i, stringer := range cast {
|
||||||
|
strings[i] = stringer.String()
|
||||||
|
}
|
||||||
|
return strings, nil
|
||||||
|
case [][]byte:
|
||||||
|
strings := make([]string, len(cast))
|
||||||
|
for i, bytes := range cast {
|
||||||
|
strings[i] = string(bytes)
|
||||||
|
}
|
||||||
|
return strings, nil
|
||||||
|
case []string:
|
||||||
|
return cast, nil
|
||||||
|
default:
|
||||||
|
return nil, ErrReqParamNotType
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseData defines format for response parameters to return
|
||||||
|
type ResponseData map[string]interface{}
|
||||||
|
|
||||||
|
// Response represents an API response to be sent
|
||||||
|
type Response struct {
|
||||||
|
Data ResponseData
|
||||||
|
Status int
|
||||||
|
Headers http.Header
|
||||||
|
err Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse creates an empty response.
|
||||||
|
func EmptyResponse() *Response {
|
||||||
|
return &Response{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: make(ResponseData),
|
||||||
|
err: ErrorFailure,
|
||||||
|
Headers: make(http.Header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithError sets the error from a base error with error arguments.
|
||||||
|
func (res *Response) WithError(err Error) *Response {
|
||||||
|
res.err = err
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface and dispatches to internal error.
|
||||||
|
func (res *Response) Error() string {
|
||||||
|
return res.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData adds/overrides a new response field
|
||||||
|
func (res *Response) SetData(name string, value interface{}) {
|
||||||
|
res.Data[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData gets a response field
|
||||||
|
func (res *Response) GetData(name string) interface{} {
|
||||||
|
value, _ := res.Data[name]
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the 'json.Marshaler' interface and is used
|
||||||
|
// to generate the JSON representation of the response
|
||||||
|
func (res *Response) MarshalJSON() ([]byte, error) {
|
||||||
|
fmt := make(map[string]interface{})
|
||||||
|
|
||||||
|
for k, v := range res.Data {
|
||||||
|
fmt[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt["error"] = res.err
|
||||||
|
|
||||||
|
return json.Marshal(fmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler and writes the API response.
|
||||||
|
func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
w.WriteHeader(res.Status)
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Write(encoded)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
157
builder.go
157
builder.go
|
@ -1,157 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
|
||||||
"github.com/xdrm-io/aicra/internal/dynfunc"
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Builder for an aicra server
|
|
||||||
type Builder struct {
|
|
||||||
// the server configuration defining available services
|
|
||||||
conf *config.Server
|
|
||||||
// user-defined handlers bound to services from the configuration
|
|
||||||
handlers []*apiHandler
|
|
||||||
// http middlewares wrapping the entire http connection (e.g. logger)
|
|
||||||
middlewares []func(http.Handler) http.Handler
|
|
||||||
// custom middlewares only wrapping the service handler of a request
|
|
||||||
// they will benefit from the request's context that contains service-specific
|
|
||||||
// information (e.g. required permissions from the configuration)
|
|
||||||
ctxMiddlewares []func(http.Handler) http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// represents an api handler (method-pattern combination)
|
|
||||||
type apiHandler struct {
|
|
||||||
Method string
|
|
||||||
Path string
|
|
||||||
dyn *dynfunc.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate adds an available Type to validate in the api definition
|
|
||||||
func (b *Builder) Validate(t validator.Type) error {
|
|
||||||
if b.conf == nil {
|
|
||||||
b.conf = &config.Server{}
|
|
||||||
}
|
|
||||||
if b.conf.Services != nil {
|
|
||||||
return errLateType
|
|
||||||
}
|
|
||||||
if b.conf.Validators == nil {
|
|
||||||
b.conf.Validators = make([]validator.Type, 0)
|
|
||||||
}
|
|
||||||
b.conf.Validators = append(b.conf.Validators, t)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// With adds an http middleware on top of the http connection
|
|
||||||
//
|
|
||||||
// Authentication management can only be done with the WithContext() methods as
|
|
||||||
// the service associated with the request has not been found at this stage.
|
|
||||||
// This stage is perfect for logging or generic request management.
|
|
||||||
func (b *Builder) With(mw func(http.Handler) http.Handler) {
|
|
||||||
if b.middlewares == nil {
|
|
||||||
b.middlewares = make([]func(http.Handler) http.Handler, 0)
|
|
||||||
}
|
|
||||||
b.middlewares = append(b.middlewares, mw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContext adds an http middleware with the fully loaded context
|
|
||||||
//
|
|
||||||
// Logging or generic request management should be done with the With() method as
|
|
||||||
// it wraps the full http connection. Middlewares added through this method only
|
|
||||||
// wrap the user-defined service handler. The context.Context is filled with useful
|
|
||||||
// data that can be access with api.GetRequest(), api.GetResponseWriter(),
|
|
||||||
// api.GetAuth(), etc methods.
|
|
||||||
func (b *Builder) WithContext(mw func(http.Handler) http.Handler) {
|
|
||||||
if b.ctxMiddlewares == nil {
|
|
||||||
b.ctxMiddlewares = make([]func(http.Handler) http.Handler, 0)
|
|
||||||
}
|
|
||||||
b.ctxMiddlewares = append(b.ctxMiddlewares, mw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the builder with its api definition file
|
|
||||||
// panics if already setup
|
|
||||||
func (b *Builder) Setup(r io.Reader) error {
|
|
||||||
if b.conf == nil {
|
|
||||||
b.conf = &config.Server{}
|
|
||||||
}
|
|
||||||
if b.conf.Services != nil {
|
|
||||||
return errAlreadySetup
|
|
||||||
}
|
|
||||||
return b.conf.Parse(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind a dynamic handler to a REST service (method and pattern)
|
|
||||||
func (b *Builder) Bind(method, path string, fn interface{}) error {
|
|
||||||
if b.conf == nil || b.conf.Services == nil {
|
|
||||||
return errNotSetup
|
|
||||||
}
|
|
||||||
|
|
||||||
// find associated service from config
|
|
||||||
var service *config.Service
|
|
||||||
for _, s := range b.conf.Services {
|
|
||||||
if method == s.Method && path == s.Pattern {
|
|
||||||
service = s
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if service == nil {
|
|
||||||
return fmt.Errorf("%s '%s': %w", method, path, errUnknownService)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dyn, err = dynfunc.Build(fn, *service)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s '%s' handler: %w", method, path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.handlers = append(b.handlers, &apiHandler{
|
|
||||||
Path: path,
|
|
||||||
Method: method,
|
|
||||||
dyn: dyn,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get is equivalent to Bind(http.MethodGet)
|
|
||||||
func (b *Builder) Get(path string, fn interface{}) error {
|
|
||||||
return b.Bind(http.MethodGet, path, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post is equivalent to Bind(http.MethodPost)
|
|
||||||
func (b *Builder) Post(path string, fn interface{}) error {
|
|
||||||
return b.Bind(http.MethodPost, path, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put is equivalent to Bind(http.MethodPut)
|
|
||||||
func (b *Builder) Put(path string, fn interface{}) error {
|
|
||||||
return b.Bind(http.MethodPut, path, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete is equivalent to Bind(http.MethodDelete)
|
|
||||||
func (b *Builder) Delete(path string, fn interface{}) error {
|
|
||||||
return b.Bind(http.MethodDelete, path, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a fully-featured HTTP server
|
|
||||||
func (b Builder) Build() (http.Handler, error) {
|
|
||||||
|
|
||||||
for _, service := range b.conf.Services {
|
|
||||||
var isHandled bool
|
|
||||||
for _, handler := range b.handlers {
|
|
||||||
if handler.Method == service.Method && handler.Path == service.Pattern {
|
|
||||||
isHandled = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isHandled {
|
|
||||||
return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, errMissingHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Handler(b), nil
|
|
||||||
}
|
|
439
builder_test.go
439
builder_test.go
|
@ -1,439 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/dynfunc"
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
func addBuiltinTypes(b *Builder) error {
|
|
||||||
if err := b.Validate(validator.AnyType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.Validate(validator.BoolType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.Validate(validator.FloatType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.Validate(validator.IntType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.Validate(validator.StringType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.Validate(validator.UintType{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Validate(validator.BoolType{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Validate(validator.FloatType{})
|
|
||||||
if err != errLateType {
|
|
||||||
t.Fatalf("expected <%v> got <%v>", errLateType, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetupNoType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestSetupTwice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
// double Setup() must fail
|
|
||||||
err = builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != errAlreadySetup {
|
|
||||||
t.Fatalf("expected error %v, got %v", errAlreadySetup, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindBeforeSetup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
// binding before Setup() must fail
|
|
||||||
err := builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
if err != errNotSetup {
|
|
||||||
t.Fatalf("expected error %v, got %v", errNotSetup, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindUnknownService(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader("[]"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
if !errors.Is(err, errUnknownService) {
|
|
||||||
t.Fatalf("expected error %v, got %v", errUnknownService, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBindInvalidHandler(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
err = builder.Bind(http.MethodGet, "/path", func() {})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected an error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !errors.Is(err, dynfunc.ErrMissingHandlerContextArgument) {
|
|
||||||
t.Fatalf("expected a dynfunc.Err got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBindGet(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = builder.Get("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Post("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Put("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
err = builder.Delete("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnhandledService(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
err := builder.Setup(strings.NewReader(`[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = builder.Get("/path", func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = builder.Build()
|
|
||||||
if !errors.Is(err, errMissingHandler) {
|
|
||||||
t.Fatalf("expected a %v error, got %v", errMissingHandler, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestBind(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tcases := []struct {
|
|
||||||
Name string
|
|
||||||
Config string
|
|
||||||
HandlerMethod string
|
|
||||||
HandlerPath string
|
|
||||||
HandlerFn interface{} // not bound if nil
|
|
||||||
BindErr error
|
|
||||||
BuildErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "none required none provided",
|
|
||||||
Config: "[]",
|
|
||||||
HandlerMethod: "",
|
|
||||||
HandlerPath: "",
|
|
||||||
HandlerFn: nil,
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "none required 1 provided",
|
|
||||||
Config: "[]",
|
|
||||||
HandlerMethod: "",
|
|
||||||
HandlerPath: "",
|
|
||||||
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: errUnknownService,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required none provided",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: "",
|
|
||||||
HandlerPath: "",
|
|
||||||
HandlerFn: nil,
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: errMissingHandler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required wrong method provided",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodPost,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: errUnknownService,
|
|
||||||
BuildErr: errMissingHandler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required wrong path provided",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/paths",
|
|
||||||
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: errUnknownService,
|
|
||||||
BuildErr: errMissingHandler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required valid provided",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required with int",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "int", "name": "Name" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context, struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required with uint",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "uint", "name": "Name" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context, struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required with string",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "string", "name": "Name" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context, struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "1 required with bool",
|
|
||||||
Config: `[
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"scope": [[]],
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"id": { "info": "info", "type": "bool", "name": "Name" }
|
|
||||||
},
|
|
||||||
"out": {}
|
|
||||||
}
|
|
||||||
]`,
|
|
||||||
HandlerMethod: http.MethodGet,
|
|
||||||
HandlerPath: "/path",
|
|
||||||
HandlerFn: func(context.Context, struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
BindErr: nil,
|
|
||||||
BuildErr: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tcase := range tcases {
|
|
||||||
t.Run(tcase.Name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
builder := &Builder{}
|
|
||||||
|
|
||||||
if err := addBuiltinTypes(builder); err != nil {
|
|
||||||
t.Fatalf("add built-in types: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := builder.Setup(strings.NewReader(tcase.Config))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("setup: unexpected error <%v>", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tcase.HandlerFn != nil {
|
|
||||||
err := builder.Bind(tcase.HandlerMethod, tcase.HandlerPath, tcase.HandlerFn)
|
|
||||||
if !errors.Is(err, tcase.BindErr) {
|
|
||||||
t.Fatalf("bind: expected <%v> got <%v>", tcase.BindErr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = builder.Build()
|
|
||||||
if !errors.Is(err, tcase.BuildErr) {
|
|
||||||
t.Fatalf("build: expected <%v> got <%v>", tcase.BuildErr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAny_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.AnyType{}
|
|
||||||
expected = reflect.TypeOf(interface{}(nil))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAny_AvailableTypes(t *testing.T) {
|
func TestAny_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.AnyType{}
|
dt := builtin.AnyDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,7 +26,7 @@ func TestAny_AvailableTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
|
@ -60,7 +47,7 @@ func TestAny_AlwaysTrue(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "any"
|
const typeName = "any"
|
||||||
|
|
||||||
validator := validator.AnyType{}.Validator(typeName)
|
validator := builtin.AnyDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,24 +1,23 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoolType makes the "bool" type available in the aicra configuration
|
// BoolDataType is what its name tells
|
||||||
// It considers valid:
|
type BoolDataType struct{}
|
||||||
// - booleans
|
|
||||||
// - strings containing "true" or "false"
|
|
||||||
// - []byte containing "true" or "false"
|
|
||||||
type BoolType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `bool` type
|
// Type returns the type of data
|
||||||
func (BoolType) GoType() reflect.Type {
|
func (BoolDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(true)
|
return reflect.TypeOf(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for bool values
|
// Build returns the validator
|
||||||
func (BoolType) Validator(typename string, avail ...Type) ValidateFunc {
|
func (BoolDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if typename != "bool" {
|
// nothing if type not handled
|
||||||
|
if typeName != "bool" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBool_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.BoolType{}
|
|
||||||
expected = reflect.TypeOf(true)
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBool_AvailableTypes(t *testing.T) {
|
func TestBool_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.BoolType{}
|
dt := builtin.BoolDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -39,7 +26,7 @@ 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) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == 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)
|
||||||
|
@ -62,7 +49,7 @@ func TestBool_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "bool"
|
const typeName = "bool"
|
||||||
|
|
||||||
validator := validator.BoolType{}.Validator(typeName)
|
validator := builtin.BoolDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,27 +1,24 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FloatType makes the "float" (or "float64") type available in the aicra configuration
|
// FloatDataType is what its name tells
|
||||||
// It considers valid:
|
type FloatDataType struct{}
|
||||||
// - float64
|
|
||||||
// - int (since it does not overflow)
|
|
||||||
// - uint (since it does not overflow)
|
|
||||||
// - strings containing json-compatible floats
|
|
||||||
// - []byte containing json-compatible floats
|
|
||||||
type FloatType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `float64` type
|
// Type returns the type of data
|
||||||
func (FloatType) GoType() reflect.Type {
|
func (FloatDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(float64(0))
|
return reflect.TypeOf(float64(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for float64 values
|
// Build returns the validator
|
||||||
func (FloatType) Validator(typename string, avail ...Type) ValidateFunc {
|
func (FloatDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if typename != "float64" && typename != "float" {
|
// nothing if type not handled
|
||||||
|
if typeName != "float64" && typeName != "float" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return func(value interface{}) (interface{}, bool) {
|
return func(value interface{}) (interface{}, bool) {
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFloat64_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.FloatType{}
|
|
||||||
expected = reflect.TypeOf(float64(0.0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFloat64_AvailableTypes(t *testing.T) {
|
func TestFloat64_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.FloatType{}
|
dt := builtin.FloatDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -46,7 +33,7 @@ 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) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == 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)
|
||||||
|
@ -69,7 +56,7 @@ func TestFloat64_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "float"
|
const typeName = "float"
|
||||||
|
|
||||||
validator := validator.FloatType{}.Validator(typeName)
|
validator := builtin.FloatDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
|
@ -1,29 +1,25 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntType makes the "int" type available in the aicra configuration
|
// IntDataType is what its name tells
|
||||||
// It considers valid:
|
type IntDataType struct{}
|
||||||
// - int
|
|
||||||
// - float64 (since it does not overflow)
|
|
||||||
// - uint (since it does not overflow)
|
|
||||||
// - strings containing json-compatible integers
|
|
||||||
// - []byte containing json-compatible integers
|
|
||||||
type IntType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `int` type
|
// Type returns the type of data
|
||||||
func (IntType) GoType() reflect.Type {
|
func (IntDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(int(0))
|
return reflect.TypeOf(int(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for int values
|
// Build returns the validator
|
||||||
func (IntType) Validator(typename string, avail ...Type) ValidateFunc {
|
func (IntDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
// nothing if type not handled
|
// nothing if type not handled
|
||||||
if typename != "int" {
|
if typeName != "int" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInt_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.IntType{}
|
|
||||||
expected = reflect.TypeOf(int(0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInt_AvailableTypes(t *testing.T) {
|
func TestInt_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.IntType{}
|
dt := builtin.IntDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,7 +27,7 @@ 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) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == 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)
|
||||||
|
@ -63,7 +50,7 @@ func TestInt_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "int"
|
const typeName = "int"
|
||||||
|
|
||||||
validator := validator.IntType{}.Validator(typeName)
|
validator := builtin.IntDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -84,7 +71,7 @@ func TestInt_Values(t *testing.T) {
|
||||||
{uint(math.MaxInt64 + 1), false},
|
{uint(math.MaxInt64 + 1), false},
|
||||||
|
|
||||||
{float64(math.MinInt64), true},
|
{float64(math.MinInt64), true},
|
||||||
// we cannot just subtract 1 because of how precision works
|
// we cannot just substract 1 because of how precision works
|
||||||
{float64(math.MinInt64 - 1024 - 1), false},
|
{float64(math.MinInt64 - 1024 - 1), false},
|
||||||
|
|
||||||
// WARNING : this is due to how floats are compared
|
// WARNING : this is due to how floats are compared
|
|
@ -1,37 +1,32 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
||||||
fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
var variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
||||||
variableLengthRegex = regexp.MustCompile(`^string\((\d+), ?(\d+)\)$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// StringType makes the types beloz available in the aicra configuration:
|
// StringDataType is what its name tells
|
||||||
// - "string" considers any string valid
|
type StringDataType struct{}
|
||||||
// - "string(n)" considers any string with an exact size of `n` valid
|
|
||||||
// - "string(a,b)" considers any string with a size between `a` and `b` valid
|
|
||||||
// > for the last one, `a` and `b` are included in the valid sizes
|
|
||||||
type StringType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `string` type
|
// Type returns the type of data
|
||||||
func (StringType) GoType() reflect.Type {
|
func (StringDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(string(""))
|
return reflect.TypeOf(string(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for strings with any/fixed/bound sizes
|
// Build returns the validator.
|
||||||
func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
// availables type names are : `string`, `string(length)` and `string(minLength, maxLength)`.
|
||||||
var (
|
func (s StringDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
simple = (typename == "string")
|
simple := typeName == "string"
|
||||||
fixedLengthMatches = fixedLengthRegex.FindStringSubmatch(typename)
|
fixedLengthMatches := fixedLengthRegex.FindStringSubmatch(typeName)
|
||||||
variableLengthMatches = variableLengthRegex.FindStringSubmatch(typename)
|
variableLengthMatches := variableLengthRegex.FindStringSubmatch(typeName)
|
||||||
)
|
|
||||||
|
|
||||||
// ignore unknown typename
|
// nothing if type not handled
|
||||||
if !simple && fixedLengthMatches == nil && variableLengthMatches == nil {
|
if !simple && fixedLengthMatches == nil && variableLengthMatches == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -45,7 +40,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
if fixedLengthMatches != nil {
|
if fixedLengthMatches != nil {
|
||||||
exLen, ok := s.getFixedLength(fixedLengthMatches)
|
exLen, ok := s.getFixedLength(fixedLengthMatches)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
mustFail = true
|
||||||
}
|
}
|
||||||
min = exLen
|
min = exLen
|
||||||
max = exLen
|
max = exLen
|
||||||
|
@ -54,7 +49,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
} else if variableLengthMatches != nil {
|
} else if variableLengthMatches != nil {
|
||||||
exMin, exMax, ok := s.getVariableLength(variableLengthMatches)
|
exMin, exMax, ok := s.getVariableLength(variableLengthMatches)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
mustFail = true
|
||||||
}
|
}
|
||||||
min = exMin
|
min = exMin
|
||||||
max = exMax
|
max = exMax
|
||||||
|
@ -89,7 +84,7 @@ func (s StringType) Validator(typename string, avail ...Type) ValidateFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (StringType) 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
|
||||||
|
@ -105,7 +100,7 @@ func (StringType) 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 (StringType) 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
|
|
@ -1,29 +1,16 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestString_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.StringType{}
|
|
||||||
expected = reflect.TypeOf(string("abc"))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString_AvailableTypes(t *testing.T) {
|
func TestString_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.StringType{}
|
dt := builtin.StringDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -66,7 +53,7 @@ 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) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
|
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
if test.Handled {
|
if test.Handled {
|
||||||
|
@ -88,7 +75,7 @@ func TestString_AnyLength(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "string"
|
const typeName = "string"
|
||||||
|
|
||||||
validator := validator.StringType{}.Validator(typeName)
|
validator := builtin.StringDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -146,7 +133,7 @@ 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) {
|
||||||
validator := validator.StringType{}.Validator(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if validator == 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()
|
||||||
|
@ -207,7 +194,7 @@ 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) {
|
||||||
validator := validator.StringType{}.Validator(test.Type)
|
validator := builtin.StringDataType{}.Build(test.Type)
|
||||||
if validator == 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()
|
|
@ -1,28 +1,25 @@
|
||||||
package validator
|
package builtin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UintType makes the "uint" type available in the aicra configuration
|
// UintDataType is what its name tells
|
||||||
// It considers valid:
|
type UintDataType struct{}
|
||||||
// - uint
|
|
||||||
// - int (since it does not overflow)
|
|
||||||
// - float64 (since it does not overflow)
|
|
||||||
// - strings containing json-compatible integers
|
|
||||||
// - []byte containing json-compatible integers
|
|
||||||
type UintType struct{}
|
|
||||||
|
|
||||||
// GoType returns the `uint` type
|
// Type returns the type of data
|
||||||
func (UintType) GoType() reflect.Type {
|
func (UintDataType) Type() reflect.Type {
|
||||||
return reflect.TypeOf(uint(0))
|
return reflect.TypeOf(uint(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator for uint values
|
// Build returns the validator
|
||||||
func (UintType) Validator(other string, avail ...Type) ValidateFunc {
|
func (UintDataType) Build(typeName string, registry ...datatype.T) datatype.Validator {
|
||||||
if other != "uint" {
|
// nothing if type not handled
|
||||||
|
if typeName != "uint" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,17 @@
|
||||||
package validator_test
|
package builtin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUint_ReflectType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dt = validator.UintType{}
|
|
||||||
expected = reflect.TypeOf(uint(0))
|
|
||||||
)
|
|
||||||
if dt.GoType() != expected {
|
|
||||||
t.Fatalf("invalid GoType() %v ; expected %v", dt.GoType(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUint_AvailableTypes(t *testing.T) {
|
func TestUint_AvailableTypes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dt := validator.UintType{}
|
dt := builtin.UintDataType{}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -40,7 +27,7 @@ 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) {
|
||||||
validator := dt.Validator(test.Type)
|
validator := dt.Build(test.Type)
|
||||||
if validator == 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)
|
||||||
|
@ -63,7 +50,7 @@ func TestUint_Values(t *testing.T) {
|
||||||
|
|
||||||
const typeName = "uint"
|
const typeName = "uint"
|
||||||
|
|
||||||
validator := validator.UintType{}.Validator(typeName)
|
validator := builtin.UintDataType{}.Build(typeName)
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
t.Errorf("expect %q to be handled", typeName)
|
t.Errorf("expect %q to be handled", typeName)
|
||||||
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
|
||||||
|
}
|
18
errors.go
18
errors.go
|
@ -3,21 +3,13 @@ package aicra
|
||||||
// cerr allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type cerr string
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
func (err cerr) Error() string {
|
func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errLateType - cannot add datatype after setting up the definition
|
// ErrNoServiceForHandler - no service matching this handler
|
||||||
const errLateType = cerr("types cannot be added after Setup")
|
const ErrNoServiceForHandler = cerr("no service found for this handler")
|
||||||
|
|
||||||
// errNotSetup - not set up yet
|
// ErrNoHandlerForService - no handler matching this service
|
||||||
const errNotSetup = cerr("not set up")
|
const ErrNoHandlerForService = cerr("no handler found for this service")
|
||||||
|
|
||||||
// errAlreadySetup - already set up
|
|
||||||
const errAlreadySetup = cerr("already set up")
|
|
||||||
|
|
||||||
// errUnknownService - no service matching this handler
|
|
||||||
const errUnknownService = cerr("unknown service")
|
|
||||||
|
|
||||||
// errMissingHandler - missing handler
|
|
||||||
const errMissingHandler = cerr("missing handler")
|
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module github.com/xdrm-io/aicra
|
module git.xdrm.io/go/aicra
|
||||||
|
|
||||||
go 1.16
|
go 1.14
|
||||||
|
|
176
handler.go
176
handler.go
|
@ -1,174 +1,32 @@
|
||||||
package aicra
|
package aicra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
"git.xdrm.io/go/aicra/dynamic"
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
"github.com/xdrm-io/aicra/internal/ctx"
|
|
||||||
"github.com/xdrm-io/aicra/internal/reqdata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler wraps the builder to handle requests
|
type handler struct {
|
||||||
type Handler Builder
|
Method string
|
||||||
|
Path string
|
||||||
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
|
dynHandler *dynamic.Handler
|
||||||
func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var h http.Handler = http.HandlerFunc(s.resolve)
|
|
||||||
|
|
||||||
for _, mw := range s.middlewares {
|
|
||||||
h = mw(h)
|
|
||||||
}
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
|
// createHandler builds a handler from its http method and path
|
||||||
func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
|
// also it checks whether the function signature is valid
|
||||||
// match service from config
|
func createHandler(method, path string, service config.Service, fn dynamic.HandlerFn) (*handler, error) {
|
||||||
var service = s.conf.Find(r)
|
method = strings.ToUpper(method)
|
||||||
if service == nil {
|
|
||||||
newResponse().WithError(api.ErrUnknownService).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract request data
|
dynHandler, err := dynamic.Build(fn, service)
|
||||||
var input, err = extractInput(service, *r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, reqdata.ErrInvalidType) {
|
return nil, fmt.Errorf("%s '%s' handler: %w", method, path, err)
|
||||||
newResponse().WithError(api.ErrInvalidParam).ServeHTTP(w, r)
|
|
||||||
} else {
|
|
||||||
newResponse().WithError(api.ErrMissingParam).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// match handler
|
return &handler{
|
||||||
var handler *apiHandler
|
Path: path,
|
||||||
for _, h := range s.handlers {
|
Method: method,
|
||||||
if h.Method == service.Method && h.Path == service.Pattern {
|
dynHandler: dynHandler,
|
||||||
handler = h
|
}, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no handler found
|
|
||||||
if handler == nil {
|
|
||||||
newResponse().WithError(api.ErrUncallableService).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// add info into context
|
|
||||||
c := r.Context()
|
|
||||||
c = context.WithValue(c, ctx.Request, r)
|
|
||||||
c = context.WithValue(c, ctx.Response, w)
|
|
||||||
c = context.WithValue(c, ctx.Auth, buildAuth(service.Scope, input.Data))
|
|
||||||
|
|
||||||
// create http handler
|
|
||||||
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// should not happen
|
|
||||||
auth := api.GetAuth(r.Context())
|
|
||||||
if auth == nil {
|
|
||||||
newResponse().WithError(api.ErrPermission).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// reject non granted requests
|
|
||||||
if !auth.Granted() {
|
|
||||||
newResponse().WithError(api.ErrPermission).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute the service handler
|
|
||||||
s.handle(r.Context(), input, handler, service, w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// run contextual middlewares
|
|
||||||
for _, mw := range s.ctxMiddlewares {
|
|
||||||
h = mw(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serve using the pre-filled context
|
|
||||||
h.ServeHTTP(w, r.WithContext(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the service request with the associated handler func and respond using
|
|
||||||
// the handler func output
|
|
||||||
func (s *Handler) handle(c context.Context, input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
|
|
||||||
// pass execution to the handler function
|
|
||||||
var outData, outErr = handler.dyn.Handle(c, input.Data)
|
|
||||||
|
|
||||||
// build response from output arguments
|
|
||||||
var res = newResponse().WithError(outErr)
|
|
||||||
for key, value := range outData {
|
|
||||||
|
|
||||||
// find original name from 'rename' field
|
|
||||||
for name, param := range service.Output {
|
|
||||||
if param.Rename == key {
|
|
||||||
res.WithValue(name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write response and close request
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
res.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractInput(service *config.Service, req http.Request) (*reqdata.T, error) {
|
|
||||||
var dataset = reqdata.New(service)
|
|
||||||
|
|
||||||
// URI data
|
|
||||||
var err = dataset.GetURI(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// query data
|
|
||||||
err = dataset.GetQuery(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// form/json data
|
|
||||||
err = dataset.GetForm(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataset, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAuth builds the api.Auth struct from the service scope configuration
|
|
||||||
//
|
|
||||||
// it replaces format '[a]' in scope where 'a' is an existing input argument's
|
|
||||||
// name with its value
|
|
||||||
func buildAuth(scope [][]string, in map[string]interface{}) *api.Auth {
|
|
||||||
updated := make([][]string, len(scope))
|
|
||||||
|
|
||||||
// replace '[arg_name]' with the 'arg_name' value if it is a known variable
|
|
||||||
// name
|
|
||||||
for a, list := range scope {
|
|
||||||
updated[a] = make([]string, len(list))
|
|
||||||
for b, perm := range list {
|
|
||||||
updated[a][b] = perm
|
|
||||||
for name, value := range in {
|
|
||||||
var (
|
|
||||||
token = fmt.Sprintf("[%s]", name)
|
|
||||||
replacement = ""
|
|
||||||
)
|
|
||||||
if value != nil {
|
|
||||||
replacement = fmt.Sprintf("[%v]", value)
|
|
||||||
}
|
|
||||||
updated[a][b] = strings.ReplaceAll(updated[a][b], token, replacement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &api.Auth{
|
|
||||||
Required: updated,
|
|
||||||
Active: []string{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
1138
handler_test.go
1138
handler_test.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,116 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/api"
|
||||||
|
"git.xdrm.io/go/aicra/internal/reqdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpServer wraps the aicra server to allow handling http requests
|
||||||
|
type httpServer Server
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler and has to be called on each request
|
||||||
|
func (server httpServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
// 1. find a matching service in the config
|
||||||
|
service := server.config.Find(req)
|
||||||
|
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 {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. feed request with scope & parameters
|
||||||
|
apireq.Scope = service.Scope
|
||||||
|
apireq.Param = dataset.Data
|
||||||
|
|
||||||
|
// 10. execute
|
||||||
|
returned, apiErr := foundHandler.dynHandler.Handle(dataset.Data)
|
||||||
|
response := api.EmptyResponse().WithError(apiErr)
|
||||||
|
for key, value := range returned {
|
||||||
|
|
||||||
|
// find original name from rename
|
||||||
|
for name, param := range service.Output {
|
||||||
|
if param.Rename == key {
|
||||||
|
response.SetData(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. apply headers
|
||||||
|
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
for key, values := range response.Headers {
|
||||||
|
for _, value := range values {
|
||||||
|
res.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. write to response
|
||||||
|
response.ServeHTTP(res, req)
|
||||||
|
}
|
|
@ -1,182 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Server definition
|
|
||||||
type Server struct {
|
|
||||||
Validators []validator.Type
|
|
||||||
Services []*Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a configuration into a server. Server.Types must be set beforehand to
|
|
||||||
// make datatypes available when checking and formatting the read configuration.
|
|
||||||
func (s *Server) Parse(r io.Reader) error {
|
|
||||||
err := json.NewDecoder(r).Decode(&s.Services)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", ErrRead, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", ErrFormat, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate implements the validator interface
|
|
||||||
func (s Server) validate(datatypes ...validator.Type) error {
|
|
||||||
for _, service := range s.Services {
|
|
||||||
err := service.validate(s.Validators...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.collide(); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", ErrFormat, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a service matching an incoming HTTP request
|
|
||||||
func (s Server) Find(r *http.Request) *Service {
|
|
||||||
for _, service := range s.Services {
|
|
||||||
if matches := service.Match(r); matches {
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// collide returns if there is collision between any service for the same method and colliding paths.
|
|
||||||
// Note that service path collision detection relies on datatypes:
|
|
||||||
// - example 1: `/user/{id}` and `/user/articles` will not collide as {id} is an int and "articles" is not
|
|
||||||
// - example 2: `/user/{name}` and `/user/articles` will collide as {name} is a string so as "articles"
|
|
||||||
// - example 3: `/user/{name}` and `/user/{id}` will collide as {name} and {id} cannot be checked against their potential values
|
|
||||||
func (s *Server) collide() error {
|
|
||||||
length := len(s.Services)
|
|
||||||
|
|
||||||
// for each service combination
|
|
||||||
for a := 0; a < length; a++ {
|
|
||||||
for b := a + 1; b < length; b++ {
|
|
||||||
aService := s.Services[a]
|
|
||||||
bService := s.Services[b]
|
|
||||||
|
|
||||||
if aService.Method != bService.Method {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aURIParts := SplitURL(aService.Pattern)
|
|
||||||
bURIParts := SplitURL(bService.Pattern)
|
|
||||||
if len(aURIParts) != len(bURIParts) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := checkURICollision(aURIParts, bURIParts, aService.Input, bService.Input)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("(%s '%s') vs (%s '%s'): %w", aService.Method, aService.Pattern, bService.Method, bService.Pattern, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if uri of services A and B collide
|
|
||||||
func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter) error {
|
|
||||||
var errors = []error{}
|
|
||||||
|
|
||||||
// for each part
|
|
||||||
for pi, aPart := range uriA {
|
|
||||||
bPart := uriB[pi]
|
|
||||||
|
|
||||||
// no need for further check as it has been done earlier in the validation process
|
|
||||||
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
|
|
||||||
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
|
|
||||||
|
|
||||||
// both captures -> as we cannot check, consider a collision
|
|
||||||
if aIsCapture && bIsCapture {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (path %s and %s)", ErrPatternCollision, aPart, bPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// no capture -> check strict equality
|
|
||||||
if !aIsCapture && !bIsCapture {
|
|
||||||
if aPart == bPart {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (same path '%s')", ErrPatternCollision, aPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A captures B -> check type (B is A ?)
|
|
||||||
if aIsCapture {
|
|
||||||
input, exists := inputA[aPart]
|
|
||||||
|
|
||||||
// fail if no type or no validator
|
|
||||||
if !exists || input.Validator == nil {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", ErrPatternCollision, aPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail if not valid
|
|
||||||
if _, valid := input.Validator(bPart); valid {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", ErrPatternCollision, aPart, bPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// B captures A -> check type (A is B ?)
|
|
||||||
} else if bIsCapture {
|
|
||||||
input, exists := inputB[bPart]
|
|
||||||
|
|
||||||
// fail if no type or no validator
|
|
||||||
if !exists || input.Validator == nil {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (invalid type for %s)", ErrPatternCollision, bPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail if not valid
|
|
||||||
if _, valid := input.Validator(aPart); valid {
|
|
||||||
errors = append(errors, fmt.Errorf("%w (%s captures '%s')", ErrPatternCollision, bPart, aPart))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errors = append(errors, nil)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// at least 1 URI part not matching -> no collision
|
|
||||||
var firstError error
|
|
||||||
for _, err := range errors {
|
|
||||||
if err != nil && firstError == nil {
|
|
||||||
firstError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstError
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitURL without empty sets
|
|
||||||
func SplitURL(url string) []string {
|
|
||||||
trimmed := strings.Trim(url, " /\t\r\n")
|
|
||||||
split := strings.Split(trimmed, "/")
|
|
||||||
|
|
||||||
// remove empty set when empty url
|
|
||||||
if len(split) == 1 && len(split[0]) == 0 {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return split
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLegalServiceName(t *testing.T) {
|
func TestLegalServiceName(t *testing.T) {
|
||||||
|
@ -80,8 +80,7 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
err := srv.Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -135,8 +134,7 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
err := srv.Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if test.ValidMethod && err != nil {
|
if test.ValidMethod && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
@ -152,22 +150,20 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestParseEmpty(t *testing.T) {
|
func TestParseEmpty(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
r := strings.NewReader(`[]`)
|
reader := strings.NewReader(`[]`)
|
||||||
srv := &Server{}
|
_, err := Parse(reader)
|
||||||
err := srv.Parse(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error (got '%s')", err)
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestParseJsonError(t *testing.T) {
|
func TestParseJsonError(t *testing.T) {
|
||||||
r := strings.NewReader(`{
|
reader := strings.NewReader(`{
|
||||||
"GET": {
|
"GET": {
|
||||||
"info": "info
|
"info": "info
|
||||||
},
|
},
|
||||||
}`) // trailing ',' is invalid JSON
|
}`) // trailing ',' is invalid JSON
|
||||||
srv := &Server{}
|
_, err := Parse(reader)
|
||||||
err := srv.Parse(r)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error")
|
t.Errorf("expected error")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -184,7 +180,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
`[ { "method": "GET", "path": "/" }]`,
|
`[ { "method": "GET", "path": "/" }]`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{ // missing descriptiontype
|
{ // missing description
|
||||||
`[ { "method": "GET", "path": "/subservice" }]`,
|
`[ { "method": "GET", "path": "/subservice" }]`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
@ -209,8 +205,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
err := srv.Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if test.ValidDescription && err != nil {
|
if test.ValidDescription && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -228,7 +223,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
|
||||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
r := strings.NewReader(`[
|
reader := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -238,9 +233,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv := &Server{}
|
srv, err := Parse(reader, builtin.AnyDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
|
||||||
err := srv.Parse(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -261,7 +254,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestOptionalParam(t *testing.T) {
|
func TestOptionalParam(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
r := strings.NewReader(`[
|
reader := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -274,10 +267,7 @@ func TestOptionalParam(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv := &Server{}
|
srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
|
||||||
err := srv.Parse(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -587,9 +577,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
_, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
|
||||||
err := srv.Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -826,10 +814,7 @@ func TestServiceCollision(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
_, err := Parse(strings.NewReader(test.Config), builtin.StringDataType{}, builtin.UintDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.StringType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.UintType{})
|
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -877,36 +862,6 @@ func TestMatchSimple(t *testing.T) {
|
||||||
"/a",
|
"/a",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{ // root url
|
|
||||||
`[ {
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a",
|
|
||||||
"info": "info",
|
|
||||||
"in": {}
|
|
||||||
} ]`,
|
|
||||||
"/",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[ {
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/a",
|
|
||||||
"info": "info",
|
|
||||||
"in": {}
|
|
||||||
} ]`,
|
|
||||||
"/",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[ {
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/",
|
|
||||||
"info": "info",
|
|
||||||
"in": {}
|
|
||||||
} ]`,
|
|
||||||
"/",
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
`[ {
|
`[ {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
@ -996,11 +951,7 @@ func TestMatchSimple(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv := &Server{}
|
srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{})
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.IntType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -1027,80 +978,3 @@ func TestMatchSimple(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindPriority(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
Config string
|
|
||||||
URL string
|
|
||||||
MatchingDesc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
`[
|
|
||||||
{ "method": "GET", "path": "/a", "info": "s1" },
|
|
||||||
{ "method": "GET", "path": "/", "info": "s2" }
|
|
||||||
]`,
|
|
||||||
"/",
|
|
||||||
"s2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[
|
|
||||||
{ "method": "GET", "path": "/", "info": "s2" },
|
|
||||||
{ "method": "GET", "path": "/a", "info": "s1" }
|
|
||||||
]`,
|
|
||||||
"/",
|
|
||||||
"s2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[
|
|
||||||
{ "method": "GET", "path": "/a", "info": "s1" },
|
|
||||||
{ "method": "GET", "path": "/", "info": "s2" }
|
|
||||||
]`,
|
|
||||||
"/a",
|
|
||||||
"s1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[
|
|
||||||
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
|
|
||||||
{ "method": "GET", "path": "/a/b", "info": "s2" }
|
|
||||||
]`,
|
|
||||||
"/a/b/c",
|
|
||||||
"s1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[
|
|
||||||
{ "method": "GET", "path": "/a/b/c", "info": "s1" },
|
|
||||||
{ "method": "GET", "path": "/a/b", "info": "s2" }
|
|
||||||
]`,
|
|
||||||
"/a/b/",
|
|
||||||
"s2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
|
||||||
srv := &Server{}
|
|
||||||
srv.Validators = append(srv.Validators, validator.AnyType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.IntType{})
|
|
||||||
srv.Validators = append(srv.Validators, validator.BoolType{})
|
|
||||||
err := srv.Parse(strings.NewReader(test.Config))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
|
|
||||||
service := srv.Find(req)
|
|
||||||
if service == nil {
|
|
||||||
t.Errorf("expected to find a service")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
if service.Description != test.MatchingDesc {
|
|
||||||
t.Errorf("expected description '%s', got '%s'", test.MatchingDesc, service.Description)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,61 +1,60 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Err allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type Err string
|
type cerr string
|
||||||
|
|
||||||
func (err Err) Error() string {
|
// Error implements the error builtin interface.
|
||||||
|
func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// ErrRead - a problem ocurred when trying to read the configuration file
|
||||||
// ErrRead - read error
|
const ErrRead = cerr("cannot read config")
|
||||||
ErrRead = Err("cannot read config")
|
|
||||||
|
|
||||||
// ErrUnknownMethod - unknown http method
|
// ErrUnknownMethod - invalid http method
|
||||||
ErrUnknownMethod = Err("unknown HTTP method")
|
const ErrUnknownMethod = cerr("unknown HTTP method")
|
||||||
|
|
||||||
// ErrFormat - invalid format
|
// ErrFormat - a invalid format has been detected
|
||||||
ErrFormat = Err("invalid config format")
|
const ErrFormat = cerr("invalid config format")
|
||||||
|
|
||||||
// ErrPatternCollision - collision between 2 services' patterns
|
// ErrPatternCollision - there is a collision between 2 services' patterns (same method)
|
||||||
ErrPatternCollision = Err("pattern collision")
|
const ErrPatternCollision = cerr("pattern collision")
|
||||||
|
|
||||||
// ErrInvalidPattern - malformed service pattern
|
// ErrInvalidPattern - a service pattern is malformed
|
||||||
ErrInvalidPattern = Err("malformed service path: must begin with a '/' and not end with")
|
const ErrInvalidPattern = cerr("must begin with a '/' and not end with")
|
||||||
|
|
||||||
// ErrInvalidPatternBraceCapture - invalid brace capture
|
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
|
||||||
ErrInvalidPatternBraceCapture = Err("invalid uri parameter")
|
const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces")
|
||||||
|
|
||||||
// ErrUnspecifiedBraceCapture - missing path brace capture
|
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
|
||||||
ErrUnspecifiedBraceCapture = Err("missing uri parameter")
|
const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path")
|
||||||
|
|
||||||
// ErrUndefinedBraceCapture - missing capturing brace definition
|
// ErrMandatoryRename - capture/query parameters must have a rename
|
||||||
ErrUndefinedBraceCapture = Err("missing uri parameter definition")
|
const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'")
|
||||||
|
|
||||||
// ErrMandatoryRename - capture/query parameters must be renamed
|
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
|
||||||
ErrMandatoryRename = Err("uri and query parameters must be renamed")
|
const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition")
|
||||||
|
|
||||||
// ErrMissingDescription - a service is missing its description
|
// ErrMissingDescription - a service is missing its description
|
||||||
ErrMissingDescription = Err("missing description")
|
const ErrMissingDescription = cerr("missing description")
|
||||||
|
|
||||||
// ErrIllegalOptionalURIParam - uri parameter cannot optional
|
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
|
||||||
ErrIllegalOptionalURIParam = Err("uri parameter cannot be optional")
|
const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional")
|
||||||
|
|
||||||
// ErrOptionalOption - cannot have optional output
|
// ErrOptionalOption - an output is optional
|
||||||
ErrOptionalOption = Err("output cannot be optional")
|
const ErrOptionalOption = cerr("output cannot be optional")
|
||||||
|
|
||||||
// ErrMissingParamDesc - missing parameter description
|
// ErrMissingParamDesc - a parameter is missing its description
|
||||||
ErrMissingParamDesc = Err("missing parameter description")
|
const ErrMissingParamDesc = cerr("missing parameter description")
|
||||||
|
|
||||||
// ErrUnknownParamType - unknown parameter type
|
// ErrUnknownDataType - a parameter has an unknown datatype name
|
||||||
ErrUnknownParamType = Err("unknown parameter datatype")
|
const ErrUnknownDataType = cerr("unknown data type")
|
||||||
|
|
||||||
// ErrIllegalParamName - illegal parameter name
|
// ErrIllegalParamName - a parameter has an illegal name
|
||||||
ErrIllegalParamName = Err("illegal parameter name")
|
const ErrIllegalParamName = cerr("illegal parameter name")
|
||||||
|
|
||||||
// ErrMissingParamType - missing parameter type
|
// ErrMissingParamType - a parameter has an illegal type
|
||||||
ErrMissingParamType = Err("missing parameter type")
|
const ErrMissingParamType = cerr("missing parameter type")
|
||||||
|
|
||||||
// ErrParamNameConflict - name/rename conflict
|
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
||||||
ErrParamNameConflict = Err("parameter name conflict")
|
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,48 +1,38 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parameter represents a parameter definition (from api.json)
|
// Validate implements the validator interface
|
||||||
type Parameter struct {
|
func (param *Parameter) Validate(datatypes ...datatype.T) error {
|
||||||
Description string `json:"info"`
|
// missing description
|
||||||
Type string `json:"type"`
|
|
||||||
Rename string `json:"name,omitempty"`
|
|
||||||
Optional bool
|
|
||||||
// GoType is the type the Validator will cast into
|
|
||||||
GoType reflect.Type
|
|
||||||
// Validator is inferred from the "type" property
|
|
||||||
Validator validator.ValidateFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (param *Parameter) validate(datatypes ...validator.Type) error {
|
|
||||||
if len(param.Description) < 1 {
|
if len(param.Description) < 1 {
|
||||||
return ErrMissingParamDesc
|
return ErrMissingParamDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invalid type
|
||||||
if len(param.Type) < 1 || param.Type == "?" {
|
if len(param.Type) < 1 || param.Type == "?" {
|
||||||
return ErrMissingParamType
|
return ErrMissingParamType
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional type
|
// optional type transform
|
||||||
if param.Type[0] == '?' {
|
if param.Type[0] == '?' {
|
||||||
param.Optional = true
|
param.Optional = true
|
||||||
param.Type = param.Type[1:]
|
param.Type = param.Type[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// find validator
|
// assign the datatype
|
||||||
for _, dtype := range datatypes {
|
for _, dtype := range datatypes {
|
||||||
param.Validator = dtype.Validator(param.Type, datatypes...)
|
param.Validator = dtype.Build(param.Type, datatypes...)
|
||||||
param.GoType = dtype.GoType()
|
param.ExtractType = dtype.Type()
|
||||||
if param.Validator != nil {
|
if param.Validator != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if param.Validator == nil {
|
if param.Validator == nil {
|
||||||
return ErrUnknownParamType
|
return ErrUnknownDataType
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
|
@ -6,71 +6,42 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/validator"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
captureRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
|
||||||
availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service definition
|
|
||||||
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"`
|
|
||||||
|
|
||||||
// Captures contains references to URI parameters from the `Input` map. The format
|
|
||||||
// of these parameter names is "{paramName}"
|
|
||||||
Captures []*BraceCapture
|
|
||||||
|
|
||||||
// Query contains references to HTTP Query parameters from the `Input` map.
|
|
||||||
// Query parameters names are "GET@paramName", this map contains escaped names (e.g. "paramName")
|
|
||||||
Query map[string]*Parameter
|
|
||||||
|
|
||||||
// Form references form parameters from the `Input` map (all but Captures and Query).
|
|
||||||
Form map[string]*Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
// BraceCapture links to the related URI parameter
|
|
||||||
type BraceCapture struct {
|
|
||||||
Name string
|
|
||||||
Index int
|
|
||||||
Ref *Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match returns if this service would handle this HTTP request
|
// Match returns if this service would handle this HTTP request
|
||||||
func (svc *Service) Match(req *http.Request) bool {
|
func (svc *Service) Match(req *http.Request) bool {
|
||||||
var (
|
// method
|
||||||
uri = req.RequestURI
|
if req.Method != svc.Method {
|
||||||
queryIndex = strings.IndexByte(uri, '?')
|
return false
|
||||||
)
|
|
||||||
|
|
||||||
// remove query part for matching the pattern
|
|
||||||
if queryIndex > -1 {
|
|
||||||
uri = uri[:queryIndex]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.Method == svc.Method && svc.matchPattern(uri)
|
// check path
|
||||||
|
if !svc.matchPattern(req.RequestURI) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check and extract input
|
||||||
|
// todo: check if input match and extract models
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if an uri matches the service's pattern
|
// checks if an uri matches the service's pattern
|
||||||
func (svc *Service) matchPattern(uri string) bool {
|
func (svc *Service) matchPattern(uri string) bool {
|
||||||
var (
|
uriparts := SplitURL(uri)
|
||||||
uriparts = SplitURL(uri)
|
parts := SplitURL(svc.Pattern)
|
||||||
parts = SplitURL(svc.Pattern)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// fail if size differ
|
||||||
if len(uriparts) != len(parts) {
|
if len(uriparts) != len(parts) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// root url '/'
|
// root url '/'
|
||||||
if len(parts) == 0 && len(uriparts) == 0 {
|
if len(parts) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,35 +76,40 @@ func (svc *Service) matchPattern(uri string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Validate implements the validator interface
|
||||||
func (svc *Service) validate(datatypes ...validator.Type) error {
|
func (svc *Service) Validate(datatypes ...datatype.T) error {
|
||||||
err := svc.checkMethod()
|
// check method
|
||||||
|
err := svc.isMethodAvailable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'method': %w", err)
|
return fmt.Errorf("field 'method': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check pattern
|
||||||
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
|
svc.Pattern = strings.Trim(svc.Pattern, " \t\r\n")
|
||||||
err = svc.checkPattern()
|
err = svc.isPatternValid()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'path': %w", err)
|
return fmt.Errorf("field 'path': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check description
|
||||||
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
|
if len(strings.Trim(svc.Description, " \t\r\n")) < 1 {
|
||||||
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
|
return fmt.Errorf("field 'description': %w", ErrMissingDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.checkInput(datatypes)
|
// check input parameters
|
||||||
|
err = svc.validateInput(datatypes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'in': %w", err)
|
return fmt.Errorf("field 'in': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail when a brace capture remains undefined
|
// fail if a brace capture remains undefined
|
||||||
for _, capture := range svc.Captures {
|
for _, capture := range svc.Captures {
|
||||||
if capture.Ref == nil {
|
if capture.Ref == nil {
|
||||||
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
|
return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.checkOutput(datatypes)
|
// check output
|
||||||
|
err = svc.validateOutput(datatypes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field 'out': %w", err)
|
return fmt.Errorf("field 'out': %w", err)
|
||||||
}
|
}
|
||||||
|
@ -141,7 +117,7 @@ func (svc *Service) validate(datatypes ...validator.Type) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkMethod() error {
|
func (svc *Service) isMethodAvailable() error {
|
||||||
for _, available := range availableHTTPMethods {
|
for _, available := range availableHTTPMethods {
|
||||||
if svc.Method == available {
|
if svc.Method == available {
|
||||||
return nil
|
return nil
|
||||||
|
@ -150,14 +126,7 @@ func (svc *Service) checkMethod() error {
|
||||||
return ErrUnknownMethod
|
return ErrUnknownMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPattern checks for the validity of the pattern definition (i.e. the uri)
|
func (svc *Service) isPatternValid() error {
|
||||||
//
|
|
||||||
// Note that the uri can contain capture params e.g. `/a/{b}/c/{d}`, in this
|
|
||||||
// example, input parameters with names `{b}` and `{d}` are expected.
|
|
||||||
//
|
|
||||||
// This methods sets up the service state with adding capture params that are
|
|
||||||
// expected; checkInputs() will be able to check params agains pattern captures.
|
|
||||||
func (svc *Service) checkPattern() error {
|
|
||||||
length := len(svc.Pattern)
|
length := len(svc.Pattern)
|
||||||
|
|
||||||
// empty pattern
|
// empty pattern
|
||||||
|
@ -180,7 +149,7 @@ func (svc *Service) checkPattern() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if brace capture
|
// if brace capture
|
||||||
if matches := captureRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
braceName := matches[0][1]
|
braceName := matches[0][1]
|
||||||
|
|
||||||
// append
|
// append
|
||||||
|
@ -199,183 +168,147 @@ func (svc *Service) checkPattern() error {
|
||||||
if strings.ContainsAny(part, "{}") {
|
if strings.ContainsAny(part, "{}") {
|
||||||
return ErrInvalidPatternBraceCapture
|
return ErrInvalidPatternBraceCapture
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkInput(types []validator.Type) error {
|
func (svc *Service) validateInput(types []datatype.T) error {
|
||||||
// no parameter
|
|
||||||
|
// ignore no parameter
|
||||||
if svc.Input == nil || len(svc.Input) < 1 {
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
svc.Input = map[string]*Parameter{}
|
svc.Input = make(map[string]*Parameter, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each parameter
|
// for each parameter
|
||||||
for name, p := range svc.Input {
|
for paramName, param := range svc.Input {
|
||||||
if len(name) < 1 {
|
if len(paramName) < 1 {
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse parameters: capture (uri), query or form and update the service
|
// fail if brace capture does not exists in pattern
|
||||||
// attributes accordingly
|
var iscapture, isquery bool
|
||||||
ptype, err := svc.parseParam(name, p)
|
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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
|
||||||
|
|
||||||
// Rename mandatory for capture and query
|
|
||||||
if len(p.Rename) < 1 && (ptype == captureParam || ptype == queryParam) {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMandatoryRename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback to name when Rename is not provided
|
|
||||||
if len(p.Rename) < 1 {
|
|
||||||
p.Rename = name
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.validate(types...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", name, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// capture parameter cannot be optional
|
// capture parameter cannot be optional
|
||||||
if p.Optional && ptype == captureParam {
|
if iscapture && param.Optional {
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalOptionalURIParam)
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = nameConflicts(name, p, svc.Input)
|
// fail on name/rename conflict
|
||||||
if err != nil {
|
for paramName2, param2 := range svc.Input {
|
||||||
return err
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkOutput(types []validator.Type) error {
|
func (svc *Service) validateOutput(types []datatype.T) error {
|
||||||
// no parameter
|
|
||||||
|
// ignore no parameter
|
||||||
if svc.Output == nil || len(svc.Output) < 1 {
|
if svc.Output == nil || len(svc.Output) < 1 {
|
||||||
svc.Output = make(map[string]*Parameter, 0)
|
svc.Output = make(map[string]*Parameter, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, p := range svc.Output {
|
// for each parameter
|
||||||
if len(name) < 1 {
|
for paramName, param := range svc.Output {
|
||||||
return fmt.Errorf("%s: %w", name, ErrIllegalParamName)
|
if len(paramName) < 1 {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to name when Rename is not provided
|
// use param name if no rename
|
||||||
if len(p.Rename) < 1 {
|
if len(param.Rename) < 1 {
|
||||||
p.Rename = name
|
param.Rename = paramName
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.validate(types...)
|
err := param.Validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", name, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Optional {
|
if param.Optional {
|
||||||
return fmt.Errorf("%s: %w", name, ErrOptionalOption)
|
return fmt.Errorf("%s: %w", paramName, ErrOptionalOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = nameConflicts(name, p, svc.Output)
|
// fail on name/rename conflict
|
||||||
if err != nil {
|
for paramName2, param2 := range svc.Output {
|
||||||
return err
|
// ignore self
|
||||||
}
|
if paramName == paramName2 {
|
||||||
}
|
continue
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type paramType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
captureParam paramType = iota
|
|
||||||
queryParam
|
|
||||||
formParam
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseParam determines which param type it is from its name:
|
|
||||||
// - `{paramName}` is an capture; it captures a segment of the uri defined in
|
|
||||||
// the pattern definition, e.g. `/some/path/with/{paramName}/somewhere`
|
|
||||||
// - `GET@paramName` is an uri query that is received from the http query format
|
|
||||||
// in the uri, e.g. `http://domain.com/uri?paramName=paramValue¶m2=value2`
|
|
||||||
// - any other name that contains valid characters is considered a Form
|
|
||||||
// parameter; it is extracted from the http request's body as: json, multipart
|
|
||||||
// or using the x-www-form-urlencoded format.
|
|
||||||
//
|
|
||||||
// Special notes:
|
|
||||||
// - capture params MUST be found in the pattern definition.
|
|
||||||
// - capture params MUST NOT be optional as they are in the pattern anyways.
|
|
||||||
// - capture and query params MUST be renamed because the `{param}` or
|
|
||||||
// `GET@param` name formats cannot be translated to a valid go exported name.
|
|
||||||
// c.f. the `dynfunc` package that creates a handler func() signature from
|
|
||||||
// the service definitions (i.e. input and output parameters).
|
|
||||||
func (svc *Service) parseParam(name string, p *Parameter) (paramType, error) {
|
|
||||||
var (
|
|
||||||
captureMatches = captureRegex.FindAllStringSubmatch(name, -1)
|
|
||||||
isCapture = len(captureMatches) > 0 && len(captureMatches[0]) > 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parameter is a capture (uri/{param})
|
|
||||||
if isCapture {
|
|
||||||
captureName := captureMatches[0][1]
|
|
||||||
|
|
||||||
// fail if brace capture does not exists in pattern
|
|
||||||
found := false
|
|
||||||
for _, capture := range svc.Captures {
|
|
||||||
if capture.Name == captureName {
|
|
||||||
capture.Ref = p
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if !found {
|
|
||||||
return captureParam, fmt.Errorf("%s: %w", name, ErrUnspecifiedBraceCapture)
|
|
||||||
}
|
|
||||||
return captureParam, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
queryMatches = queryRegex.FindAllStringSubmatch(name, -1)
|
|
||||||
isQuery = len(queryMatches) > 0 && len(queryMatches[0]) > 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parameter is a query (uri?param)
|
|
||||||
if isQuery {
|
|
||||||
queryName := queryMatches[0][1]
|
|
||||||
|
|
||||||
// init map
|
|
||||||
if svc.Query == nil {
|
|
||||||
svc.Query = make(map[string]*Parameter)
|
|
||||||
}
|
|
||||||
svc.Query[queryName] = p
|
|
||||||
|
|
||||||
return queryParam, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter is a form param
|
|
||||||
if svc.Form == nil {
|
|
||||||
svc.Form = make(map[string]*Parameter)
|
|
||||||
}
|
|
||||||
svc.Form[name] = p
|
|
||||||
return formParam, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameConflicts returns whether ar given parameter has its name or Rename field
|
|
||||||
// in conflict with an existing parameter
|
|
||||||
func nameConflicts(name string, param *Parameter, others map[string]*Parameter) error {
|
|
||||||
for otherName, other := range others {
|
|
||||||
// ignore self
|
|
||||||
if otherName == name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. same rename field
|
|
||||||
// 2. original name matches a renamed field
|
|
||||||
// 3. renamed field matches an original name
|
|
||||||
if param.Rename == other.Rename || name == other.Rename || otherName == param.Rename {
|
|
||||||
return fmt.Errorf("%s: %w", otherName, ErrParamNameConflict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
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,13 +0,0 @@
|
||||||
package ctx
|
|
||||||
|
|
||||||
// Key defines a custom context key type
|
|
||||||
type Key int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Request is the key for the current *http.Request
|
|
||||||
Request Key = iota
|
|
||||||
// Response is the key for the associated http.ResponseWriter
|
|
||||||
Response
|
|
||||||
// Auth is the key for the request's authentication information
|
|
||||||
Auth
|
|
||||||
)
|
|
|
@ -1,52 +0,0 @@
|
||||||
package dynfunc
|
|
||||||
|
|
||||||
// Err allows you to create constant "const" error with type boxing.
|
|
||||||
type Err string
|
|
||||||
|
|
||||||
func (err Err) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrHandlerNotFunc - handler is not a func
|
|
||||||
ErrHandlerNotFunc = Err("handler must be a func")
|
|
||||||
|
|
||||||
// ErrNoServiceForHandler - no service matching this handler
|
|
||||||
ErrNoServiceForHandler = Err("no service found for this handler")
|
|
||||||
|
|
||||||
// errMissingHandlerArgumentParam - missing params arguments for handler
|
|
||||||
ErrMissingHandlerContextArgument = Err("missing handler first argument of type context.Context")
|
|
||||||
|
|
||||||
// ErrInvalidHandlerContextArgument - missing handler output error
|
|
||||||
ErrInvalidHandlerContextArgument = Err("first input argument should be of type context.Context")
|
|
||||||
|
|
||||||
// ErrMissingHandlerInputArgument - missing params arguments for handler
|
|
||||||
ErrMissingHandlerInputArgument = Err("missing handler argument: input struct")
|
|
||||||
|
|
||||||
// ErrUnexpectedInput - input argument is not expected
|
|
||||||
ErrUnexpectedInput = Err("unexpected input struct")
|
|
||||||
|
|
||||||
// ErrMissingHandlerOutputArgument - missing output for handler
|
|
||||||
ErrMissingHandlerOutputArgument = Err("missing handler first output argument: output struct")
|
|
||||||
|
|
||||||
// ErrMissingHandlerErrorArgument - missing error output for handler
|
|
||||||
ErrMissingHandlerErrorArgument = Err("missing handler last output argument of type api.Err")
|
|
||||||
|
|
||||||
// ErrInvalidHandlerErrorArgument - missing handler output error
|
|
||||||
ErrInvalidHandlerErrorArgument = Err("last output must be of type api.Err")
|
|
||||||
|
|
||||||
// ErrMissingParamArgument - missing parameters argument for handler
|
|
||||||
ErrMissingParamArgument = Err("handler second argument must be a struct")
|
|
||||||
|
|
||||||
// ErrUnexportedName - argument is unexported in struct
|
|
||||||
ErrUnexportedName = Err("unexported name")
|
|
||||||
|
|
||||||
// ErrWrongOutputArgumentType - wrong type for output first argument
|
|
||||||
ErrWrongOutputArgumentType = Err("handler first output argument must be a *struct")
|
|
||||||
|
|
||||||
// ErrMissingConfigArgument - missing an input/output argument in handler struct
|
|
||||||
ErrMissingConfigArgument = Err("missing an argument from the configuration")
|
|
||||||
|
|
||||||
// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
|
|
||||||
ErrWrongParamTypeFromConfig = Err("invalid struct field type")
|
|
||||||
)
|
|
|
@ -1,144 +0,0 @@
|
||||||
package dynfunc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler represents a dynamic aicra service handler
|
|
||||||
type Handler struct {
|
|
||||||
// signature defined from the service configuration
|
|
||||||
signature *Signature
|
|
||||||
// fn provided function that will be the service's handler
|
|
||||||
fn interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a handler from a dynamic function and checks its signature against a
|
|
||||||
// service configuration
|
|
||||||
//
|
|
||||||
// `fn` must have as a signature : `func(context.Context, in) (*out, api.Err)`
|
|
||||||
// - `in` is a struct{} containing a field for each service input (with valid reflect.Type)
|
|
||||||
// - `out` is a struct{} containing a field for each service output (with valid reflect.Type)
|
|
||||||
//
|
|
||||||
// Special cases:
|
|
||||||
// - it there is no input, `in` MUST be omitted
|
|
||||||
// - it there is no output, `out` CAN be omitted
|
|
||||||
func Build(fn interface{}, service config.Service) (*Handler, error) {
|
|
||||||
var (
|
|
||||||
h = &Handler{
|
|
||||||
signature: BuildSignature(service),
|
|
||||||
fn: fn,
|
|
||||||
}
|
|
||||||
fnType = reflect.TypeOf(fn)
|
|
||||||
)
|
|
||||||
|
|
||||||
if fnType.Kind() != reflect.Func {
|
|
||||||
return nil, ErrHandlerNotFunc
|
|
||||||
}
|
|
||||||
if err := h.signature.ValidateInput(fnType); err != nil {
|
|
||||||
return nil, fmt.Errorf("input: %w", err)
|
|
||||||
}
|
|
||||||
if err := h.signature.ValidateOutput(fnType); err != nil {
|
|
||||||
return nil, fmt.Errorf("output: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle binds input `data` into the dynamic function and returns an output map
|
|
||||||
func (h *Handler) Handle(ctx context.Context, data map[string]interface{}) (map[string]interface{}, api.Err) {
|
|
||||||
var (
|
|
||||||
ert = reflect.TypeOf(api.Err{})
|
|
||||||
fnv = reflect.ValueOf(h.fn)
|
|
||||||
callArgs = make([]reflect.Value, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
// bind context
|
|
||||||
callArgs = append(callArgs, reflect.ValueOf(ctx))
|
|
||||||
|
|
||||||
inputStructRequired := fnv.Type().NumIn() > 1
|
|
||||||
|
|
||||||
// bind input arguments
|
|
||||||
if inputStructRequired {
|
|
||||||
// create zero value struct
|
|
||||||
var (
|
|
||||||
callStructPtr = reflect.New(fnv.Type().In(1))
|
|
||||||
callStruct = callStructPtr.Elem()
|
|
||||||
)
|
|
||||||
|
|
||||||
// set each field
|
|
||||||
for name := range h.signature.Input {
|
|
||||||
field := callStruct.FieldByName(name)
|
|
||||||
if !field.CanSet() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// get value from @data
|
|
||||||
value, provided := data[name]
|
|
||||||
if !provided {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var refvalue = reflect.ValueOf(value)
|
|
||||||
|
|
||||||
// T to pointer of T
|
|
||||||
if field.Kind() == reflect.Ptr {
|
|
||||||
var ptrType = field.Type().Elem()
|
|
||||||
|
|
||||||
if !refvalue.Type().ConvertibleTo(ptrType) {
|
|
||||||
log.Printf("Cannot convert %v into *%v", refvalue.Type(), ptrType)
|
|
||||||
return nil, api.ErrUncallableService
|
|
||||||
}
|
|
||||||
|
|
||||||
ptr := reflect.New(ptrType)
|
|
||||||
ptr.Elem().Set(reflect.ValueOf(value).Convert(ptrType))
|
|
||||||
|
|
||||||
field.Set(ptr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.ValueOf(value).Type().ConvertibleTo(field.Type()) {
|
|
||||||
log.Printf("Cannot convert %v into %v", reflect.ValueOf(value).Type(), field.Type())
|
|
||||||
return nil, api.ErrUncallableService
|
|
||||||
}
|
|
||||||
|
|
||||||
field.Set(refvalue.Convert(field.Type()))
|
|
||||||
}
|
|
||||||
callArgs = append(callArgs, callStruct)
|
|
||||||
}
|
|
||||||
|
|
||||||
// call the handler
|
|
||||||
output := fnv.Call(callArgs)
|
|
||||||
|
|
||||||
// no output OR pointer to output struct is nil
|
|
||||||
outdata := make(map[string]interface{})
|
|
||||||
if len(h.signature.Output) < 1 || output[0].IsNil() {
|
|
||||||
var structerr = output[len(output)-1].Convert(ert)
|
|
||||||
return outdata, api.Err{
|
|
||||||
Code: int(structerr.FieldByName("Code").Int()),
|
|
||||||
Reason: structerr.FieldByName("Reason").String(),
|
|
||||||
Status: int(structerr.FieldByName("Status").Int()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract struct from pointer
|
|
||||||
returnStruct := output[0].Elem()
|
|
||||||
|
|
||||||
for name := range h.signature.Output {
|
|
||||||
field := returnStruct.FieldByName(name)
|
|
||||||
outdata[name] = field.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract api.Err
|
|
||||||
var structerr = output[len(output)-1].Convert(ert)
|
|
||||||
return outdata, api.Err{
|
|
||||||
Code: int(structerr.FieldByName("Code").Int()),
|
|
||||||
Reason: structerr.FieldByName("Reason").String(),
|
|
||||||
Status: int(structerr.FieldByName("Status").Int()),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
package dynfunc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testsignature Signature
|
|
||||||
|
|
||||||
// builds a mock service with provided arguments as Input and matched as Output
|
|
||||||
func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature {
|
|
||||||
if s.Input == nil {
|
|
||||||
s.Input = make(map[string]reflect.Type)
|
|
||||||
}
|
|
||||||
if s.Output == nil {
|
|
||||||
s.Output = make(map[string]reflect.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, dtype := range dtypes {
|
|
||||||
name := fmt.Sprintf("P%d", i+1)
|
|
||||||
s.Input[name] = dtype
|
|
||||||
if dtype.Kind() == reflect.Ptr {
|
|
||||||
s.Output[name] = dtype.Elem()
|
|
||||||
} else {
|
|
||||||
s.Output[name] = dtype
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInput(t *testing.T) {
|
|
||||||
|
|
||||||
type intstruct struct {
|
|
||||||
P1 int
|
|
||||||
}
|
|
||||||
type intptrstruct struct {
|
|
||||||
P1 *int
|
|
||||||
}
|
|
||||||
|
|
||||||
tcases := []struct {
|
|
||||||
Name string
|
|
||||||
Spec *testsignature
|
|
||||||
HasContext bool
|
|
||||||
Fn interface{}
|
|
||||||
Input []interface{}
|
|
||||||
ExpectedOutput []interface{}
|
|
||||||
ExpectedErr api.Err
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "none required none provided",
|
|
||||||
Spec: (&testsignature{}).withArgs(),
|
|
||||||
Fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{},
|
|
||||||
ExpectedOutput: []interface{}{},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "int proxy (0)",
|
|
||||||
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
|
|
||||||
Fn: func(ctx context.Context, in intstruct) (*intstruct, api.Err) {
|
|
||||||
return &intstruct{P1: in.P1}, api.ErrSuccess
|
|
||||||
},
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{int(0)},
|
|
||||||
ExpectedOutput: []interface{}{int(0)},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "int proxy (11)",
|
|
||||||
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
|
|
||||||
Fn: func(ctx context.Context, in intstruct) (*intstruct, api.Err) {
|
|
||||||
return &intstruct{P1: in.P1}, api.ErrSuccess
|
|
||||||
},
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{int(11)},
|
|
||||||
ExpectedOutput: []interface{}{int(11)},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "*int proxy (nil)",
|
|
||||||
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
|
|
||||||
Fn: func(ctx context.Context, in intptrstruct) (*intptrstruct, api.Err) {
|
|
||||||
return &intptrstruct{P1: in.P1}, api.ErrSuccess
|
|
||||||
},
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{},
|
|
||||||
ExpectedOutput: []interface{}{nil},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "*int proxy (28)",
|
|
||||||
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
|
|
||||||
Fn: func(ctx context.Context, in intptrstruct) (*intstruct, api.Err) {
|
|
||||||
return &intstruct{P1: *in.P1}, api.ErrSuccess
|
|
||||||
},
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{28},
|
|
||||||
ExpectedOutput: []interface{}{28},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "*int proxy (13)",
|
|
||||||
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
|
|
||||||
Fn: func(ctx context.Context, in intptrstruct) (*intstruct, api.Err) {
|
|
||||||
return &intstruct{P1: *in.P1}, api.ErrSuccess
|
|
||||||
},
|
|
||||||
HasContext: false,
|
|
||||||
Input: []interface{}{13},
|
|
||||||
ExpectedOutput: []interface{}{13},
|
|
||||||
ExpectedErr: api.ErrSuccess,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tcase := range tcases {
|
|
||||||
t.Run(tcase.Name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var handler = &Handler{
|
|
||||||
signature: &Signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output},
|
|
||||||
fn: tcase.Fn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// build input
|
|
||||||
input := make(map[string]interface{})
|
|
||||||
for i, val := range tcase.Input {
|
|
||||||
var key = fmt.Sprintf("P%d", i+1)
|
|
||||||
input[key] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
var output, err = handler.Handle(context.Background(), input)
|
|
||||||
if err != tcase.ExpectedErr {
|
|
||||||
t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check output
|
|
||||||
for i, expected := range tcase.ExpectedOutput {
|
|
||||||
var (
|
|
||||||
key = fmt.Sprintf("P%d", i+1)
|
|
||||||
val, exists = output[key]
|
|
||||||
)
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("missing output[%s]", key)
|
|
||||||
}
|
|
||||||
if expected != val {
|
|
||||||
var (
|
|
||||||
expectedt = reflect.ValueOf(expected)
|
|
||||||
valt = reflect.ValueOf(val)
|
|
||||||
expectedNil = !expectedt.IsValid() || expectedt.Kind() == reflect.Ptr && expectedt.IsNil()
|
|
||||||
valNil = !valt.IsValid() || valt.Kind() == reflect.Ptr && valt.IsNil()
|
|
||||||
)
|
|
||||||
// ignore both nil
|
|
||||||
if valNil && expectedNil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.Fatalf("expected output[%s] to equal %T <%v> got %T <%v>", key, expected, expected, val, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package dynfunc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signature represents input/output arguments for service from the aicra configuration
|
|
||||||
type Signature struct {
|
|
||||||
// Input arguments of the service
|
|
||||||
Input map[string]reflect.Type
|
|
||||||
// Output arguments of the service
|
|
||||||
Output map[string]reflect.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildSignature builds a signature for a service configuration
|
|
||||||
func BuildSignature(service config.Service) *Signature {
|
|
||||||
s := &Signature{
|
|
||||||
Input: make(map[string]reflect.Type),
|
|
||||||
Output: make(map[string]reflect.Type),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, param := range service.Input {
|
|
||||||
if len(param.Rename) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// make a pointer if optional
|
|
||||||
if param.Optional {
|
|
||||||
s.Input[param.Rename] = reflect.PtrTo(param.GoType)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.Input[param.Rename] = param.GoType
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, param := range service.Output {
|
|
||||||
if len(param.Rename) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.Output[param.Rename] = param.GoType
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateInput validates a handler's input arguments against the service signature
|
|
||||||
func (s *Signature) ValidateInput(handlerType reflect.Type) error {
|
|
||||||
ctxType := reflect.TypeOf((*context.Context)(nil)).Elem()
|
|
||||||
|
|
||||||
// missing or invalid first arg: context.Context
|
|
||||||
if handlerType.NumIn() < 1 {
|
|
||||||
return ErrMissingHandlerContextArgument
|
|
||||||
}
|
|
||||||
firstArgType := handlerType.In(0)
|
|
||||||
|
|
||||||
if !firstArgType.Implements(ctxType) {
|
|
||||||
return ErrInvalidHandlerContextArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// no input required
|
|
||||||
if len(s.Input) == 0 {
|
|
||||||
// input struct provided
|
|
||||||
if handlerType.NumIn() > 1 {
|
|
||||||
return ErrUnexpectedInput
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// too much arguments
|
|
||||||
if handlerType.NumIn() != 2 {
|
|
||||||
return ErrMissingHandlerInputArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// arg must be a struct
|
|
||||||
inStruct := handlerType.In(1)
|
|
||||||
if inStruct.Kind() != reflect.Struct {
|
|
||||||
return ErrMissingParamArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for invalid param
|
|
||||||
for name, ptype := range s.Input {
|
|
||||||
if name[0] == strings.ToLower(name)[0] {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
|
||||||
}
|
|
||||||
|
|
||||||
field, exists := inStruct.FieldByName(name)
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingConfigArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ptype.AssignableTo(field.Type) {
|
|
||||||
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateOutput validates a handler's output arguments against the service signature
|
|
||||||
func (s Signature) ValidateOutput(handlerType reflect.Type) error {
|
|
||||||
errType := reflect.TypeOf(api.ErrUnknown)
|
|
||||||
|
|
||||||
if handlerType.NumOut() < 1 {
|
|
||||||
return ErrMissingHandlerErrorArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// last output must be api.Err
|
|
||||||
lastArgType := handlerType.Out(handlerType.NumOut() - 1)
|
|
||||||
if !lastArgType.AssignableTo(errType) {
|
|
||||||
return ErrInvalidHandlerErrorArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// no output required -> ok
|
|
||||||
if len(s.Output) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if handlerType.NumOut() < 2 {
|
|
||||||
return ErrMissingHandlerOutputArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail if first output is not a pointer to struct
|
|
||||||
outStructPtr := handlerType.Out(0)
|
|
||||||
if outStructPtr.Kind() != reflect.Ptr {
|
|
||||||
return ErrWrongOutputArgumentType
|
|
||||||
}
|
|
||||||
|
|
||||||
outStruct := outStructPtr.Elem()
|
|
||||||
if outStruct.Kind() != reflect.Struct {
|
|
||||||
return ErrWrongOutputArgumentType
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail on invalid output
|
|
||||||
for name, ptype := range s.Output {
|
|
||||||
if name[0] == strings.ToLower(name)[0] {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrUnexportedName)
|
|
||||||
}
|
|
||||||
|
|
||||||
field, exists := outStruct.FieldByName(name)
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingConfigArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore types evalutating to nil
|
|
||||||
if ptype == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !field.Type.ConvertibleTo(ptype) {
|
|
||||||
return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,570 +0,0 @@
|
||||||
package dynfunc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInputValidation(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
input map[string]reflect.Type
|
|
||||||
fn interface{}
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "missing context",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func() {},
|
|
||||||
err: ErrMissingHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid context",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func(int) {},
|
|
||||||
err: ErrInvalidHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no input 0 given",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func(context.Context) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no input 1 given",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func(context.Context, int) {},
|
|
||||||
err: ErrUnexpectedInput,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no input 2 given",
|
|
||||||
input: map[string]reflect.Type{},
|
|
||||||
fn: func(context.Context, int, string) {},
|
|
||||||
err: ErrUnexpectedInput,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input 0 given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context) {},
|
|
||||||
err: ErrMissingHandlerInputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input non-struct given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, int) {},
|
|
||||||
err: ErrMissingParamArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unexported input",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{}) {},
|
|
||||||
err: ErrUnexportedName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input empty struct given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{}) {},
|
|
||||||
err: ErrMissingConfigArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input invalid given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input valid given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 int }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input ptr empty struct given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{}) {},
|
|
||||||
err: ErrMissingConfigArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input ptr invalid given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input ptr invalid ptr type given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *string }) {},
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 input ptr valid given",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(int)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *int }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid string",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(string("")),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 string }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid uint",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(uint(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 uint }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid float64",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(float64(0)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 float64 }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid []byte",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf([]byte("")),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 []byte }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid []rune",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf([]rune("")),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 []rune }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid *string",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(string)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *string }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid *uint",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(uint)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *uint }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid *float64",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new(float64)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *float64 }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid *[]byte",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new([]byte)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *[]byte }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 valid *[]rune",
|
|
||||||
input: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(new([]rune)),
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *[]rune }) {},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// mock spec
|
|
||||||
s := Signature{
|
|
||||||
Input: tc.input,
|
|
||||||
Output: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.ValidateInput(reflect.TypeOf(tc.fn))
|
|
||||||
if err == nil && tc.err != nil {
|
|
||||||
t.Fatalf("expected an error: '%s'", tc.err.Error())
|
|
||||||
}
|
|
||||||
if err != nil && tc.err == nil {
|
|
||||||
t.Fatalf("unexpected error: '%s'", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.err != nil {
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputValidation(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
output map[string]reflect.Type
|
|
||||||
fn interface{}
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no output missing err",
|
|
||||||
output: map[string]reflect.Type{},
|
|
||||||
fn: func() {},
|
|
||||||
err: ErrMissingHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no output invalid err",
|
|
||||||
output: map[string]reflect.Type{},
|
|
||||||
fn: func() bool { return true },
|
|
||||||
err: ErrInvalidHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 output none required",
|
|
||||||
output: map[string]reflect.Type{},
|
|
||||||
fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no output 1 required",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrMissingHandlerOutputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid int output",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (int, api.Err) { return 0, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid int ptr output",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (*int, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid struct output",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (struct{ Test1 int }, api.Err) { return struct{ Test1 int }{Test1: 1}, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unexported param",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrUnexportedName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing output param",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrMissingConfigArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid output param",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid param",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2 valid params",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": reflect.TypeOf(int(0)),
|
|
||||||
"Test2": reflect.TypeOf(string("")),
|
|
||||||
},
|
|
||||||
fn: func() (*struct {
|
|
||||||
Test1 int
|
|
||||||
Test2 string
|
|
||||||
}, api.Err) {
|
|
||||||
return nil, api.ErrSuccess
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil type ignore typecheck",
|
|
||||||
output: map[string]reflect.Type{
|
|
||||||
"Test1": nil,
|
|
||||||
},
|
|
||||||
fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// mock spec
|
|
||||||
s := Signature{
|
|
||||||
Input: nil,
|
|
||||||
Output: tc.output,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.ValidateOutput(reflect.TypeOf(tc.fn))
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceValidation(t *testing.T) {
|
|
||||||
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
in []*config.Parameter
|
|
||||||
out []*config.Parameter
|
|
||||||
fn interface{}
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "missing context",
|
|
||||||
fn: func() {},
|
|
||||||
err: ErrMissingHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid context",
|
|
||||||
fn: func(int) {},
|
|
||||||
err: ErrInvalidHandlerContextArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing error",
|
|
||||||
fn: func(context.Context) {},
|
|
||||||
err: ErrMissingHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid error",
|
|
||||||
fn: func(context.Context) int { return 1 },
|
|
||||||
err: ErrInvalidHandlerErrorArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no in no out",
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unamed in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "", // should be ignored
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrMissingHandlerInputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional in not ptr",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid optional in",
|
|
||||||
in: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context, struct{ Test1 *int }) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "unamed out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "", // should be ignored
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing out struct",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) api.Err { return api.ErrSuccess },
|
|
||||||
err: ErrMissingHandlerOutputArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid out struct type",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (int, api.Err) { return 0, api.ErrSuccess },
|
|
||||||
err: ErrWrongOutputArgumentType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrMissingConfigArgument,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid out",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional out not ptr",
|
|
||||||
out: []*config.Parameter{
|
|
||||||
{
|
|
||||||
Rename: "Test1",
|
|
||||||
GoType: reflect.TypeOf(int(0)),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fn: func(context.Context) (*struct{ Test1 *int }, api.Err) { return nil, api.ErrSuccess },
|
|
||||||
err: ErrWrongParamTypeFromConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
service := config.Service{
|
|
||||||
Input: make(map[string]*config.Parameter),
|
|
||||||
Output: make(map[string]*config.Parameter),
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill service with arguments
|
|
||||||
if tc.in != nil && len(tc.in) > 0 {
|
|
||||||
for i, in := range tc.in {
|
|
||||||
service.Input[fmt.Sprintf("%d", i)] = in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tc.out != nil && len(tc.out) > 0 {
|
|
||||||
for i, out := range tc.out {
|
|
||||||
service.Output[fmt.Sprintf("%d", i)] = out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := BuildSignature(service)
|
|
||||||
|
|
||||||
err := s.ValidateInput(reflect.TypeOf(tc.fn))
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.ValidateOutput(reflect.TypeOf(tc.fn))
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, tc.err) {
|
|
||||||
t.Fatalf("expected the error <%s> got <%s>", tc.err, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// no error encountered but expected 1
|
|
||||||
if tc.err != nil {
|
|
||||||
t.Fatalf("expected an error <%v>", tc.err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,19 +13,19 @@ func (comp *Component) parseHeaders(_raw []byte) error {
|
||||||
// 1. Extract lines
|
// 1. Extract lines
|
||||||
_lines := strings.Split(string(_raw), "\n")
|
_lines := strings.Split(string(_raw), "\n")
|
||||||
if len(_lines) < 2 {
|
if len(_lines) < 2 {
|
||||||
return errNoHeader
|
return ErrNoHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. trim each line + remove 'Content-Disposition' prefix
|
// 2. trim each line + remove 'Content-Disposition' prefix
|
||||||
header := strings.Trim(_lines[0], " \t\r")
|
header := strings.Trim(_lines[0], " \t\r")
|
||||||
|
|
||||||
if !strings.HasPrefix(header, "Content-Disposition: form-data;") {
|
if !strings.HasPrefix(header, "Content-Disposition: form-data;") {
|
||||||
return errNoHeader
|
return ErrNoHeader
|
||||||
}
|
}
|
||||||
header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r")
|
header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r")
|
||||||
|
|
||||||
if len(header) < 1 {
|
if len(header) < 1 {
|
||||||
return errNoHeader
|
return ErrNoHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Extract each key-value pair
|
// 3. Extract each key-value pair
|
||||||
|
|
|
@ -71,11 +71,11 @@ func (reader *Reader) Parse() error {
|
||||||
|
|
||||||
name := comp.GetHeader("name")
|
name := comp.GetHeader("name")
|
||||||
if len(name) < 1 {
|
if len(name) < 1 {
|
||||||
return errMissingDataName
|
return ErrMissingDataName
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, nameUsed := reader.Data[name]; nameUsed {
|
if _, nameUsed := reader.Data[name]; nameUsed {
|
||||||
return errDataNameConflict
|
return ErrDataNameConflict
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.Data[name] = comp
|
reader.Data[name] = comp
|
||||||
|
|
|
@ -196,8 +196,8 @@ func TestNoName(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = mpr.Parse(); err != errMissingDataName {
|
if err = mpr.Parse(); err != ErrMissingDataName {
|
||||||
t.Errorf("expected the error <%s>, got <%s>", errMissingDataName, err)
|
t.Errorf("expected the error <%s>, got <%s>", ErrMissingDataName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -238,8 +238,8 @@ func TestNoHeader(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = mpr.Parse(); err != errNoHeader {
|
if err = mpr.Parse(); err != ErrNoHeader {
|
||||||
t.Errorf("expected the error <%s>, got <%s>", errNoHeader, err)
|
t.Errorf("expected the error <%s>, got <%s>", ErrNoHeader, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -274,8 +274,8 @@ facebook.com
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Fatalf("unexpected error <%s>", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = mpr.Parse(); err != errDataNameConflict {
|
if err = mpr.Parse(); err != ErrDataNameConflict {
|
||||||
t.Fatalf("expected the error <%s>, got <%s>", errDataNameConflict, err)
|
t.Fatalf("expected the error <%s>, got <%s>", ErrDataNameConflict, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,19 @@ package multipart
|
||||||
// cerr allows you to create constant "const" error with type boxing.
|
// cerr allows you to create constant "const" error with type boxing.
|
||||||
type cerr string
|
type cerr string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
func (err cerr) Error() string {
|
func (err cerr) Error() string {
|
||||||
return string(err)
|
return string(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errMissingDataName is set when a multipart variable/file has no name="..."
|
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
||||||
const errMissingDataName = cerr("data has no name")
|
const ErrMissingDataName = cerr("data has no name")
|
||||||
|
|
||||||
// errDataNameConflict is set when a multipart variable/file name is already used
|
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
||||||
const errDataNameConflict = cerr("data name conflict")
|
const ErrDataNameConflict = cerr("data name conflict")
|
||||||
|
|
||||||
// errNoHeader is set when a multipart variable/file has no (valid) header
|
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
||||||
const errNoHeader = cerr("data has no header")
|
const ErrNoHeader = cerr("data has no header")
|
||||||
|
|
||||||
// Component represents a multipart variable/file
|
// Component represents a multipart variable/file
|
||||||
type Component struct {
|
type Component struct {
|
|
@ -1,7 +1,6 @@
|
||||||
package reqdata
|
package reqdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -25,7 +24,7 @@ func TestSimpleFloat(t *testing.T) {
|
||||||
tcases := []float64{12.3456789, -12.3456789, 0.0000001, -0.0000001}
|
tcases := []float64{12.3456789, -12.3456789, 0.0000001, -0.0000001}
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := parseParameter(tcase)
|
p := parseParameter(tcase)
|
||||||
|
|
||||||
cast, canCast := p.(float64)
|
cast, canCast := p.(float64)
|
||||||
|
@ -46,7 +45,7 @@ func TestSimpleBool(t *testing.T) {
|
||||||
tcases := []bool{true, false}
|
tcases := []bool{true, false}
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := parseParameter(tcase)
|
p := parseParameter(tcase)
|
||||||
|
|
||||||
cast, canCast := p.(bool)
|
cast, canCast := p.(bool)
|
||||||
|
@ -137,7 +136,7 @@ func TestJsonPrimitiveBool(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := parseParameter(tcase.Raw)
|
p := parseParameter(tcase.Raw)
|
||||||
|
|
||||||
cast, canCast := p.(bool)
|
cast, canCast := p.(bool)
|
||||||
|
@ -174,7 +173,7 @@ func TestJsonPrimitiveFloat(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tcase := range tcases {
|
for i, tcase := range tcases {
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
p := parseParameter(tcase.Raw)
|
p := parseParameter(tcase.Raw)
|
||||||
|
|
||||||
cast, canCast := p.(float64)
|
cast, canCast := p.(float64)
|
||||||
|
|
|
@ -6,36 +6,40 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
"github.com/xdrm-io/aicra/internal/multipart"
|
"git.xdrm.io/go/aicra/internal/multipart"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// T represents all data that can be caught from an http request for a specific
|
// Set represents all data that can be caught:
|
||||||
// configuration Service; it features:
|
|
||||||
// - URI (from the URI)
|
// - URI (from the URI)
|
||||||
// - GET (standard url data)
|
// - GET (default url data)
|
||||||
// - POST (from json, form-data, url-encoded)
|
// - POST (from json, form-data, url-encoded)
|
||||||
// - 'application/json' => key-value pair is parsed as json into the map
|
// - 'application/json' => key-value pair is parsed as json into the map
|
||||||
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
||||||
// - 'multipart/form-data' => parse form-data format
|
// - 'multipart/form-data' => parse form-data format
|
||||||
type T struct {
|
type Set struct {
|
||||||
service *config.Service
|
service *config.Service
|
||||||
Data map[string]interface{}
|
|
||||||
|
// contains URL+GET+FORM data with prefixes:
|
||||||
|
// - FORM: no prefix
|
||||||
|
// - URL: '{uri_var}'
|
||||||
|
// - GET: 'GET@' followed by the key in GET
|
||||||
|
Data map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new empty store.
|
// New creates a new empty store.
|
||||||
func New(service *config.Service) *T {
|
func New(service *config.Service) *Set {
|
||||||
return &T{
|
return &Set{
|
||||||
service: service,
|
service: service,
|
||||||
Data: map[string]interface{}{},
|
Data: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetURI parameters
|
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
||||||
func (i *T) GetURI(req http.Request) error {
|
func (i *Set) ExtractURI(req *http.Request) error {
|
||||||
uriparts := config.SplitURL(req.URL.RequestURI())
|
uriparts := config.SplitURL(req.URL.RequestURI())
|
||||||
|
|
||||||
for _, capture := range i.service.Captures {
|
for _, capture := range i.service.Captures {
|
||||||
|
@ -50,115 +54,122 @@ func (i *T) GetURI(req http.Request) error {
|
||||||
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
|
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
parsed := parseParameter(value)
|
parsed := parseParameter(value)
|
||||||
|
|
||||||
|
// check type
|
||||||
cast, valid := capture.Ref.Validator(parsed)
|
cast, valid := capture.Ref.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store cast value in 'Set'
|
||||||
i.Data[capture.Ref.Rename] = cast
|
i.Data[capture.Ref.Rename] = cast
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQuery data from the url query parameters
|
// ExtractQuery data from the url query parameters
|
||||||
func (i *T) GetQuery(req http.Request) error {
|
func (i *Set) ExtractQuery(req *http.Request) error {
|
||||||
query := req.URL.Query()
|
query := req.URL.Query()
|
||||||
|
|
||||||
for name, param := range i.service.Query {
|
for name, param := range i.service.Query {
|
||||||
values, exist := query[name]
|
value, exist := query[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
if !exist {
|
if !exist {
|
||||||
if !param.Optional {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed interface{}
|
// parse parameter
|
||||||
|
parsed := parseParameter(value)
|
||||||
// consider element instead of slice or elements when only 1
|
|
||||||
if len(values) == 1 {
|
|
||||||
parsed = parseParameter(values[0])
|
|
||||||
} else { // consider slice
|
|
||||||
parsed = parseParameter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// check type
|
||||||
cast, valid := param.Validator(parsed)
|
cast, valid := param.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
i.Data[param.Rename] = cast
|
i.Data[param.Rename] = cast
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetForm parameters the from request
|
// ExtractForm data from request
|
||||||
|
//
|
||||||
// - parse 'form-data' if not supported for non-POST requests
|
// - parse 'form-data' if not supported for non-POST requests
|
||||||
// - parse 'x-www-form-urlencoded'
|
// - parse 'x-www-form-urlencoded'
|
||||||
// - parse 'application/json'
|
// - parse 'application/json'
|
||||||
func (i *T) GetForm(req http.Request) error {
|
func (i *Set) ExtractForm(req *http.Request) error {
|
||||||
|
|
||||||
|
// ignore GET method
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ct := req.Header.Get("Content-Type")
|
contentType := req.Header.Get("Content-Type")
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(ct, "application/json"):
|
|
||||||
err := i.parseJSON(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
|
// parse json
|
||||||
err := i.parseUrlencoded(req)
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
if err != nil {
|
return i.parseJSON(req)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasPrefix(ct, "multipart/form-data; boundary="):
|
|
||||||
err := i.parseMultipart(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail on at least 1 mandatory form param when there is no body
|
// parse urlencoded
|
||||||
for name, param := range i.service.Form {
|
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||||
_, exists := i.Data[param.Rename]
|
return i.parseUrlencoded(req)
|
||||||
if !exists && !param.Optional {
|
|
||||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse multipart
|
||||||
|
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||||
|
return i.parseMultipart(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to parse
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJSON parses JSON from the request body inside 'Form'
|
// parseJSON parses JSON from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *T) parseJSON(req http.Request) error {
|
func (i *Set) parseJSON(req *http.Request) error {
|
||||||
var parsed map[string]interface{}
|
|
||||||
|
parsed := make(map[string]interface{}, 0)
|
||||||
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
decoder := json.NewDecoder(req.Body)
|
||||||
err := decoder.Decode(&parsed)
|
if err := decoder.Decode(&parsed); err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
|
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
value, exist := parsed[name]
|
value, exist := parsed[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
cast, valid := param.Validator(value)
|
cast, valid := param.Validator(value)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
i.Data[param.Rename] = cast
|
i.Data[param.Rename] = cast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,31 +178,35 @@ func (i *T) parseJSON(req http.Request) error {
|
||||||
|
|
||||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *T) parseUrlencoded(req http.Request) error {
|
func (i *Set) parseUrlencoded(req *http.Request) error {
|
||||||
|
// use http.Request interface
|
||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
values, exist := req.PostForm[name]
|
value, exist := req.PostForm[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed interface{}
|
// parse parameter
|
||||||
|
parsed := parseParameter(value)
|
||||||
// consider element instead of slice or elements when only 1
|
|
||||||
if len(values) == 1 {
|
|
||||||
parsed = parseParameter(values[0])
|
|
||||||
} else { // consider slice
|
|
||||||
parsed = parseParameter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// check type
|
||||||
cast, valid := param.Validator(parsed)
|
cast, valid := param.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
i.Data[param.Rename] = cast
|
i.Data[param.Rename] = cast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,33 +215,46 @@ func (i *T) parseUrlencoded(req http.Request) error {
|
||||||
|
|
||||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||||
// and 'Set'
|
// and 'Set'
|
||||||
func (i *T) parseMultipart(req http.Request) error {
|
func (i *Set) parseMultipart(req *http.Request) error {
|
||||||
|
|
||||||
|
// 1. create reader
|
||||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||||
mpr, err := multipart.NewReader(req.Body, boundary)
|
mpr, err := multipart.NewReader(req.Body, boundary)
|
||||||
if err == io.EOF {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mpr.Parse()
|
// 2. parse multipart
|
||||||
if err != nil {
|
if err = mpr.Parse(); err != nil {
|
||||||
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, param := range i.service.Form {
|
for name, param := range i.service.Form {
|
||||||
component, exist := mpr.Data[name]
|
component, exist := mpr.Data[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
if !exist {
|
if !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse parameter
|
||||||
parsed := parseParameter(string(component.Data))
|
parsed := parseParameter(string(component.Data))
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
cast, valid := param.Validator(parsed)
|
cast, valid := param.Validator(parsed)
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store cast value
|
||||||
i.Data[param.Rename] = cast
|
i.Data[param.Rename] = cast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,47 +266,58 @@ func (i *T) parseMultipart(req http.Request) error {
|
||||||
// - []string : return array of json elements
|
// - []string : return array of json elements
|
||||||
// - string : return json if valid, else return raw string
|
// - string : return json if valid, else return raw string
|
||||||
func parseParameter(data interface{}) interface{} {
|
func parseParameter(data interface{}) interface{} {
|
||||||
rt := reflect.TypeOf(data)
|
dtype := reflect.TypeOf(data)
|
||||||
rv := reflect.ValueOf(data)
|
dvalue := reflect.ValueOf(data)
|
||||||
|
|
||||||
switch rt.Kind() {
|
switch dtype.Kind() {
|
||||||
|
|
||||||
// []string -> recursive
|
/* (1) []string -> recursive */
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if rv.Len() == 0 {
|
|
||||||
|
// 1. ignore empty
|
||||||
|
if dvalue.Len() == 0 {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
slice := make([]interface{}, rv.Len())
|
// 2. parse each element recursively
|
||||||
for i, l := 0, rv.Len(); i < l; i++ {
|
result := make([]interface{}, dvalue.Len())
|
||||||
element := rv.Index(i)
|
|
||||||
slice[i] = parseParameter(element.Interface())
|
|
||||||
}
|
|
||||||
return slice
|
|
||||||
|
|
||||||
// string -> parse as json
|
for i, l := 0, dvalue.Len(); i < l; i++ {
|
||||||
// keep as string if invalid json
|
element := dvalue.Index(i)
|
||||||
|
result[i] = parseParameter(element.Interface())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
/* (2) string -> parse */
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
var cast interface{}
|
|
||||||
wrapper := fmt.Sprintf("{\"wrapped\":%s}", rv.String())
|
// build json wrapper
|
||||||
err := json.Unmarshal([]byte(wrapper), &cast)
|
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 {
|
if err != nil {
|
||||||
return rv.String()
|
return dvalue.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
mapval, ok := cast.(map[string]interface{})
|
mapval, ok := result.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return rv.String()
|
return dvalue.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapped, ok := mapval["wrapped"]
|
wrapped, ok := mapval["wrapped"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return rv.String()
|
return dvalue.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
// any type -> unchanged
|
|
||||||
default:
|
|
||||||
return rv.Interface()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* (3) NIL if unknown type */
|
||||||
|
return dvalue.Interface()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/internal/config"
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEmptyService() *config.Service {
|
func getEmptyService() *config.Service {
|
||||||
|
@ -131,15 +131,17 @@ func TestStoreWithUri(t *testing.T) {
|
||||||
store := New(service)
|
store := New(service)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
||||||
err := store.GetURI(*req)
|
err := store.ExtractURI(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(store.Data) != len(service.Input) {
|
if len(store.Data) != len(service.Input) {
|
||||||
|
@ -181,14 +183,14 @@ func TestExtractQuery(t *testing.T) {
|
||||||
Query: "a",
|
Query: "a",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a"},
|
ServiceParam: []string{"a"},
|
||||||
Query: "a&b",
|
Query: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "missing"},
|
ServiceParam: []string{"a", "missing"},
|
||||||
|
@ -202,58 +204,61 @@ func TestExtractQuery(t *testing.T) {
|
||||||
Query: "a&b",
|
Query: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {""}},
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a"},
|
ServiceParam: []string{"a"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=",
|
Query: "a=",
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "b"},
|
ServiceParam: []string{"a", "b"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=&b=x",
|
Query: "a=&b=x",
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {"x"}},
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "c"},
|
ServiceParam: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=b&c=d",
|
Query: "a=b&c=d",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b"}, {"d"}},
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParam: []string{"a", "c"},
|
ServiceParam: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Query: "a=b&c=d&a=x",
|
Query: "a=b&c=d&a=x",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b", "x"}, {"d"}},
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("request[%d]", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
|
||||||
store := New(getServiceWithQuery(test.ServiceParam...))
|
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||||
err := store.GetQuery(*req)
|
err := store.ExtractQuery(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -261,7 +266,8 @@ func TestExtractQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", 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 {
|
for pi, pName := range test.ParamNames {
|
||||||
|
@ -270,35 +276,29 @@ func TestExtractQuery(t *testing.T) {
|
||||||
t.Run(pName, func(t *testing.T) {
|
t.Run(pName, func(t *testing.T) {
|
||||||
param, isset := store.Data[pName]
|
param, isset := store.Data[pName]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("param does not exist")
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// single value, should return a single element
|
|
||||||
if len(values) == 1 {
|
|
||||||
cast, canCast := param.(string)
|
|
||||||
if !canCast {
|
|
||||||
t.Fatalf("should return a string (got '%v')", cast)
|
|
||||||
}
|
|
||||||
if values[0] != cast {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiple values, should return a slice
|
|
||||||
cast, canCast := param.([]interface{})
|
cast, canCast := param.([]interface{})
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Fatalf("should return a []string (got '%v')", cast)
|
t.Errorf("should return a []string (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
if len(cast) != len(values) {
|
||||||
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for vi, value := range values {
|
for vi, value := range values {
|
||||||
if value != cast[vi] {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -324,9 +324,11 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||||
|
|
||||||
// defer req.Body.Close()
|
// defer req.Body.Close()
|
||||||
store := New(nil)
|
store := New(nil)
|
||||||
err := store.GetForm(*req)
|
err := store.ExtractForm(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestExtractFormUrlEncoded(t *testing.T) {
|
func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
|
@ -357,14 +359,14 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
URLEncoded: "a",
|
URLEncoded: "a",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a"},
|
ServiceParams: []string{"a"},
|
||||||
URLEncoded: "a&b",
|
URLEncoded: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "missing"},
|
ServiceParams: []string{"a", "missing"},
|
||||||
|
@ -378,35 +380,35 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
URLEncoded: "a&b",
|
URLEncoded: "a&b",
|
||||||
Err: nil,
|
Err: nil,
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {""}},
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a"},
|
ServiceParams: []string{"a"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=",
|
URLEncoded: "a=",
|
||||||
ParamNames: []string{"a"},
|
ParamNames: []string{"a"},
|
||||||
ParamValues: [][]string{{""}},
|
ParamValues: [][]string{[]string{""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "b"},
|
ServiceParams: []string{"a", "b"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=&b=x",
|
URLEncoded: "a=&b=x",
|
||||||
ParamNames: []string{"a", "b"},
|
ParamNames: []string{"a", "b"},
|
||||||
ParamValues: [][]string{{""}, {"x"}},
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "c"},
|
ServiceParams: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=b&c=d",
|
URLEncoded: "a=b&c=d",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b"}, {"d"}},
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ServiceParams: []string{"a", "c"},
|
ServiceParams: []string{"a", "c"},
|
||||||
Err: nil,
|
Err: nil,
|
||||||
URLEncoded: "a=b&c=d&a=x",
|
URLEncoded: "a=b&c=d&a=x",
|
||||||
ParamNames: []string{"a", "c"},
|
ParamNames: []string{"a", "c"},
|
||||||
ParamValues: [][]string{{"b", "x"}, {"d"}},
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,20 +420,23 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
|
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
err := store.GetForm(*req)
|
err := store.ExtractForm(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -439,7 +444,8 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", 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 {
|
for pi, key := range test.ParamNames {
|
||||||
|
@ -448,35 +454,29 @@ func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
t.Run(key, func(t *testing.T) {
|
t.Run(key, func(t *testing.T) {
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("param does not exist")
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// single value, should return a single element
|
|
||||||
if len(values) == 1 {
|
|
||||||
cast, canCast := param.(string)
|
|
||||||
if !canCast {
|
|
||||||
t.Fatalf("should return a string (got '%v')", cast)
|
|
||||||
}
|
|
||||||
if values[0] != cast {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiple values, should return a slice
|
|
||||||
cast, canCast := param.([]interface{})
|
cast, canCast := param.([]interface{})
|
||||||
if !canCast {
|
if !canCast {
|
||||||
t.Fatalf("should return a []string (got '%v')", cast)
|
t.Errorf("should return a []interface{} (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
if len(cast) != len(values) {
|
||||||
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for vi, value := range values {
|
for vi, value := range values {
|
||||||
if value != cast[vi] {
|
|
||||||
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -563,20 +563,23 @@ func TestJsonParameters(t *testing.T) {
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
err := store.GetForm(*req)
|
err := store.ExtractForm(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -584,7 +587,8 @@ func TestJsonParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", 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 {
|
for pi, pName := range test.ParamNames {
|
||||||
|
@ -595,7 +599,8 @@ func TestJsonParameters(t *testing.T) {
|
||||||
|
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("store should contain element with key '%s'", key)
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,11 +610,13 @@ func TestJsonParameters(t *testing.T) {
|
||||||
paramValueType := reflect.TypeOf(param)
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
if valueType != paramValueType {
|
if valueType != paramValueType {
|
||||||
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if paramValue != value {
|
if paramValue != value {
|
||||||
t.Fatalf("should return %v (got '%v')", value, paramValue)
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -713,20 +720,23 @@ x
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
store := New(getServiceWithForm(test.ServiceParams...))
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
err := store.GetForm(*req)
|
err := store.ExtractForm(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if test.Err != nil {
|
if test.Err != nil {
|
||||||
if !errors.Is(err, test.Err) {
|
if !errors.Is(err, test.Err) {
|
||||||
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected error <%s>", err)
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
if len(store.Data) != 0 {
|
if len(store.Data) != 0 {
|
||||||
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// no param to check
|
// no param to check
|
||||||
|
@ -734,7 +744,8 @@ x
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", 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 {
|
for pi, key := range test.ParamNames {
|
||||||
|
@ -744,7 +755,8 @@ x
|
||||||
|
|
||||||
param, isset := store.Data[key]
|
param, isset := store.Data[key]
|
||||||
if !isset {
|
if !isset {
|
||||||
t.Fatalf("store should contain element with key '%s'", key)
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -754,11 +766,13 @@ x
|
||||||
paramValueType := reflect.TypeOf(param)
|
paramValueType := reflect.TypeOf(param)
|
||||||
|
|
||||||
if valueType != paramValueType {
|
if valueType != paramValueType {
|
||||||
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if paramValue != value {
|
if paramValue != value {
|
||||||
t.Fatalf("should return %v (got '%v')", value, paramValue)
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
60
response.go
60
response.go
|
@ -1,60 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
// response for an service call
|
|
||||||
type response struct {
|
|
||||||
Data map[string]interface{}
|
|
||||||
Status int
|
|
||||||
err api.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// newResponse creates an empty response.
|
|
||||||
func newResponse() *response {
|
|
||||||
return &response{
|
|
||||||
Status: http.StatusOK,
|
|
||||||
Data: make(map[string]interface{}),
|
|
||||||
err: api.ErrFailure,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithError sets the response error
|
|
||||||
func (r *response) WithError(err api.Err) *response {
|
|
||||||
r.err = err
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithValue sets a response value
|
|
||||||
func (r *response) WithValue(name string, value interface{}) *response {
|
|
||||||
r.Data[name] = value
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON generates the JSON representation of the response
|
|
||||||
//
|
|
||||||
// implements json.Marshaler
|
|
||||||
func (r *response) MarshalJSON() ([]byte, error) {
|
|
||||||
fmt := make(map[string]interface{})
|
|
||||||
for k, v := range r.Data {
|
|
||||||
fmt[k] = v
|
|
||||||
}
|
|
||||||
fmt["error"] = r.err
|
|
||||||
return json.Marshal(fmt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP writes the response representation back to the http.ResponseWriter
|
|
||||||
//
|
|
||||||
// implements http.Handler
|
|
||||||
func (res *response) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
w.WriteHeader(res.err.Status)
|
|
||||||
encoded, err := json.Marshal(res)
|
|
||||||
if err == nil {
|
|
||||||
w.Write(encoded)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
package aicra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/xdrm-io/aicra/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func printEscaped(raw string) string {
|
|
||||||
raw = strings.ReplaceAll(raw, "\n", "\\n")
|
|
||||||
raw = strings.ReplaceAll(raw, "\r", "\\r")
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResponseJSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tt := []struct {
|
|
||||||
name string
|
|
||||||
err api.Err
|
|
||||||
data map[string]interface{}
|
|
||||||
json string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty success response",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty failure response",
|
|
||||||
err: api.ErrFailure,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":1,"reason":"it failed"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty unknown error response",
|
|
||||||
err: api.ErrUnknown,
|
|
||||||
data: map[string]interface{}{},
|
|
||||||
json: `{"error":{"code":-1,"reason":"unknown error"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data before err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"a": 12},
|
|
||||||
json: `{"a":12,"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data right before err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"e": 12},
|
|
||||||
json: `{"e":12,"error":{"code":0,"reason":"all right"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data right after err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"f": 12},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"},"f":12}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data after err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"z": 12},
|
|
||||||
json: `{"error":{"code":0,"reason":"all right"},"z":12}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with data around err",
|
|
||||||
err: api.ErrSuccess,
|
|
||||||
data: map[string]interface{}{"d": "before", "f": "after"},
|
|
||||||
json: `{"d":"before","error":{"code":0,"reason":"all right"},"f":"after"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
res := newResponse().WithError(tc.err)
|
|
||||||
for k, v := range tc.data {
|
|
||||||
res.WithValue(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot marshal to json: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(raw) != tc.json {
|
|
||||||
t.Fatalf("mismatching json:\nexpect: %v\nactual: %v", printEscaped(tc.json), printEscaped(string(raw)))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
|
"git.xdrm.io/go/aicra/dynamic"
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server represents an AICRA instance featuring: type checkers, services
|
||||||
|
type Server struct {
|
||||||
|
config *config.Server
|
||||||
|
handlers []*handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a framework instance from a configuration file
|
||||||
|
func New(configPath string, dtypes ...datatype.T) (*Server, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
configFile io.ReadCloser
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1. init instance
|
||||||
|
var i = &Server{
|
||||||
|
config: nil,
|
||||||
|
handlers: make([]*handler, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. open config file
|
||||||
|
configFile, err = os.Open(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer configFile.Close()
|
||||||
|
|
||||||
|
// 3. load configuration
|
||||||
|
i.config, err = config.Parse(configFile, dtypes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sets a new handler for an HTTP method to a path
|
||||||
|
func (s *Server) Handle(method, path string, fn dynamic.HandlerFn) error {
|
||||||
|
// find associated service
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, err := createHandler(method, path, *found, fn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.handlers = append(s.handlers, handler)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHTTPServer converts the server to a http server
|
||||||
|
func (s Server) ToHTTPServer() (*httpServer, error) {
|
||||||
|
|
||||||
|
// check if handlers are missing
|
||||||
|
for _, service := range s.config.Services {
|
||||||
|
found := false
|
||||||
|
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
|
||||||
|
httpServer := httpServer(s)
|
||||||
|
return &httpServer, nil
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package aicra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handledMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
|
// Prints an error as HTTP response
|
||||||
|
func logError(res *api.Response) {
|
||||||
|
log.Printf("[http.fail] %v\n", res)
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
package validator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AnyType makes the "any" type available in the aicra configuration
|
|
||||||
// It considers valid any value
|
|
||||||
type AnyType struct{}
|
|
||||||
|
|
||||||
// GoType returns the interface{} type
|
|
||||||
func (AnyType) GoType() reflect.Type {
|
|
||||||
return reflect.TypeOf(interface{}(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator that considers any value valid
|
|
||||||
func (AnyType) Validator(typename string, avail ...Type) ValidateFunc {
|
|
||||||
if typename != "any" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(value interface{}) (interface{}, bool) {
|
|
||||||
return value, true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package validator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateFunc returns whether a given value fulfills the datatype and casts
|
|
||||||
// the value into a go type.
|
|
||||||
//
|
|
||||||
// for example, if a validator checks for upper case strings, whether the value
|
|
||||||
// is a []byte, a string or a []rune, if the value matches is all upper-case, it
|
|
||||||
// will be cast into a go type, say, string.
|
|
||||||
type ValidateFunc func(value interface{}) (cast interface{}, valid bool)
|
|
||||||
|
|
||||||
// Type defines an available in/out parameter "type" for the aicra configuration
|
|
||||||
//
|
|
||||||
// A Type maps to a go type in order to generate the handler signature from the
|
|
||||||
// aicra configuration
|
|
||||||
//
|
|
||||||
// A Type returns a custom validator when the typename matches
|
|
||||||
type Type interface {
|
|
||||||
// Validator function when the typename matches. It must return nil when the
|
|
||||||
// typename does not match
|
|
||||||
//
|
|
||||||
// The `typename` argument has to match types used in your aicra configuration
|
|
||||||
// in parameter definitions ("in", "out") and in the "type" json field.
|
|
||||||
//
|
|
||||||
// basic example:
|
|
||||||
// - `IntType.Validator("string")`` should return nil
|
|
||||||
// - `IntType.Validator("int")`` should return its ValidateFunc
|
|
||||||
//
|
|
||||||
// The `typename` is not returned by a simple method i.e. `TypeName() string`
|
|
||||||
// because it allows for validation relative to the typename, for instance:
|
|
||||||
// - `VarcharType.Validator("varchar")` valides any string
|
|
||||||
// - `VarcharType.Validator("varchar(2)")` validates any string of 2
|
|
||||||
// characters
|
|
||||||
// - `VarcharType.Validator("varchar(1,3)")` validates any string
|
|
||||||
// with a length between 1 and 3
|
|
||||||
//
|
|
||||||
// The `avail` argument represents all other available Types. It allows a
|
|
||||||
// Type to use other available Types internally.
|
|
||||||
//
|
|
||||||
// recursive example: slices
|
|
||||||
// - `SliceType.Validator("[]int", avail...)` validates a slice containing
|
|
||||||
// values that are valide to the `IntType`
|
|
||||||
// - `SliceType.Validator("[]varchar", avail...)` validates a slice containing
|
|
||||||
// values that are valid to the `VarcharType`
|
|
||||||
//
|
|
||||||
// and so on.. this works for maps, structs, etc
|
|
||||||
Validator(typename string, avail ...Type) ValidateFunc
|
|
||||||
|
|
||||||
// GoType must return the go type associated with the output type of ValidateFunc.
|
|
||||||
// It is used to define handlers' signature from the configuration file.
|
|
||||||
GoType() reflect.Type
|
|
||||||
}
|
|
Loading…
Reference in New Issue