You may use your openapi specification file to map the routes and handlers inside your project.
Please note, that the tarantool's http v2 project was discontinued due to its low performance. There is a specific branch now that still holds the v2 codebase and is available as http-v2-legacy package. So if you're going to update or reinstall your project's dependencies, be sure to run the following commands first:
tarantoolctl rocks remove http
tarantoolctl rocks install http-v2-legacy
The first one is not really necessary in case if you're going to remove your project's .rocks folder completely
- Quickstart
- Logging
- Routing
- Security Example
- Describing multipart/formdata request
- Route settings
- Check schema coverage
- Strict mode
- CORS handling
local openapi = require("gtn.openapi")
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization")
}
)
app:start()
As you can see, openapi call takes four positional arguments, which are: server module, router module, path to read specification from and also a table with options.
To log requests, errors and also show error messages on the client side, you could pass the debug options:
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization"),
debug = true
}
)
This option will enable the logging and write it to the sysout by default. To pass a logfile or logging level, just add a "log" table to the options:
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization"),
log = {
file = "log/app.log",
level = 6
},
debug = true
}
)
The openapi module uses opeartionId
and tags
options from operation object
to map controller function to the route. For example, you wrote a ./controllers/example.lua
module:
local _M = {}
function _M.userinfo(self)
-- some code here
return self:render({
json = {
data = user_data
}
})
end
return _M
Operation object from spec file should look something like this:
/example:
get:
tags:
- example
operationId: "userinfo"
summary: "Example request"
# ...
So, the module will map GET /example
request to be handled by userinfo
function in controllers.example
module.
All the modules must be stored within the controllers
directory, because the module exploits tarantool-http's ability
to map handlers with a controller#action
string. Place your controller modules inside the controllers folder, relative
to your app_dir
option.
The controller module may also return a function instead of a table, in that case just make sure your example.lua
module
returns the function and drop the operationId from operation object schema.
Note that the module maps only the first tag of the list, every other tag would be ignored.
The security
option contains either a table of methods to handle security schemas described
in your spec-file, or a function if you don't have multiple authorization protocols.
api.yaml
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
apiKeyAuth:
type: apiKey
in: header
name: X-API-KEY
basicAuth:
type: http
scheme: basic
cookieAuth:
type: apiKey
in: cookie
name: cookiename
authorization.lua file
local _M = {}
function _M.bearerAuth(url, scopes, token)
-- validate token
return result, err
end
function _M.apiKeyAuth(url, scopes, api_key)
-- validate API-key
return result, err
end
function _M.basicAuth(url, scopes, username, password)
-- validate user credentials
return result, err
end
function _M.cookieAuth(url, scopes, cookie)
-- validate cookie here
return result, err
end
return _M
The module correlate those automatically by the name of security scheme and method. and also sends corresponding arguments to the function. The basic authorization header is decoded from base64 and also splits by ":" symbol to form username and password.
The url
argument is the current request's path, taken from request_object.req.path
.
The scopes are sent from the security option in the operation object schema:
get:
tags:
- example
operationId: "userinfo"
summary: "Example request"
security:
- bearerAuth: ["test_scope", "example_scope", "etc_scope"] #here they are
As for token, api_key and username\password pair: those are, obviously, your authorization data, that needs to be checked in order to proceed with the request handling process.
Authorization functions must return two values. In case of an error, be sure to return nil, error_string
,
in case of successful authorization just return current user's data, that you may later access inside your
controller function. For example, inside of our controllers/example.lua
described above:
function userinfo(self)
local user_data = self.authorization
return self:render({
json = {
data = user_data
}
})
end
You may also override default error handling with a function:
local openapi = require("gtn.openapi")
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization")
}
)
-- the default error override
-- invalid repsponse format, or some crucial option that is not set for the endpoint will end up here
app:default(
function(ctx, err)
return ctx:render({
status = 204,
json = {
success = false,
message = err,
error = "No Content"
}
})
end
)
-- all unexpected errors during the call of the actual handler will end up here
app:error_handler(
function(ctx, err)
-- err argument here will be a table most of the time
return ctx:render({
json = {
success = false,
errors = err
}
})
end
)
-- the second return value of our `bearerAuth`, `apiKeyAuth` and `basicAuth` functions will be here
app:security_error_handler(
function(ctx, err)
return ctx:render({
status = 401,
json = {
success = false,
error = "Unauthorized",
message = err
}
})
end
)
-- override the default 404 handler if needed
-- 404.html file form the "/templates" folder will be rendered by default
-- the first parameter is either a function, or a boolean value
-- the second parameter is the matching pattern
app:not_found_handler(
function(ctx)
return ctx:render({
json = {
success = false,
error = "Not found"
}
})
end,
"/api/v1/*path"
)
-- to render default 404 for any unmatched path
app:not_found_handler(true)
-- all of the request parameters validation errors would end up here
-- by default the response in json = { error = err } with http-status of 400
app:bad_request_handler(
function(ctx, err)
ctx:render({
status = 400,
json = {
success = false,
error = err,
msg = "Bad request"
}
})
end
)
OpenAPI specification has two options to describe a file parameter for now:
# this one is for raw binary file data
/upload/binary:
post:
tags:
- main
operationId: "upload_binary"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
# this one is for base64-encoded binary
/upload/bytes:
post:
tags:
- main
operationId: "upload_bytes"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: bytes
Take note, that inside of an openapi validator, the bytes and binary formats are actually treated as an object and not a string value. So, inside of a controller function the file parameter will be a table containing:
- data — binary file content
- headers — file headers, containing filename and name which are: actual file name(~duh) and parameter name accordingly
- mime — a mime type of the file: image/jpeg, image/png etc.
There's a possibility to pass multiple schemas to the openapi object constructor function. It's primarily aimed to add an ability to maintain several versions of your API.
-- app.lua file
local openapi = require("gtn.openapi")
local schema_options = {
base_path = "schemas",
primary_schema = "base.yaml",
secondary_schemas = {
{
schema = "api_v2.yaml",
path = "/api/v2",
},
{
schema = "api_v3.yaml"
},
{
schema = "relative.yaml",
path = "/relative",
relative = true
}
}
}
local app = openapi(
require("http.server"),
require("http.router"),
schema_options,
{
security = require("authorization")
}
)
app:start()
Let's take a closer look to schema_options table fields:
- base_path — option indicates in which directory the schemas will be located, in this case the server will look for a schemas folder relatively to your app_dir option
- primary_schema — quite self-descriptive: the primary schema from which the server settings and all of the basic paths and components will be taken
- secondary_schemas — a list of additional schemas, in this case a v2 and v3 of our API.
- schema — in secondary_schemas is a name of a file relative to our base_path option, i.e ./schemas/v2.yaml and ./schemas/v3.yaml in this case.
- path — is a prefix to all the paths, described in current schema. If this option is not set, this file will simply extend the primary schema. Please note, that if there's a global path option set in primary schema, it'll also be applied to this one's paths.
- relative - is and option that idicates, that given schema paths would be relative to primary ones, i.e. the primary schema's base_path would be a prefix to every path withing this schema
You may also set new schema without changing the old-way options, just by calling a couple of new methods:
-- app.lua file
local app = openapi(
require("http.server"),
require("http.router"),
"schema.yaml",
{
security = require("authorization")
}
)
--[[
the first argument is the path to the schema file to read
the second one is actually a base_path option from before.
this calls will add new secondary schemas inside of openapi object
]]
app.openapi:add_schema("./schemas/api_v2.yaml", "/api/v2")
app.openapi:add_schema("./schemas/api_v3.yaml")
-- calling this method will automatically set new routes to server object
app:bind_paths()
There are additional tweaks for a single path object, that are set in a special option, called x-settings. For example, you have a global prefix for your API endpoints:
servers:
- url: http://localhost:{port}/{path}/
description: 'development'
variables:
port:
default: '8080'
path:
default: 'api/v1' # this one here
And you need just a couple of enpoints to ignore this options and be outside of "/api/v1' path. It's simple: just add an x-settings option with fullPath inside of those:
/outside_path:
get:
x-settings:
fullPath: true
responses:
200:
desciption: I am outside
/outside_too:
post:
x-settings:
fullPath: true
responses: ...
It primarily has some features for automatic schema testing, that are initiated by ./app.lua --test command. For, example, if you want to exclude some parts of api from being tested or you expect some other http-code in response:
/outside_path:
get:
x-settings:
fullPath: true
skipTest: true # this endpoint will skip testing
strict: true
/outside_too:
post:
x-settings:
fullPath: true
testStatus: 403 # Will look for 403 schema in responses part
responses:
# not this one
200:
desciption: Ok
# yup, this one
403:
description: Bad Request
content:
application/json:
schema:
type: object
required:
- success
properties:
success:
type: boolean
# will expect this value in assertion
example: true
error:
type: string
# this field is optional, but if exists, this value will be expected during auto testing
example: invalid parameters
The strict option determines if it should check the response value for equality with an example value or should it just check that the response types match with those set in the schema.
There's a command to check schema controller coverage, i.e. to determine which parts of the schema do not have actual controllers written. For example, you wrote a simple schema with 3 paths:
paths:
/this_one:
get:
tags:
- example
operationId: this_one
responses:
200:
description: ok
/that_one:
get:
tags:
- example
operationId: that_one
responses:
200:
description: ok
/another_one:
get:
tags:
- example
operationId: another_one
responses:
200:
description: ok
Also, you have example.lua file inside your project's controllers directory:
-- example.lua
local _M = {}
function _M.this_one(self)
return self:render({
json = {
success = true
}
})
end
return _M
And you want to find parts of your API not covered by handlers. Just run the command:
./app.lua --test coverage
FAILED TOTAL: 2
FAILED PATHS:
/that_one
/another_one
See, there are two endpoints described in openapi schema, but not actually handled. This feature is particularly handy if you have a massive several-thousand-lined schema.
To set the strict parameter validation mode set the strict parameter in options to true:
local app = openapi(
require("http.server"),
require("http.router"),
"schema.yaml",
{
security = require("authorization"),
strict = true
}
)
You may set cors handling by setting the cors option to your openapi instance initialization
local openapi = require("gtn.openapi")
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization"),
cors = {
-- default value 3600
max_age = 18400,
-- default value true
allow_credentials = false,
-- default {"Authorization", "Content-Type"}
allow_headers = {"Authorization", "Content-Type", "X-Requested-With"},
-- default {"*"}
allow_origin = {"http://example.com"}
}
}
)
app:route({path="/api/user", method="GET"}, some_handler)
app:start()
To set default CORS settings:
local openapi = require("gtn.openapi")
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization"),
cors = {} -- takes only a table value
}
)
app:start()
The default CORS options should suffice for the development process.
There's also a way to override CORS handler for specific endpoints:
local app = openapi(
require("http.server"),
require("http.router"),
"api.yaml",
{
security = require("authorization"),
cors = {
max_age = 18600,
allow_credentials = false,
allow_headers = {"Authorization", "Content-Type", "X-Requested-With"},
allow_origin = {"http://example.com"},
specific = {
{
-- this option obviously determines which path the current settings will be applied to
path = "/some_path",
-- this option disables the method check for current endpoint
-- i.e it won't intercept the control if requested method is not allowed
-- and also it won't set the self.hdrs table automatically
skip_method = false,
-- a function to override the cors handling
-- you may leave it empty if there's a need to skip CORS handling at all
handler = function(self)
self.hdrs["access-control-allow-origin"] = "example.com"
self.hdrs["access-control-allow-headers"] = "Authorization,Content-Type"
return self:render({
status = 201,
text = ""
})
end
}
}
}
}
)