improvements, fixes, update to go 1.16 #16

Merged
xdrm-brackets merged 7 commits from refactor/go1.16 into 0.3.0 2021-03-28 17:44:59 +00:00
1 changed files with 215 additions and 124 deletions
Showing only changes of commit 546130cfd0 - Show all commits

243
README.md
View File

@ -6,50 +6,59 @@
[![Go doc](https://godoc.org/git.xdrm.io/go/aicra?status.svg)](https://godoc.org/git.xdrm.io/go/aicra) [![Go doc](https://godoc.org/git.xdrm.io/go/aicra?status.svg)](https://godoc.org/git.xdrm.io/go/aicra)
[![Build Status](https://drone.xdrm.io/api/badges/go/aicra/status.svg)](https://drone.xdrm.io/go/aicra) [![Build Status](https://drone.xdrm.io/api/badges/go/aicra/status.svg)](https://drone.xdrm.io/go/aicra)
----
Aicra is a *configuration-driven* REST API engine written in Go. `aicra` is a lightweight and idiomatic API engine for building Go services. It's especially good at helping you write large REST API services that remain maintainable as your project grows.
Most of the management is done for you using a configuration file describing your API. you're left with implementing : 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 configuration file to drive the server. The configuration format describes the whole API: routes, input arguments, expected output, permissions, etc.
- handlers
- optionnally middle-wares (_e.g. authentication, csrf_)
- and optionnally your custom type checkers to check input parameters
> A example project is available [here](https://git.xdrm.io/go/articles-api) TL;DR: `aicra` is a fast configuration-driven REST API engine.
## Table of contents Repetitive tasks is automatically processed by `aicra` from your configuration file, you just have to implement your handlers.
The engine automates :
- catching input data (_url, query, form-data, json, url-encoded_)
- handling missing input data (_required arguments_)
- handling input data validation
- checking for mandatory output parameters
- checking for missing method implementations
- checking for handler signature (input and output arguments)
> An example project is available [here](https://git.xdrm.io/go/articles-api)
### Table of contents
<!-- toc --> <!-- toc -->
- [I/ Installation](#i-installation) * [Installation](#installation)
- [II/ Usage](#ii-usage) - [Usage](#usage)
* [1) Build a server](#1-build-a-server) - [Create a server](#create-a-server)
* [2) API Configuration](#2-api-configuration) - [Create a handler](#create-a-handler)
- [Definition](#definition) - [Configuration](#configuration)
+ [Input Arguments](#input-arguments) - [Global format](#global-format)
- [1. Input types](#1-input-types) * [Input section](#input-section)
- [2. Global Format](#2-global-format) + [Format](#format)
- [III/ Change Log](#iii-change-log) - [Example](#example)
- [Changelog](#changelog)
<!-- tocstop --> <!-- tocstop -->
## I/ Installation ## Installation
You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.14**. You need a recent machine with `go` [installed](https://golang.org/doc/install). The package has not been tested under **go1.14**.
```bash ```bash
go get -u git.xdrm.io/go/aicra/cmd/aicra go get -u git.xdrm.io/go/aicra
``` ```
The library should now be available as `git.xdrm.io/go/aicra` in your imports.
# Usage
## II/ Usage #### Create a server
The code below sets up and creates an HTTP server from the `api.json` configuration.
### 1) Build a server
Here is some sample code that builds and sets up an aicra server using your api configuration file.
```go ```go
package main package main
@ -65,22 +74,20 @@ import (
) )
func main() { func main() {
builder := &aicra.Builder{} builder := &aicra.Builder{}
// add datatypes your api uses // register available validators
builder.AddType(builtin.BoolDataType{}) builder.AddType(builtin.BoolDataType{})
builder.AddType(builtin.UintDataType{}) builder.AddType(builtin.UintDataType{})
builder.AddType(builtin.StringDataType{}) builder.AddType(builtin.StringDataType{})
// load your configuration
config, err := os.Open("./api.json") config, err := os.Open("./api.json")
if err != nil { if err != nil {
log.Fatalf("cannot open config: %s", err) log.Fatalf("cannot open config: %s", err)
} }
// pass your configuration
err = builder.Setup(config) err = builder.Setup(config)
config.Close() config.Close() // free config file
if err != nil { if err != nil {
log.Fatalf("invalid config: %s", err) log.Fatalf("invalid config: %s", err)
} }
@ -89,87 +96,123 @@ func main() {
builder.Bind(http.MethodGet, "/user/{id}", getUserById) builder.Bind(http.MethodGet, "/user/{id}", getUserById)
builder.Bind(http.MethodGet, "/user/{id}/username", getUsernameByID) builder.Bind(http.MethodGet, "/user/{id}/username", getUsernameByID)
// build the server and start listening // build the handler and start listening
server, err := builder.Build() handler, err := builder.Build()
if err != nil { if err != nil {
log.Fatalf("cannot build server: %s", err) log.Fatalf("cannot build handler: %s", err)
} }
http.ListenAndServe("localhost:8080", server) 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{},
// aicra handler
Handler: handler,
}
server.ListenAndServe()
} }
``` ```
Here is an example handler #### Create a handler
The code below implements a simple handler.
```go ```go
// "in": {
// "Input1": { "info": "...", "type": "int" },
// "Input2": { "info": "...", "type": "?string" }
// },
type req struct{ type req struct{
Param1 int Input1 int
Param3 *string // optional are pointers Input2 *string // optional are pointers
} }
// "out": {
// "Output1": { "info": "...", "type": "string" },
// "Output2": { "info": "...", "type": "bool" }
// }
type res struct{ type res struct{
Output1 string Output1 string
Output2 bool Output2 bool
} }
func myHandler(r req) (*res, api.Error) { func myHandler(r req) (*res, api.Err) {
err := doSomething() err := doSomething()
if err != nil { if err != nil {
return nil, api.ErrorFailure 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 will not start.
The `api.Err` type automatically maps to HTTP status codes and error descriptions that will be sent to the client as json; client will then always have to manage the same format.
```json
{
"error": {
"code": 0,
"reason": "all right"
} }
return &res{}, api.ErrorSuccess
} }
``` ```
### 2) API Configuration # Configuration
The whole api behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [template](https://git.xdrm.io/go/articles-api/src/master/api.json). This file defines : The whole api behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [configuration](https://git.xdrm.io/go/articles-api/src/master/api.json).
The configuration file defines :
- routes and their methods - routes and their methods
- every input for each method (called *argument*) - every input argument for each method
- every output for each method - every output for each method
- scope permissions (list of permissions needed by clients) - scope permissions (list of permissions required by clients)
- input policy : - input policy :
- type of argument (_c.f. data types_) - type of argument (_c.f. data types_)
- required/optional - required/optional
- variable renaming - variable renaming
#### Format #### Global format
The root of the json file must be an array containing your requests definitions. For each, you will have to create fields described in the table above. The root of the json file must feature an array containing your requests definitions. For each, you will have to create fields described in the table above.
| field path | description | example | - `info`: Short description of the method
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | - `in`: List of arguments that the clients will have to provide. [Read more](#input-arguments).
| `info` | A short human-readable description of what the method does | `create a new user` | - `out`: List of output data that your controllers will output. It has the same syntax as the `in` field but optional parameters are not allowed.
| `scope` | A 2-dimensional array of permissions. The first dimension can be translated to a **or** operator, the second dimension as a **and**. It allows you to combine permissions in complex ways. | `[["A", "B"], ["C", "D"]]` can be translated to : this method needs users to have permissions (A **and** B) **or** (C **and** D) | - `scope`: A 2-dimensional array of permissions. The first level means **or**, the second means **and**. It allows to combine permissions in complex ways.
| `in` | The list of arguments that the clients will have to provide. [Read more](#input-arguments). | | - Example: `[["A", "B"], ["C", "D"]]` translates to : this method requires users to have permissions (A **and** B) **or** (C **and** D)
| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but optional parameters are not allowed |
### Input Arguments ##### Input section
Input arguments defines what data from the HTTP request the method needs. Aicra is able to extract 3 types of data : Input arguments defines what data from the HTTP request the method requires. `aicra` is able to extract 3 types of data :
- **URI** - data from inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part. - **URI** - data from inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part.
- **Query** - data formatted at the end of the URL following the standard [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax. - **Query** - data 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. - **Form** - data send from the body of the request ; it can be extracted in 3 ways:
- **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. - _URL encoded_: data send in the body following the [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax.
- **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. - _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 send in the body as a json object ; each key being a variable name, each value its content. Note that the 'Content-Type' header must be set to `application/json` for the API to use it.
> For Form data, the 3 methods can be used at once for different arguments; for instance if you need to send a file to an aicra server as well as other parameters, you can use JSON for parameters and Multipart for the file.
###### Format
The `in` field describes 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 :
- an **URI** variable `{var}` from your request route must be named `{var}` in the `in` section
- a variable `var` in the **Query** has to be named `GET@var` in the `in` section
#### 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 :
>
> - the **URI** variable `{id}` from your request route must be named `{id}`.
> - the variable `somevar` in the **Query** has to be names `GET@somevar`.
**Example**
In this example we want 3 arguments :
#### Example
```json ```json
[ [
{ {
@ -178,9 +221,9 @@ In this example we want 3 arguments :
"scope": [["author"]], "scope": [["author"]],
"info": "updates an article", "info": "updates an article",
"in": { "in": {
"{id}": { "info": "article id", "type": "int", "name": "article_id" }, "{id}": { "info": "...", "type": "int", "name": "id" },
"GET@title": { "info": "new article title", "type": "?string", "name": "title" }, "GET@title": { "info": "...", "type": "?string", "name": "title" },
"content": { "info": "new article content", "type": "string" } "content": { "info": "...", "type": "string" }
}, },
"out": { "out": {
"id": { "info": "updated article id", "type": "uint" }, "id": { "info": "updated article id", "type": "uint" },
@ -191,6 +234,54 @@ In this example we want 3 arguments :
] ]
``` ```
- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler. 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.
- 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`. 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`.
- 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`. 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`.
# Changelog
- [x] human-readable json configuration
- [x] nested routes (*i.e. `/user/{id}` and `/user/post/{id}`*)
- [x] nested URL arguments (*i.e. `/user/{id}` and `/user/{uid}/post/{id}`*)
- [x] useful http methods: GET, POST, PUT, DELETE
- [ ] add support for PATCH method
- [ ] add support for OPTIONS method
- [ ] it might be interesting to generate the list of allowed methods from the configuration
- [ ] add CORS support
- [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] parameter renaming
- [x] generic type check (*i.e. you can add custom types alongside built-in ones*)
- [x] built-in types
- [x] `any` - matches any value
- [x] `int` - see go types
- [x] `uint` - see go types
- [x] `float` - see go types
- [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`
- [ ] `[]a` - array containing **only** elements matching `a` type
- [ ] `a[b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
- [x] generic handler implementation
- [x] response interface
- [x] generic errors that automatically formats into response
- [x] builtin errors
- [x] possibility to add custom errors
- [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