diff --git a/README.md b/README.md index 34d0f95..38061a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Go doc](https://godoc.org/git.xdrm.io/go/aicra?status.svg)](https://godoc.org/git.xdrm.io/go/aicra) -**Aicra** is a self-working framework coded in *Go* that allows anyone to create a fully featured REST API. It features type checking, authentication management through middlewares, file upload, rich argument parsing (*i.e. url slash-separated, urlencoded, form-data, json*), nested routes, project compiler (*i.e. aicra*), etc. +**Aicra** is a self-working **REST API engine** coded in *Go* that allows anyone to create a fully featured API. It features type checking, authentication management through middlewares, file upload, rich argument parsing (*i.e. url slash-separated, urlencoded, form-data, json*), nested routes, project compiler (*i.e. aicra*), etc. All the REST API management is done for you, you just need to implement : diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..81514bb --- /dev/null +++ b/doc/README.md @@ -0,0 +1,608 @@ +```yaml +module: api +version: 1.2 +``` + +For developers +==== + +[**I.** Overview](#i-overview) + +- [**1** Introduction & features](#1-introduction--features) +- [**2** Basic knowledge](#2-basic-knowledge) + +[**II.** Usage](#ii-usage) + +- [**1** REST API](#1-rest-api) +- [**2** Internal use (PHP)](#2-internal-use) + +[**III.** Configuration](#iii-configuration) + +- [**1** Configuration format](#1---configuration-format) +- [**2** Method definition](#2---methoddefinition) + - [**2.1** Method description](#21---methoddescription) + - [**2.2** Method permissions](#22---methodpermissions) + - [**2.3** Method parameters](#23---methodparameters) + - [parameter name](#parametername) + - [parameter description](#parameterdescription) + - [parameter type](#parameterchecker_type) + - [parameter optional vs. required](#parameteris_optional) + - [parameter rename](#parameterrename) + - [parameter default value](#parameterdefault_value) + - [**2.4** Method output format](#24---methodoutput_format) + - [**2.5** Method options](#25---methodoptions) + +[**IV.** Implementation](#iv-implementation) + +- [**1** Permissions : AuthSystem](#1---permissions--authsystem) +- [**2** Core implementation](#2---core-implementation) + - [Classes](#classes) + - [Methods](#methods) + - [Method arguments](#method-arguments) + - [Return statement](#return-statement) + - [Before and After scripts](#before-and-after-scripts) + - [Example](#example) + +[**V.** Type Checker](#v-type-checker) + +- [**1** Default Types](#1---default-types) +- [**2** Complex Types](#2---complex-types) + +[**VI.** Documentation](#vi-documentation) + +- [**1** API accessible documentation](#1---api-accessible-documentation) + + +For clients +==== + +[**I.** Simple request](#i-simple-request) + +- [**1** URL](#1---URL) +- [**2** variables](#2---variables) + +[**II.** Usage](#ii-usage) + + + + +# **I.** Overview +## **1** Introduction & features + +The `api` package (v1.2) allows you to easily create and manage a REST API for your applications. + +The aim of this package is to make your life easier working with APIs and internal delegation. The only things you have to do is to implement your controllers, middle-wares and write 2 configuration files, the package will do the rest. + +Things you **have** to do : +- write the project configuration file (cf. [project configuration](#iii-project-configuration)) +- write the API definition file (cf. [api definition](#iv-api-definition)) +- implement your middle-wares to manage authentication, csrf, etc (cf. [AuthSystem](#1---middle-wares)) +- implement your controllers (cf. ???) +- implement additional project-specific type checkers (cf. ???) + +Things you **don't have** to do : +- check the input variables (cf. [Checker](#v-type-checker)) +- multiple permission management +- optional or required input +- Form data type : x-www-urlencoded, multipart, or file upload +- URL variables (slash separated at the end of the route, like standard uri) +- and a lot more ... + +## **2** Basic knowledge + +The API uses the routes defined in the _api.json_ configuration to export your code (in compiled go or other language). Middle-wares are executed when receiving a new request, then the controllers will be loaded according to the URI. + +Example: +* the module `article` contains methods: + * `GET` to get article data + * `POST` to post a new article + * `PUT` to edit an existing article + * `DELETE` to delete an exisint article + +*Note that each method must be a valid HTTP METHOD*. + + + + +# **III.** Project Configuration + + +The documentation consists of a _chain_ of urls each one can contain several HTTP method specifications. +Note that each url can be chained apart the method specifications. + + + + +## **1** - configuration format + +The configuration is a set of `uri` paths that can contain up to 4 method types: **POST**, **DELETE**, **PUT**, **GET** or `uri` subpaths. + +For instance the 4 methods directly inside `uri1` will be triggered when the calling URI is `/uri1`, the ones directly inside `uri2` will be triggered when calling `/uri1/uri2` and so on.. + +You can also set full paths if you don't need transitional methods, for instance the path `uri5/uri6/uri7` will be triggered by the url `/uri5/uri6/uri7`. + +**Example:** The example file loaded with the default configuration can be found [here](./../../src/config/api/3.0/modules.json). +```json +{ + "uri1" : { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition, + + "uri2": { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition, + + "uri3": {} + } + }, + + "uri5/uri6/uri7": { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition + } +} +``` + +**Note**: It is possible to trigger the *root uri* (`/`), so you can set methods directly at the root of the JSON file. + +## **2** - method.definition +```json +{ + "des": method.description, + "per": method.permissions, + "par": method.parameters, + "out": method.output_format, + "opt": method.options +} +``` + +## **2.1** - method.description + +The *description* field must be a **string** containing the human-readable description of what the method does. + +## **2.2** - method.permissions + +The *permissions* field must be an array. You can manage **OR** and **AND** permission combinations. + +- **OR** is applied between each **0-depth** array +- **AND** is applied between each **1-depth** array + +For instance the following permission `[ ["a","b"], ["c"] ]` means you need the permissions **a** and **b** combined or only the permission **c**. + +## **2.3** - method.parameters + +The *parameters* field must be an object containing each required or optional parameter needed for the implementation. + +```json +"parameter.name": { + "des": parameter.description, + "typ": parameter.checker_type, + "opt": parameter.is_optional, + "ren": parameter.rename, + "def": parameter.default_value, +} +``` + +#### parameter.name + +The *name* field must be a **string** containing variable name that will be asked for the caller. + +Note that you can set any string for **body parameters**, but **GET parameters** must be named `URL#`, where `#` is the index within the URI, beginning with 0. +#### parameter.description + +The *description* field must be a **string** containing the human-readable description of what the parameter is or must be. +#### parameter.checker_type + +The *checker_type* field must be a **string** corresponding to a `\api\core\Checker` type that will be checked before calling the implementation. + +#### parameter.is_optional + +The *is_optional* field must be a **boolean** set to `true` if the parameter can be ommited. By default, the field is set to `false` so each parameter is required. + +#### parameter.rename + +The *rename* field must be a **string** corresponding to the *variable name* given to the implementation. It is useful for **GET parameters** because they need to be called `URL#`, where `#` is the position in the URI. (cf. [paramter.name](#parametername))) + +If ommited, by default, `parameter.name` will be used. +#### parameter.default_value + +The *default_value* field must be of any type according to the *checker_type* field, it will be used only for **optional** parameters when ommited by the caller + +By default, each optional parameter will exist and will be set to `null` to the implementation. + +## **2.4** - method.output_format + +The *output_format* field must have the same format as `method.parameters` but will only be used to generate a documentation or to tell other developers what the method returns if no error occurs. + +## **2.5** - method.options + +The *options* field must be an **object** containing the available options. + +The only option available for now is: + +```json +"download": true +``` + +Your implementation must return 2 fields: +- `body` a string containing the file body +- `headers` an array containing as an associative array file headers + +If the API is called with HTTP directly, it will not print the **json** response (only on error), but will instead directly return the created file. + +*AJAX:* If called with ajax, you must give the header `HTTP_X_REQUESTED_WITH` set to `XMLHttpRequest`. In that case only, it will return a normal JSON response with the field `link` containing the link to call for downloading the created file. **You must take care of deleting not used files** - there is no such mechanism. + + + +# **III.** API Definition + +The documentation consists of a list of URIs each one can contain several HTTP method specifications. + +## **1** - configuration format + +The configuration is a set of `uri` paths that can contain up to 4 method types: **POST**, **DELETE**, **PUT**, **GET** or the `/` field to define sub URIs (recursive). + +For instance the 4 methods directly inside `/`.`uri1` will be triggered when the calling URI is `/uri1`, the ones directly inside `uri2` will be triggered when calling `/uri1/uri2` and so on.. + +You can also set full paths if you don't need transitional methods, for instance the path `/`.`uri5`.`/`.`uri6`.`/`.`uri7` will be triggered by the url `/uri5/uri6/uri7`. + +**Example:** The example file loaded with the default configuration can be found [here](./../../src/config/api/3.0/modules.json). + +```json +{ + + "/": { + "uri1" : { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition, + + "/": { + "uri2": { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition + } + } + }, + "uri5": + "/": { + "uri6": { + "/": { + "uri7": { + "GET": method.definition, + "POST": method.definition, + "PUT": method.definition, + "DELETE": method.definition + } + } + } + } + } + } +} +``` + +**Note**: It is possible to trigger the *root uri* (`/`), so you can set methods directly at the root of the JSON file. + +## **2** - method.definition + +```json +{ + "info": method.description, + "scope": method.permissions, + "in": method.parameters, + "out": method.output_format +} +``` + +## **2.1** - method.description + +The *description* field must be a **string** containing the human-readable description of what the method does. + +## **2.2** - method.permissions + +The *permissions* field must be an array. You can manage **OR** and **AND** permission combinations. + +- **OR** is applied between each **0-depth** array +- **AND** is applied between each **1-depth** array + +For instance the following permission `[ [a,b], [c] ]` means you need the permissions **a** and **b** combined or only the permission **c**. + +## **2.3** - method.parameters + +The *parameters* field must be an object containing each required or optional parameter needed for the implementation. + +```json +"parameter.name": { + "info": parameter.description, + "type": parameter.checker_type, + "name": parameter.rename, + "default": parameter.default_value, +} +``` + +#### parameter.name + +The *name* field must be a **string** containing variable name that will be asked for the caller. + +Note that you can set any string for **body parameters**, but you can also catch : + +- **URI** parameters by prefixing your variable name with `URL#` and followed by a number which is the index in the URL (starts with 0). For instance the **first** URI parameter received from `/path/to/controller/some_value` has to be named `URL#0` inside this configuration. It is a good practice to rename these parameters for better access in your code. + +- **GET** parameters by prefixing your variable name with `GET@`. For instance the get parameter **somevar** received from `/host/uri?somevar=some_value` has to be named `GET@somevar` inside this configuration. + +#### parameter.description + +The *description* field must be a **string** containing the human-readable description of what the parameter is or must be. + +#### parameter.checker_type + +The *checker_type* field must be a **string** corresponding to a `\api\core\Checker` type that will be checked before calling the implementation. + +#### parameter.is_optional + +If the parameter is optional and can be ignored by clients, you must prefix _parameter.checker_type_ with a question mark. For instance a number variable will have a type of `number`, if the variable is optional, the type will then be `?number`. + +**Note :** it is recommended to add a default value for optional parameters. + +#### parameter.rename + +The *name* field must be a **string** corresponding to the wanted *variable name* that will be passed to the controller. It is mainly useful for **URI parameters** because their name is not explicit at all. + +If omitted, by default, `parameter.name` will be used. + +#### parameter.default_value + +The *default_value* field must be of compliant to the variable type checker, it will be used only for **optional** parameters when omitted by the client. + +By default, each optional parameter will exist and will be set to `null` to the implementation. + +## **2.4** - method.output_format + +The *output_format* field must have the same format as `method.parameters` but will only be used to generate a documentation or to tell other developers what the method returns if no error occurs. It allows better overview of an API without looking at the code. + + + + + +# **IV.** Implementation + +## **1** - middle-wares + +In order to implement your Permission System you have to implement the **interface** `AuthSystem` located in `/build/api/core/AuthSystem`. + +You must register your custom authentification system before each api call. + +For instance, lets suppose your implementation is called `myAuthSystem`. +```php +\api\core\Request::setAuthSystem(new myAuthSystem); +``` +**Note**: By default the API will try to find `\api\core\AuthSystemDefault`. + + +## **2** - core implementation + +### classes +Each module's implementation is represented as a **file** so as a **class** located in `/build/api/module/`. In order for the autoloader to work, you must name the **file** the same name as the **class**. +Also the **namespace** must match, it corresponds to the path (starting at `/api`). + +For instance if you have in your configuration a path called `/uri1/uri2/uri3`, you will create the file `/build/api/module/uri1/uri2/uri3.php`. + +*Specific*: For the root (`/`) path, it will trigger the class `\api\module\root`, so you have to create the file `/build/api/module/root.php`. (cf. [configuration format](#1---configuration-format)) + + + +### methods +Each middle-wares must implement the [driver.Middleware](https://godoc.org/git.xdrm.io/go/aicra/driver#Middleware) interface. + +The `Inspect(http.Request, *[]string)` method gives you the actual http request. And you must edit the string list to add scope elements. After all middle-wares are executed, the final scope is used to check the **method.permission** field to decide whether the controller can be accessed. + +### example + +For instance here, we check if a token is sent inside the **Authorization** HTTP header. The token if valid defines scope permissions. + +```go +package main + +import ( + "git.xdrm.io/aicra/driver" + "net/http" +) + +// for the API code to export our middle-ware +func Export() driver.Middleware { return new(TokenManager) } + +// mockup type to implement the driver.Middleware interface +type TokenManager interface{} + +func (tm TokenManager) Inspect(req http.Request, scope *[]string) { + token := req.Header.Get("Authorization") + if !isTokenValid(token) { + return + } + + scope = append(scope, getTokenPermissions(token)...) +} +``` + +*Note*: Functions `isTokenValid()` and `getTokenPermissions()` do not exist, it was in order for all to understand the example. + + +# **V.** Type Checker + +Each type checker checks the parameter values according to the type given in the api definition. + +The default types below are available in the default package. +To add a new type, just open the file `/build/api/Checker.php` and add an entry in the `switch` statement. + +## **1** - Default types + +**Warning :** All received values are extracted using JSON format if possible, else values are considered raw strings. + +- `2`, `-3.2` are extracted as floats +- `true` and `false` are stored as booleans +- `null` is stored as null +- `[x, y, z]` is stored as an array containing **x**, **y**, and **z** that can be themselves JSON-decoded if possible. +- `{ "a": x }` is stored as a map containing **x** at the key "a", **x** can be itself JSON-decoded if possible. +- `"some string"` is stored as a string +- `some string` here, the decoding will fail (it is no valid JSON) so it will be stored as a string + +|Type|Example|Description| +|---|---|---| +|`any`|`[9,"a"]`, `"a"`|Any data (can be simple or complex)| +|`id`|`10`, `"23"`|Positive integer number| +|`int`|`-10`, `"23"`|Any integer number| +|`float`|`-10.2`, `"23.5"`|Any float| +|`text`|`"Hello!"`|String that can be of any length (even empty)| +|`digest(L)`|`"4612473aa81f93a878..."`|String with a length of `L`, containing only hexadecimal lowercase characters.| +|`mail`|`"a.b@c.def"`|Valid email address| +|`array`|`[]`, `[1, 3]`|Any array| +|`bool`|`true`, `false`|Boolean| +|`varchar(a,b)`|`"Hello!"`|String with a length between `a` and `b` (included)| + +## **2** - Complex types + +|Type|Sub-Type|Description| +|---|---|---| +|`array`|`a`|Array containing only entries matching the type `a`| +|`FILE`|_a raw file send in `multipart/form-data`|A raw file sent by `multipart/form-data`| + +> **Note:** It is possible to chain `array` type as many as needed. + +**Ex.:** `array>` - Will only match an array containing arrays that only contains `id` entries. + + + +# **VI.** Documentation + +With the *all-in-config* method, we can generate a consistent documentation or other documents from the `/config/modules.json` file. + +## **1** - API accessible documentation + +You can request the API for information about the current URI by using the `OPTIONS` HTTP method. + + +==== + + +# **I.** Simple request +## **1** - URL + +### format +The `uri` format is as defined: `{base}/{path}/{GET_parameters}`, where +- `{base}` is the server's *API* base uri (ex: `https://example.com/api/v1` or `https://api.exampl.com`) +- `{path}` is the effective path you want to access (ex: `article/author`) +- `{GET_parameters}` is a set of slash-separated values (ex: `val0/val1/val2//val4`) + +*Note:* GET parameters are not used as usual (`?var1=val1&var2=val2...`), instead the position in the URL gives them an implicit name which is `URL#`, where `#` is the index in the uri (beginning with 0). + +### example 1 + +If you want to edit an article with the server's REST API, it could be defined as following: + +```yaml +http: PUT +path: article/{id_article} +input: + body: new content of the article +output: + updated: the updated article data +``` + +Let's take the example where you want to update the article which id is **23** and set its body to "**blabla new content**" + +`HTTP REQUEST` +``` +PUT article/23 HTTP/1.0 + +body=blabla+new+content +``` + + +`HTTP RESPONSE` +``` +HTTP/1.0 200 OK +Content-Type: application/json + +{ + "error": 0, + "ErrorDescription": "all right", + + "updated": { + "id_article": 23, + "title": "article 23", + "body": "blabla new content" + } + +} +``` + +### example 2 + +If you want to get a specific article line, the request could be defined as following + +```yaml +http: GET +path: article/line/{id_article}/{no_line} +input: - +output: + articles: the list of matching lines +``` + +Let's take the example where you want to get **all articles** because `id_article` is set to optional, but you only want the first line of each so you have to give only the second parameter set to `1` (first line). + +*Solution:* The position in the `uri` where `id_article` must be, have to be left empty: it will result of 2 slashes (`//`). + +`HTTP REQUEST` +``` +GET article/line//1 HTTP/1.0 +``` + + +`HTTP RESPONSE` +``` +HTTP/1.0 200 OK +Content-Type: application/json + +{ + "error": 0, + "ErrorDescription": "all right", + + "articles": [ + { + "id_article": 23, + "line": [ 0: "blabla new content" + },{ + "id_article": 25, + "line": [ 0: "some article" + } + ] + +} +``` \ No newline at end of file