aicra/README.md

11 KiB
Raw Permalink Blame History

| aicra |

Go version License: MIT Go Report Card Go doc Build Status


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

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 method
  • in: 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 the in 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)
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 the in section
  • a variable var in the Query has to be named GET@var in the in 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" }
        }
    }
]
  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). 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.

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 value
    • int - see go types
    • uint - see go types
    • float - see go types
    • string - any text
    • string(len) - any string with a length of exactly len characters
    • 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)
  • 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
  • check for unavailable types in configuration at server boot
  • recover panics from handlers
  • improve tests and coverage