Skip to content

Commit

Permalink
feat: support to Rocket-Okapi (#1071)
Browse files Browse the repository at this point in the history
* feat: support to okapi

* fix: fmt checks

* chore: rocket-okapi-example: add required schemas

* chore: rocket-okapi-example: add dto

* chore: rocket-okapi-example: add custom error

* chore: rocket-okapi-example: add api controllers

* chore: rocket-okapi-example: add notes in Readme

* chore: make rocket_okapi optional

* refactor: delete rocket example from rocket_example

* chore: rocket-okapi-example: add base files for okapi example

* chore: rocket-okapi-example: add controllers and dto

* chore: rocket-okapi-example: add docs
  • Loading branch information
eum602 authored Oct 16, 2022
1 parent 0ca62ba commit 2cdc065
Show file tree
Hide file tree
Showing 30 changed files with 1,014 additions and 0 deletions.
12 changes: 12 additions & 0 deletions examples/rocket_okapi_example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "sea-orm-rocket-okapi-example"
version = "0.1.0"
authors = ["Sam Samai <sam@studio2pi.com.au>", "Erick Pacheco <eum602@gmail.com"]
edition = "2021"
publish = false

[workspace]
members = [".", "api", "core", "entity", "migration", "dto"]

[dependencies]
rocket-example-api = { path = "api" }
12 changes: 12 additions & 0 deletions examples/rocket_okapi_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Rocket and Rocket-API with SeaORM example app

1. Modify the `url` var in `api/Rocket.toml` to point to your chosen database

1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-postgres",` line)

1. Execute `cargo run` to start the server

1. You can go to ```http://localhost:8000/swagger-ui/index.html``` to see the api documentation about this demo project.
![swagger](swagger.png)
1. Additionally, you can navigate to ```http://localhost:8000/rapidoc/index.html``` to see the rapidoc format for api documentation
![rapidoc](rapidoc.png)
11 changes: 11 additions & 0 deletions examples/rocket_okapi_example/Rocket.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[default]
template_dir = "api/templates/"

[default.databases.sea_orm]
# Mysql
# make sure to enable "sqlx-mysql" feature in Cargo.toml, i.e default = ["sqlx-mysql"]
# url = "mysql://root:@localhost/rocket_example"

# Postgres
# make sure to enable "sqlx-postgres" feature in Cargo.toml, i.e default = ["sqlx-postgres"]
url = "postgres://user:pass@localhost:5432/rocket"
39 changes: 39 additions & 0 deletions examples/rocket_okapi_example/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "rocket-example-api"
version = "0.1.0"
authors = ["Sam Samai <sam@studio2pi.com.au>"]
edition = "2021"
publish = false

[dependencies]
async-stream = { version = "^0.3" }
async-trait = { version = "0.1" }
rocket-example-core = { path = "../core" }
futures = { version = "^0.3" }
futures-util = { version = "^0.3" }
rocket = { version = "0.5.0-rc.1", features = [
"json",
] }
rocket_dyn_templates = { version = "0.1.0-rc.1", features = [
"tera",
] }
serde_json = { version = "^1" }
entity = { path = "../entity" }
migration = { path = "../migration" }
tokio = "1.20.0"
serde = "1.0"
dto = { path = "../dto" }

[dependencies.sea-orm-rocket]
path = "../../../sea-orm-rocket/lib" # remove this line in your own project and use the git line
features = ["rocket_okapi"] #enables rocket_okapi so to have open api features enabled
# git = "https://github.com/SeaQL/sea-orm"

[dependencies.rocket_okapi]
version = "0.8.0-rc.2"
features = ["swagger", "rapidoc","rocket_db_pools"]

[dependencies.rocket_cors]
git = "https://github.com/lawliet89/rocket_cors.git"
rev = "54fae070"
default-features = false
117 changes: 117 additions & 0 deletions examples/rocket_okapi_example/api/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use rocket::{
http::{ContentType, Status},
request::Request,
response::{self, Responder, Response},
};
use rocket_okapi::okapi::openapi3::Responses;
use rocket_okapi::okapi::schemars::{self, Map};
use rocket_okapi::{gen::OpenApiGenerator, response::OpenApiResponderInner, OpenApiError};

/// Error messages returned to user
#[derive(Debug, serde::Serialize, schemars::JsonSchema)]
pub struct Error {
/// The title of the error message
pub err: String,
/// The description of the error
pub msg: Option<String>,
// HTTP Status Code returned
#[serde(skip)]
pub http_status_code: u16,
}

impl OpenApiResponderInner for Error {
fn responses(_generator: &mut OpenApiGenerator) -> Result<Responses, OpenApiError> {
use rocket_okapi::okapi::openapi3::{RefOr, Response as OpenApiReponse};

let mut responses = Map::new();
responses.insert(
"400".to_string(),
RefOr::Object(OpenApiReponse {
description: "\
# [400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\
The request given is wrongly formatted or data asked could not be fulfilled. \
"
.to_string(),
..Default::default()
}),
);
responses.insert(
"404".to_string(),
RefOr::Object(OpenApiReponse {
description: "\
# [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404)\n\
This response is given when you request a page that does not exists.\
"
.to_string(),
..Default::default()
}),
);
responses.insert(
"422".to_string(),
RefOr::Object(OpenApiReponse {
description: "\
# [422 Unprocessable Entity](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422)\n\
This response is given when you request body is not correctly formatted. \
".to_string(),
..Default::default()
}),
);
responses.insert(
"500".to_string(),
RefOr::Object(OpenApiReponse {
description: "\
# [500 Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\
This response is given when something wend wrong on the server. \
".to_string(),
..Default::default()
}),
);
Ok(Responses {
responses,
..Default::default()
})
}
}

impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"Error `{}`: {}",
self.err,
self.msg.as_deref().unwrap_or("<no message>")
)
}
}

impl std::error::Error for Error {}

impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
// Convert object to json
let body = serde_json::to_string(&self).unwrap();
Response::build()
.sized_body(body.len(), std::io::Cursor::new(body))
.header(ContentType::JSON)
.status(Status::new(self.http_status_code))
.ok()
}
}

impl From<rocket::serde::json::Error<'_>> for Error {
fn from(err: rocket::serde::json::Error) -> Self {
use rocket::serde::json::Error::*;
match err {
Io(io_error) => Error {
err: "IO Error".to_owned(),
msg: Some(io_error.to_string()),
http_status_code: 422,
},
Parse(_raw_data, parse_error) => Error {
err: "Parse Error".to_owned(),
msg: Some(parse_error.to_string()),
http_status_code: 422,
},
}
}
}
139 changes: 139 additions & 0 deletions examples/rocket_okapi_example/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#[macro_use]
extern crate rocket;

use rocket::fairing::{self, AdHoc};
use rocket::{Build, Rocket};

use migration::MigratorTrait;
use sea_orm_rocket::Database;

use rocket_okapi::mount_endpoints_and_merged_docs;
use rocket_okapi::okapi::openapi3::OpenApi;
use rocket_okapi::rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig};
use rocket_okapi::settings::UrlObject;
use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig};

use rocket::http::Method;
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};

mod pool;
use pool::Db;
mod error;
mod okapi_example;

pub use entity::post;
pub use entity::post::Entity as Post;

async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
let conn = &Db::fetch(&rocket).unwrap().conn;
let _ = migration::Migrator::up(conn, None).await;
Ok(rocket)
}

#[tokio::main]
async fn start() -> Result<(), rocket::Error> {
let mut building_rocket = rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
.mount(
"/swagger-ui/",
make_swagger_ui(&SwaggerUIConfig {
url: "../v1/openapi.json".to_owned(),
..Default::default()
}),
)
.mount(
"/rapidoc/",
make_rapidoc(&RapiDocConfig {
title: Some("Rocket/SeaOrm - RapiDoc documentation | RapiDoc".to_owned()),
general: GeneralConfig {
spec_urls: vec![UrlObject::new("General", "../v1/openapi.json")],
..Default::default()
},
hide_show: HideShowConfig {
allow_spec_url_load: false,
allow_spec_file_load: false,
..Default::default()
},
..Default::default()
}),
)
.attach(cors());

let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
let custom_route_spec = (vec![], custom_openapi_spec());
mount_endpoints_and_merged_docs! {
building_rocket, "/v1".to_owned(), openapi_settings,
"/additional" => custom_route_spec,
"/okapi-example" => okapi_example::get_routes_and_docs(&openapi_settings),
};

building_rocket.launch().await.map(|_| ())
}

fn cors() -> Cors {
let allowed_origins =
AllowedOrigins::some_exact(&["http://localhost:8000", "http://127.0.0.1:8000"]);

let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec![Method::Get, Method::Post, Method::Delete]
.into_iter()
.map(From::from)
.collect(),
allowed_headers: AllowedHeaders::all(),
allow_credentials: true,
..Default::default()
}
.to_cors()
.unwrap();
cors
}

fn custom_openapi_spec() -> OpenApi {
use rocket_okapi::okapi::openapi3::*;
OpenApi {
openapi: OpenApi::default_version(),
info: Info {
title: "SeaOrm-Rocket-Okapi Example".to_owned(),
description: Some("API Docs for Rocket/SeaOrm example".to_owned()),
terms_of_service: Some("https://github.com/SeaQL/sea-orm#license".to_owned()),
contact: Some(Contact {
name: Some("SeaOrm".to_owned()),
url: Some("https://github.com/SeaQL/sea-orm".to_owned()),
email: None,
..Default::default()
}),
license: Some(License {
name: "MIT".to_owned(),
url: Some("https://github.com/SeaQL/sea-orm/blob/master/LICENSE-MIT".to_owned()),
..Default::default()
}),
version: env!("CARGO_PKG_VERSION").to_owned(),
..Default::default()
},
servers: vec![
Server {
url: "http://127.0.0.1:8000/v1".to_owned(),
description: Some("Localhost".to_owned()),
..Default::default()
},
Server {
url: "https://production-server.com/".to_owned(),
description: Some("Remote development server".to_owned()),
..Default::default()
},
],
..Default::default()
}
}

pub fn main() {
let result = start();

println!("Rocket: deorbit.");

if let Some(err) = result.err() {
println!("Error: {}", err);
}
}
Loading

0 comments on commit 2cdc065

Please sign in to comment.