Skip to content

Commit

Permalink
Add macro to simplify nesting route specs (#138)
Browse files Browse the repository at this point in the history
* Add `get_nested_endpoints_and_docs` macro
* Add nested example
  • Loading branch information
maximeborges authored Jan 14, 2024
1 parent fa77b69 commit 8421dbd
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"rocket-okapi-codegen",
"examples/json-web-api",
"examples/custom_schema",
"examples/nested",
"examples/uuid_usage",
"examples/special-types",
"examples/secure_request_guard",
Expand Down
4 changes: 4 additions & 0 deletions examples/nested/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
/.idea
12 changes: 12 additions & 0 deletions examples/nested/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "nested"
version = "0.1.0"
authors = ["Maxime Borges <[email protected]>", "Ralph Bisschops <[email protected]>"]
edition = "2021"

[dependencies]
rocket = { version = "=0.5.0", default-features = false, features = ["json"] }
rocket_okapi = { path = "../../rocket-okapi", features = ["rapidoc"] }
serde = "1.0"
serde_json = "1.0"
indexmap = "1.8.2"
42 changes: 42 additions & 0 deletions examples/nested/src/api/message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use rocket::form::FromForm;
use rocket::{get, post, serde::json::Json};
use rocket_okapi::okapi::openapi3::OpenApi;
use rocket_okapi::okapi::schemars::{self, JsonSchema};
use rocket_okapi::openapi;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use serde::{Deserialize, Serialize};

pub fn get_routes_and_docs(settings: &OpenApiSettings) -> (Vec<rocket::Route>, OpenApi) {
openapi_get_routes_spec![settings: create_message, get_message]
}

#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
struct Message {
/// The unique identifier for the message.
message_id: u64,
/// Content of the message.
content: String,
}

/// # Create a message
///
/// Returns the created message.
#[openapi(tag = "Message")]
#[post("/", data = "<message>")]
fn create_message(message: crate::DataResult<'_, Message>) -> crate::Result<Message> {
let message = message?.into_inner();
Ok(Json(message))
}

/// # Get a message by id
///
/// Returns the message with the requested id.
#[openapi(tag = "Message")]
#[get("/<id>")]
fn get_message(id: u64) -> crate::Result<Message> {
Ok(Json(Message {
message_id: id,
content: "Hey, how are you?".to_owned(),
}))
}
13 changes: 13 additions & 0 deletions examples/nested/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
mod message;
mod post;

use rocket_okapi::{
get_nested_endpoints_and_docs, okapi::openapi3::OpenApi, settings::OpenApiSettings,
};

pub fn get_routes_and_docs(settings: &OpenApiSettings) -> (Vec<rocket::Route>, OpenApi) {
get_nested_endpoints_and_docs! {
"/posts" => post::get_routes_and_docs(settings),
"/message" => message::get_routes_and_docs(settings),
}
}
45 changes: 45 additions & 0 deletions examples/nested/src/api/post.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use rocket::form::FromForm;
use rocket::{get, post, serde::json::Json};
use rocket_okapi::okapi::openapi3::OpenApi;
use rocket_okapi::okapi::schemars::{self, JsonSchema};
use rocket_okapi::openapi;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use serde::{Deserialize, Serialize};

pub fn get_routes_and_docs(settings: &OpenApiSettings) -> (Vec<rocket::Route>, OpenApi) {
openapi_get_routes_spec![settings: create_post, get_post]
}

#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
struct Post {
/// The unique identifier for the post.
post_id: u64,
/// The title of the post.
title: String,
/// A short summary of the post.
summary: Option<String>,
}

/// # Create post
///
/// Returns the created post.
#[openapi(tag = "Posts")]
#[post("/", data = "<post>")]
fn create_post(post: crate::DataResult<'_, Post>) -> crate::Result<Post> {
let post = post?.into_inner();
Ok(Json(post))
}

/// # Get a post by id
///
/// Returns the post with the requested id.
#[openapi(tag = "Posts")]
#[get("/<id>")]
fn get_post(id: u64) -> crate::Result<Post> {
Ok(Json(Post {
post_id: id,
title: "Your post".to_owned(),
summary: Some("Best summary ever.".to_owned()),
}))
}
117 changes: 117 additions & 0 deletions examples/nested/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,
},
}
}
}
128 changes: 128 additions & 0 deletions examples/nested/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use rocket::{Build, Rocket};
use rocket_okapi::okapi::openapi3::OpenApi;
use rocket_okapi::settings::UrlObject;
use rocket_okapi::{mount_endpoints_and_merged_docs, rapidoc::*};

mod api;
mod error;

pub type Result<T> = std::result::Result<rocket::serde::json::Json<T>, error::Error>;
pub type DataResult<'a, T> =
std::result::Result<rocket::serde::json::Json<T>, rocket::serde::json::Error<'a>>;

#[rocket::main]
async fn main() {
let launch_result = create_server().launch().await;
match launch_result {
Ok(_) => println!("Rocket shut down gracefully."),
Err(err) => println!("Rocket had an error: {}", err),
};
}

pub fn create_server() -> Rocket<Build> {
let mut building_rocket = rocket::build().mount(
"/rapidoc/",
make_rapidoc(&RapiDocConfig {
title: Some("My special 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()
}),
);

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,
"/external" => custom_route_spec,
"/api" => api::get_routes_and_docs(&openapi_settings),
};

building_rocket
}

fn custom_openapi_spec() -> OpenApi {
use indexmap::indexmap;
use rocket_okapi::okapi::openapi3::*;
use rocket_okapi::okapi::schemars::schema::*;
OpenApi {
openapi: OpenApi::default_version(),
info: Info {
title: "The best API ever".to_owned(),
description: Some("This is the best API ever, please use me!".to_owned()),
terms_of_service: Some(
"https://github.com/GREsau/okapi/blob/master/LICENSE".to_owned(),
),
contact: Some(Contact {
name: Some("okapi example".to_owned()),
url: Some("https://github.com/GREsau/okapi".to_owned()),
email: None,
..Default::default()
}),
license: Some(License {
name: "MIT".to_owned(),
url: Some("https://github.com/GREsau/okapi/blob/master/LICENSE".to_owned()),
..Default::default()
}),
version: env!("CARGO_PKG_VERSION").to_owned(),
..Default::default()
},
servers: vec![
Server {
url: "http://127.0.0.1:8000/".to_owned(),
description: Some("Localhost".to_owned()),
..Default::default()
},
Server {
url: "https://example.com/".to_owned(),
description: Some("Possible Remote".to_owned()),
..Default::default()
},
],
// Add paths that do not exist in Rocket (or add extra info to existing paths)
paths: {
indexmap! {
"/home".to_owned() => PathItem{
get: Some(
Operation {
tags: vec!["HomePage".to_owned()],
summary: Some("This is my homepage".to_owned()),
responses: Responses{
responses: indexmap!{
"200".to_owned() => RefOr::Object(
Response{
description: "Return the page, no error.".to_owned(),
content: indexmap!{
"text/html".to_owned() => MediaType{
schema: Some(SchemaObject{
instance_type: Some(SingleOrVec::Single(Box::new(
InstanceType::String
))),
..Default::default()
}),
..Default::default()
}
},
..Default::default()
}
)
},
..Default::default()
},
..Default::default()
}
),
..Default::default()
}
}
},
..Default::default()
}
}
Loading

0 comments on commit 8421dbd

Please sign in to comment.