Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add macro to simplify nesting route specs #138

Merged
merged 4 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading