Host OpenAPI spec through actix-web
With actix4
feature enabled, paperclip exports a plugin for actix-web framework to host OpenAPI v2 spec for your APIs automatically. While it's not feature complete, you can rely on it to not break your actix-web flow.
Let's start with a simple actix-web application. It has actix-web
and serde
for JSON'ifying your APIs. Let's also add paperclip
with actix4
feature.
# [package] ignored for brevity
[dependencies]
# actix-web 2.0 is supported through "actix2" and "actix2-nightly" features
# actix-web 3.0 is supported through "actix3" and "actix3-nightly" features
actix-web = "4.0"
# The "actix4-nightly" feature can be specified if you're using nightly compiler. Even though
# this plugin works smoothly with the nightly compiler, it also works in stable
# channel (replace "actix4-nightly" feature with "actix4" in that case). There maybe compilation errors,
# but those can be fixed.
# Add the "v3" option if you want to expose an OpenAPI v3 document
paperclip = { version = "0.8", features = ["actix4"] }
serde = { version = "1.0", features = ["derive"] }
Our main.rs
looks like this:
use actix_web::{App, HttpServer, Error, web::{self, Json}};
use serde::{Serialize, Deserialize};
// Mark containers (body, query, parameter, etc.) like so...
#[derive(Serialize, Deserialize)]
struct Pet {
name: String,
id: Option<i64>,
}
async fn echo_pet(body: Json<Pet>) -> Result<Json<Pet>, Error> {
Ok(body)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new()
.service(
web::resource("/pets")
.route(web::post().to(echo_pet))
)
).bind("127.0.0.1:8080")?
.run().await
}
Now, let's modify it to use the plugin!
use actix_web::{App, HttpServer, Error};
use paperclip::actix::{
// extension trait for actix_web::App and proc-macro attributes
OpenApiExt, Apiv2Schema, api_v2_operation,
// If you prefer the macro syntax for defining routes, import the paperclip macros
// get, post, put, delete
// use this instead of actix_web::web
web::{self, Json},
};
use serde::{Serialize, Deserialize};
// Mark containers (body, query, parameter, etc.) like so...
#[derive(Serialize, Deserialize, Apiv2Schema)]
struct Pet {
// You can optionaly add description for an individual property with the following line
/// Name of the pet
name: String,
id: Option<i64>,
}
// Mark operations like so...
#[api_v2_operation]
// Add the next line if you want to use the macro syntax
// #[post("/pets")]
async fn echo_pet(body: Json<Pet>) -> Result<Json<Pet>, Error> {
Ok(body)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new()
// Record services and routes from this line.
.wrap_api()
// Add routes like you normally do...
.service(
web::resource("/pets")
.route(web::post().to(echo_pet))
)
// Or just .service(echo_pet) if you're using the macro syntax
// Mount the v2/Swagger JSON spec at this path.
.with_json_spec_at("/api/spec/v2")
// If you added the "v3" feature, you can also include
// .with_json_spec_v3_at("/api/spec/v3")
// ... or if you wish to build the spec by yourself...
// .with_raw_json_spec(|app, spec| {
// app.route("/api/spec", web::get().to(move || {
// let spec = spec.clone();
// async move {
// paperclip::actix::HttpResponseWrapper(actix_web::HttpResponse::Ok().json(&spec))
// }
// }))
// })
// IMPORTANT: Build the app!
.build()
).bind("127.0.0.1:8080")?
.run().await
}
We have:
- Imported
OpenApiExt
extension trait foractix_web::App
along withApiv2Schema
derive macro andapi_v2_operation
proc macro attributes. - Switched from
actix_web::web
topaperclip::actix::web
. - Marked our
Pet
struct andadd_pet
function as OpenAPI-compatible schema and operation using proc macro attributes. - Transformed our
actix_web::App
to a wrapper using.wrap_api()
. - Optionally switched to Paperclip's macros instead of the actix-web route macros
- Mounted the JSON spec at a relative path using
.with_json_spec_at("/api/spec/v2")
(and optionally a v3 spec as well). - Built (using
.build()
) and passed the originalApp
back to actix-web.
Note that we never touched the service, resources, routes or anything else! This means that our original actix-web flow is unchanged.
Now you can check the API with the following cURL command:
curl -X POST http://localhost:8080/pets -H "Content-Type: application/json" -d '{"id":1,"name":"Felix"}'
And see the specs with this:
curl http://localhost:8080/api/spec/v2
... we get the swagger spec as a JSON!
{
"swagger": "2.0",
"definitions": {
"Pet": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"required": ["name"]
}
},
"paths": {
"/pets": {
"post": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Pet"
}
}
},
"parameters": [{
"in": "body",
"name": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
}
}]
}
}
},
"info": {
"version": "",
"title": ""
}
}
Similarly, if we were to use other extractors like web::Query<T>
, web::Form<T>
or web::Path
, the plugin will emit the corresponding specification as expected.
Description for an individual property uses Rust doc comments technology.
Known limitations
- Enums: OpenAPI (v2) itself supports using simple enums (i.e., with unit variants), but Rust and serde has support for variants with fields and tuples. I still haven't looked deep enough either to say whether this can/cannot be done in OpenAPI or find an elegant way to represent this in OpenAPI.
- Functions returning abstractions: The plugin has no way to obtain any useful information from functions returning abstractions such as
HttpResponse
,impl Responder
or containers such asResult<T, E>
containing those abstractions. So currently, the plugin silently ignores these types, which results in an empty value in your hosted specification.
Missing features
At the time of this writing, this plugin didn't support a few OpenAPI v2 features:
Affected entity | Missing feature(s) |
---|---|
Parameter | Non-body parameters allowing validations like allowEmptyValue , collectionFormat , items , etc. |
Performance implications?
Even though we use some wrappers and generate schema structs for building the spec, we do this only once i.e., until the .build()
function call. At runtime, it's basically a pointer read, which is quite fast!
We also add wrappers to blocks in functions tagged with #[api_v2_operation]
, but those wrappers follow the Newtype pattern and the code eventually gets optimized away anyway.