11 KiB
| aicra |
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.
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.
TL;DR: aicra
is a fast configuration-driven REST API engine.
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
Table of contents
- Usage - Create a server - Create a handler
- Configuration - Global format * Input section + Format - Example
- Changelog
Installation
You need a recent machine with go
installed. The package has not been tested under go1.14.
go get -u git.xdrm.io/go/aicra
Usage
Create a server
The code below sets up and creates an HTTP server from the api.json
configuration.
package main
import (
"log"
"net/http"
"os"
"git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype/builtin"
)
func main() {
builder := &aicra.Builder{}
// register available validators
builder.AddType(builtin.BoolDataType{})
builder.AddType(builtin.UintDataType{})
builder.AddType(builtin.StringDataType{})
// load your configuration
config, err := os.Open("./api.json")
if err != nil {
log.Fatalf("cannot open config: %s", err)
}
err = builder.Setup(config)
config.Close() // free config file
if err != nil {
log.Fatalf("invalid config: %s", err)
}
// bind your handlers
builder.Bind(http.MethodGet, "/user/{id}", getUserById)
builder.Bind(http.MethodGet, "/user/{id}/username", getUsernameByID)
// build the handler and start listening
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
.
func main() {
server := &http.Server{
Addr: "localhost:8080",
TLSConfig: tls.Config{},
// aicra handler
Handler: handler,
}
server.ListenAndServe()
}
Create a handler
The code below implements a simple handler.
// "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(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 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.
{
"error": {
"code": 0,
"reason": "all right"
}
}
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 configuration.
The configuration file defines :
- routes and their methods
- every input argument for each method
- every output for each method
- scope permissions (list of permissions required by clients)
- input policy :
- type of argument (c.f. data types)
- required/optional
- variable renaming
Global format
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.
info
: Short description of the methodin
: List of arguments that the clients will have to provide. Read more.out
: List of output data that your controllers will output. It has the same syntax as thein
field but optional parameters are not allowed.scope
: A 2-dimensional array of permissions. The first level means or, the second means and. It allows to combine permissions in complex ways.- Example:
[["A", "B"], ["C", "D"]]
translates to : this method requires users to have permissions (A and B) or (C and D)
- Example:
Input section
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. - Query - data at the end of the URL following the standard HTTP Query syntax.
- Form - data send from the body of the request ; it can be extracted in 3 ways:
- URL encoded: data send in the body following the HTTP Query syntax.
- Multipart: data send in the body with a dedicated format. 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 thein
section - a variable
var
in the Query has to be namedGET@var
in thein
section
Example
[
{
"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" }
}
}
]
{id}
is extracted from the end of the URI and is a number compliant with theint
type checker. It is renamedid
, this new name will be sent to the handler.GET@title
is extracted from the query (e.g. http://host/uri?get-var=value). It must be a validstring
or not given at all (the?
at the beginning of the type tells that the argument is optional) ; it will be namedtitle
.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 namecontent
.
Changelog
- human-readable json configuration
- nested routes (i.e.
/user/{id}
and/user/post/{id}
) - nested URL arguments (i.e.
/user/{id}
and/user/{uid}/post/{id}
) - 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
- manage request data extraction:
- URL slash-separated strings
- HTTP Query named parameters
- manage array format
- body parameters
- multipart/form-data (variables and file uploads)
- application/x-www-form-urlencoded
- application/json
- required vs. optional parameters with a default value
- parameter renaming
- generic type check (i.e. you can add custom types alongside built-in ones)
- built-in types
any
- matches any valueint
- see go typesuint
- see go typesfloat
- see go typesstring
- any textstring(len)
- any string with a length of exactlylen
charactersstring(min, max)
- any string with a length betweenmin
andmax
[]a
- array containing only elements matchinga
typea[b]
- map containing only keys of typea
and values of typeb
(a or b can be ommited)
- generic handler implementation
- response interface
- generic errors that automatically formats into response
- builtin errors
- possibility to add custom errors
- check for missing handlers when building the handler
- check handlers not matching a route in the configuration at server boot
- specific configuration format errors qt server boot
- statically typed handlers - avoids having to check every input and its type (which is used by context.Context for instance)
- using reflection to use structs as input and output arguments to match the configuration
- check for input and output arguments structs at server boot
- using reflection to use structs as input and output arguments to match the configuration
- check for unavailable types in configuration at server boot
- recover panics from handlers
- improve tests and coverage