mirror of
https://github.com/stoatchat/rust-okapi.git
synced 2026-06-30 21:57:54 -04:00
Retrieve and merge OpenApi objects
- Allow customization of OpenApi object. - Allow merging of OpenApi objects. - Retrieve OpenApi object after generating. (Closes: #28) - Create `mount_endpoints_and_merged_docs` marco in order to streamline code structure for bigger projects. (Closes: #30) - Added new example for structuring bigger projects. - Allowed changing path where OpenApi file is hosted. - Added `log v0.4` as a dependency.
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
/.idea
|
||||
.vscode
|
||||
+2
-1
@@ -3,5 +3,6 @@ members = [
|
||||
"okapi",
|
||||
"rocket-okapi",
|
||||
"rocket-okapi-codegen",
|
||||
"examples/json-web-api"
|
||||
"examples/json-web-api",
|
||||
"examples/custom_schema",
|
||||
]
|
||||
|
||||
@@ -9,11 +9,12 @@ rocket-okapi: [](https:
|
||||
|
||||
Automated OpenAPI (AKA Swagger) document generation for Rust/Rocket projects.
|
||||
|
||||
Never have outdated documentation again. Okapi will generate documentation for you at compile time.
|
||||
Never have outdated documentation again.
|
||||
Okapi will generate documentation for you while setting up the server.
|
||||
It uses a combination of [Rust Doc comments](https://doc.rust-lang.org/reference/comments.html#doc-comments)
|
||||
and programming logic to document your API.
|
||||
|
||||
The generated [OpenAPI][OpenAPI_3.0.0] files can then be used to in by various programs to
|
||||
The generated [OpenAPI][OpenAPI_3.0.0] files can then be used by various programs to
|
||||
visualize the documentation. Rocket-okapi currently includes [RapiDoc][RapiDoc] and
|
||||
[Swagger UI][Swagger_UI], but others can be used too.
|
||||
|
||||
@@ -95,14 +96,14 @@ pub fn make_rocket() -> rocket::Rocket {
|
||||
```
|
||||
|
||||
## TODO
|
||||
- Tests
|
||||
- Documentation
|
||||
- Benchmark/optimise memory usage and allocations
|
||||
- [ ] Tests
|
||||
- [ ] Documentation
|
||||
- [ ] Benchmark/optimise memory usage and allocations
|
||||
- Note to self: https://crates.io/crates/graphannis-malloc_size_of looks useful
|
||||
- Implement `OpenApiFrom___`/`OpenApiResponder` for more rocket/rocket-contrib types
|
||||
- Allow customizing openapi generation settings, e.g.
|
||||
- custom json schema generation settings
|
||||
- change path the document is hosted at
|
||||
- [ ] Implement `OpenApiFrom___`/`OpenApiResponder` for more rocket/rocket-contrib types
|
||||
- [ ] Allow customizing openapi generation settings, e.g.
|
||||
- [ ] custom json schema generation settings
|
||||
- [x] change path the document is hosted at
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
/.idea
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "custom_schema"
|
||||
version = "0.1.0"
|
||||
authors = ["Ralph Bisschops <ralph.bisschops.dev@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
|
||||
schemars = { version = "0.8", features = ["preserve_order"] }
|
||||
okapi = { version = "0.6.0-alpha-1", path = "../../okapi" }
|
||||
rocket_okapi = { version = "0.8.0-alpha-1", path = "../../rocket-okapi", features = ["swagger", "rapidoc"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Custom Schema
|
||||
|
||||
This example shows how to add custom info and merge OpenAPI files before hosting the file.
|
||||
This is useful for when projects grow bigger and you want to separate the routes into
|
||||
separate modules.
|
||||
@@ -0,0 +1,117 @@
|
||||
use okapi::openapi3::Responses;
|
||||
use rocket::{
|
||||
http::{ContentType, Status},
|
||||
request::Request,
|
||||
response::{self, Responder, Response},
|
||||
};
|
||||
use rocket_okapi::{gen::OpenApiGenerator, response::OpenApiResponderInner, OpenApiError};
|
||||
use schemars::Map;
|
||||
|
||||
/// 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 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_okapi::settings::UrlObject;
|
||||
use rocket_okapi::{mount_endpoints_and_merged_docs, rapidoc::*, swagger_ui::*};
|
||||
|
||||
mod error;
|
||||
mod message;
|
||||
mod post;
|
||||
|
||||
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(
|
||||
"/swagger-ui/",
|
||||
make_swagger_ui(&SwaggerUIConfig {
|
||||
url: "../v1/openapi.json".to_owned(),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.mount(
|
||||
"/rapidoc/",
|
||||
make_rapidoc(&RapiDocConfig {
|
||||
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,
|
||||
"/v1/" => custom_route_spec,
|
||||
"/v1/post" => post::get_routes_and_docs(),
|
||||
"/v1/message" => message::get_routes_and_docs(),
|
||||
};
|
||||
|
||||
building_rocket
|
||||
}
|
||||
|
||||
fn custom_openapi_spec() -> OpenApi {
|
||||
use okapi::openapi3::*;
|
||||
OpenApi {
|
||||
openapi: OpenApi::default_version(),
|
||||
info: Info {
|
||||
title: "The best API ever".to_owned(),
|
||||
description: Some("This is the best API every, 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()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::form::FromForm;
|
||||
use rocket::{get, post, serde::json::Json};
|
||||
use rocket_okapi::openapi;
|
||||
use rocket_okapi::parse_openapi_routes;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn get_routes_and_docs() -> (Vec<rocket::Route>, OpenApi) {
|
||||
parse_openapi_routes![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(),
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::form::FromForm;
|
||||
use rocket::{get, post, serde::json::Json};
|
||||
use rocket_okapi::openapi;
|
||||
use rocket_okapi::parse_openapi_routes;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn get_routes_and_docs() -> (Vec<rocket::Route>, OpenApi) {
|
||||
parse_openapi_routes![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()),
|
||||
}))
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
# JSON Web API
|
||||
|
||||
A simple web API written using [Rocket](https://rocket.rs/) including openapi. The openapi document will be hosted at `/openapi.json`, and the Swagger UI will be at `/swagger-ui`.
|
||||
A simple web API written using [Rocket](https://rocket.rs/) including openapi.
|
||||
The openapi document will be hosted at `/openapi.json`,
|
||||
and the Swagger UI will be at `/swagger-ui`. The RapiDoc UI will be at `/rapidoc`.
|
||||
@@ -6,6 +6,9 @@ This project follows the [Semantic Versioning standard](https://semver.org/).
|
||||
|
||||
### Added
|
||||
- Forbid unsafe code in this crate. (#36)
|
||||
- Allow customization of OpenApi object.
|
||||
- Allow merging of OpenApi objects.
|
||||
- Added `log v0.4` as a dependency.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@ derive_json_schema = ["schemars/derive_json_schema"]
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
pub type Map<K, V> = schemars::Map<K, V>;
|
||||
pub type MapEntry<'a, K, V> = schemars::MapEntry<'a, K, V>;
|
||||
|
||||
pub mod merge;
|
||||
pub mod openapi3;
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
use crate::openapi3::{Components, Info, OpenApi, PathItem, Tag};
|
||||
use crate::{Map, MapEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct MergeError {
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
impl Display for MergeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeError {
|
||||
fn new<S: AsRef<str>>(msg: S) -> Self {
|
||||
MergeError {
|
||||
msg: msg.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApi {
|
||||
/// Merge the given OpenAPI spec into the current one.
|
||||
pub fn merge_spec<S: Display>(mut self, path_prefix: &S, s2: &Self) -> Result<(), MergeError> {
|
||||
merge_specs(&mut self, path_prefix, s2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Marge the list of all specs together into on big OpenApi object.
|
||||
pub fn marge_spec_list<S: Display>(spec_list: &[(S, OpenApi)]) -> Result<OpenApi, MergeError> {
|
||||
let mut openapi_docs = OpenApi::new();
|
||||
for (path_prefix, spec) in spec_list {
|
||||
merge_specs(&mut openapi_docs, path_prefix, spec)?;
|
||||
}
|
||||
Ok(openapi_docs)
|
||||
}
|
||||
|
||||
/// Merge the given OpenAPI spec into the current one.
|
||||
pub fn merge_specs<S: Display>(
|
||||
s1: &mut OpenApi,
|
||||
path_prefix: &S,
|
||||
s2: &OpenApi,
|
||||
) -> Result<(), MergeError> {
|
||||
// Check if specs are same version
|
||||
if s1.openapi != s2.openapi {
|
||||
return Err(MergeError::new("OpenAPI specs version do not match."));
|
||||
}
|
||||
merge_spec_info(&mut s1.info, &s2.info)?;
|
||||
merge_vec(&mut s1.servers, &s2.servers);
|
||||
merge_paths(&mut s1.paths, path_prefix, &s2.paths)?;
|
||||
merge_components(&mut s1.components, &s2.components)?;
|
||||
// This is a `Vec<Map<String, _>` but just merge the `Vec` items together.
|
||||
// Do not merge the `Map` items together.
|
||||
merge_vec(&mut s1.security, &s2.security);
|
||||
merge_tags(&mut s1.tags, &s2.tags)?;
|
||||
// Replace the external_docs info as 1 block, so don't mix
|
||||
merge_option(&mut s1.external_docs, &s2.external_docs);
|
||||
merge_map(&mut s1.extensions, &s2.extensions, "extensions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_spec_info(s1: &mut Info, s2: &Info) -> Result<(), MergeError> {
|
||||
s1.title = merge_string(&s1.title, &s2.title);
|
||||
merge_opt_string(&mut s1.description, &s2.description);
|
||||
merge_opt_string(&mut s1.terms_of_service, &s2.terms_of_service);
|
||||
// Replace the contact info as 1 block, so don't mix
|
||||
merge_option(&mut s1.contact, &s2.contact);
|
||||
// Replace the license info as 1 block, so don't mix
|
||||
merge_option(&mut s1.license, &s2.license);
|
||||
s1.version = merge_string(&s1.version, &s2.version);
|
||||
merge_map(&mut s1.extensions, &s2.extensions, "extensions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge `Map<String, PathItem>`/`&Map<String, PathItem>`:
|
||||
/// Merge together. If key already exists, use s1 version.
|
||||
/// Use `path_prefix` in order to specify the mounting points for the routes.
|
||||
pub fn merge_paths<S: Display>(
|
||||
s1: &mut Map<String, PathItem>,
|
||||
path_prefix: &S,
|
||||
s2: &Map<String, PathItem>,
|
||||
) -> Result<(), MergeError> {
|
||||
// Add all s2 values
|
||||
// (if key does not already exists)
|
||||
for (key, value) in s2 {
|
||||
let new_key = if key.starts_with('/') {
|
||||
format!("{}{}", path_prefix, key)
|
||||
} else {
|
||||
log::error!(
|
||||
"All routes should have a leading '/' but non found in `{}`.",
|
||||
key
|
||||
);
|
||||
format!("{}/{}", path_prefix, key)
|
||||
};
|
||||
match s1.entry(new_key) {
|
||||
MapEntry::Occupied(mut entry) => {
|
||||
// Merge `PathItem` so get/post/put routes are getting merged
|
||||
let current_value = entry.get_mut();
|
||||
merge_path_item(current_value, value)?;
|
||||
}
|
||||
MapEntry::Vacant(entry) => {
|
||||
entry.insert(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_path_item(s1: &mut PathItem, s2: &PathItem) -> Result<(), MergeError> {
|
||||
merge_opt_string(&mut s1.reference, &s2.reference);
|
||||
merge_opt_string(&mut s1.summary, &s2.summary);
|
||||
merge_opt_string(&mut s1.description, &s2.description);
|
||||
|
||||
merge_option(&mut s1.get, &s2.get);
|
||||
merge_option(&mut s1.put, &s2.put);
|
||||
merge_option(&mut s1.post, &s2.post);
|
||||
merge_option(&mut s1.delete, &s2.delete);
|
||||
merge_option(&mut s1.options, &s2.options);
|
||||
merge_option(&mut s1.head, &s2.head);
|
||||
merge_option(&mut s1.patch, &s2.patch);
|
||||
merge_option(&mut s1.trace, &s2.trace);
|
||||
|
||||
merge_option(&mut s1.servers, &s2.servers);
|
||||
merge_vec(&mut s1.parameters, &s2.parameters);
|
||||
merge_map(&mut s1.extensions, &s2.extensions, "extensions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_components(
|
||||
s1: &mut Option<Components>,
|
||||
s2: &Option<Components>,
|
||||
) -> Result<(), MergeError> {
|
||||
if s1.is_none() {
|
||||
*s1 = s2.clone();
|
||||
Ok(())
|
||||
} else if s2.is_none() {
|
||||
// Use/keep s1
|
||||
Ok(())
|
||||
} else {
|
||||
if let Some(s1) = s1 {
|
||||
let s2 = s2.as_ref().unwrap();
|
||||
merge_map(&mut s1.schemas, &s2.schemas, "schemas");
|
||||
merge_map(&mut s1.responses, &s2.responses, "responses");
|
||||
merge_map(&mut s1.parameters, &s2.parameters, "parameters");
|
||||
merge_map(&mut s1.examples, &s2.examples, "examples");
|
||||
merge_map(&mut s1.request_bodies, &s2.request_bodies, "request_bodies");
|
||||
merge_map(&mut s1.headers, &s2.headers, "headers");
|
||||
merge_map(
|
||||
&mut s1.security_schemes,
|
||||
&s2.security_schemes,
|
||||
"security_schemes",
|
||||
);
|
||||
merge_map(&mut s1.links, &s2.links, "links");
|
||||
merge_map(&mut s1.callbacks, &s2.callbacks, "callbacks");
|
||||
merge_map(&mut s1.extensions, &s2.extensions, "extensions");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_tags(s1: &mut Vec<Tag>, s2: &[Tag]) -> Result<Vec<Tag>, MergeError> {
|
||||
// Create a `Map` so we can easily merge tag names.
|
||||
let mut new_tags: Map<String, Tag> = Map::new();
|
||||
// Add all s1 tags
|
||||
for tag in s1 {
|
||||
match new_tags.entry(tag.name.clone()) {
|
||||
MapEntry::Occupied(mut entry) => {
|
||||
let current_value = entry.get_mut();
|
||||
merge_tag(current_value, tag)?;
|
||||
}
|
||||
MapEntry::Vacant(entry) => {
|
||||
entry.insert(tag.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add all s2 tags
|
||||
for tag in s2 {
|
||||
match new_tags.entry(tag.name.clone()) {
|
||||
MapEntry::Occupied(mut entry) => {
|
||||
let current_value = entry.get_mut();
|
||||
merge_tag(current_value, tag)?;
|
||||
}
|
||||
MapEntry::Vacant(entry) => {
|
||||
entry.insert(tag.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert `Map` to `Vec`
|
||||
let mut new_tags_vec = Vec::new();
|
||||
|
||||
// Remove/add in reverser order
|
||||
for _i in 0..new_tags.len() {
|
||||
if let Some((_name, tag)) = new_tags.pop() {
|
||||
new_tags_vec.push(tag);
|
||||
} else {
|
||||
unreachable!("List sizes or same list do not match.");
|
||||
}
|
||||
}
|
||||
// Switch the order to to keep the order consistent.
|
||||
new_tags_vec.reverse();
|
||||
Ok(new_tags_vec)
|
||||
}
|
||||
|
||||
pub fn merge_tag(s1: &mut Tag, s2: &Tag) -> Result<(), MergeError> {
|
||||
if s1.name != s2.name {
|
||||
return Err(MergeError::new("Tried to merge Tags with different names."));
|
||||
}
|
||||
merge_opt_string(&mut s1.description, &s2.description);
|
||||
merge_option(&mut s1.external_docs, &s2.external_docs);
|
||||
merge_map(&mut s1.extensions, &s2.extensions, "extensions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge `String`/`&str`:
|
||||
/// - If one is empty: Use other
|
||||
/// - Otherwise: Use first value
|
||||
pub fn merge_string(s1: &str, s2: &str) -> String {
|
||||
if s1.is_empty() {
|
||||
s2.to_owned()
|
||||
} else {
|
||||
s1.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge `Option<String>`/`&Option<String>`:
|
||||
/// - If one is `None`: Use other
|
||||
/// - If both are `Some`: Merge `String`
|
||||
/// - Otherwise: Use first value
|
||||
pub fn merge_opt_string(s1: &mut Option<String>, s2: &Option<String>) {
|
||||
if s1.is_none() {
|
||||
*s1 = s2.clone();
|
||||
} else if s1.is_some() && s2.is_some() {
|
||||
*s1 = Some(merge_string(s1.as_ref().unwrap(), s2.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge `Option<T>`/`&Option<T>`:
|
||||
/// - If one is `None`: Use other
|
||||
/// - Otherwise: Use first value
|
||||
pub fn merge_option<T: Clone>(s1: &mut Option<T>, s2: &Option<T>) {
|
||||
if s1.is_none() {
|
||||
*s1 = s2.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge `Map<String, _>`/`&Map<String, _>`:
|
||||
/// Merge together. If key already exists, use s1 version.
|
||||
pub fn merge_map<T: Clone + PartialEq>(s1: &mut Map<String, T>, s2: &Map<String, T>, name: &str) {
|
||||
// Add all s2 values
|
||||
// (if key does not already exists)
|
||||
for (key, value) in s2 {
|
||||
if let Some(s1_value) = s1.get(key) {
|
||||
// Check if this is the same element
|
||||
if value != s1_value {
|
||||
log::warn!(
|
||||
"Found conflicting {} keys while merging, \
|
||||
they have the same name but different values: `{}`",
|
||||
name,
|
||||
key
|
||||
);
|
||||
}
|
||||
} else {
|
||||
s1.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge `Vec<_>`/`&Vec<_>`:
|
||||
/// Append lists, `s1` first and `s2` after that.
|
||||
pub fn merge_vec<T: Clone>(s1: &mut Vec<T>, s2: &[T]) {
|
||||
// Add all s2 values
|
||||
for value in s2 {
|
||||
s1.push(value.clone());
|
||||
}
|
||||
}
|
||||
+17
-3
@@ -4,9 +4,9 @@ pub use schemars::schema::SchemaObject;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
type Object = Map<String, Value>;
|
||||
|
||||
type SecurityRequirement = Map<String, Vec<String>>;
|
||||
pub type Object = Map<String, Value>;
|
||||
pub type SecurityRequirement = Map<String, Vec<String>>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "derive_json_schema", derive(JsonSchema))]
|
||||
@@ -29,6 +29,19 @@ impl<T> From<T> for RefOr<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApi {
|
||||
pub fn new() -> Self {
|
||||
OpenApi {
|
||||
openapi: Self::default_version(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_version() -> String {
|
||||
"3.0.0".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
#[cfg_attr(feature = "derive_json_schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -57,6 +70,7 @@ pub struct Info {
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// URL to the terms of service.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub terms_of_service: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -242,7 +256,7 @@ pub struct Response {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parameter {
|
||||
pub name: String,
|
||||
// TODO this should probaby be an enum, not String
|
||||
// TODO this should probably be an enum, not String
|
||||
#[serde(rename = "in")]
|
||||
pub location: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate quote;
|
||||
#[macro_use]
|
||||
extern crate syn;
|
||||
|
||||
extern crate proc_macro;
|
||||
//! This crate is used by [`rocket_okapi`](https://crates.io/crates/rocket_okapi)
|
||||
//! for code generation. This crate includes the procedural macros like:
|
||||
//! - `#[openapi]`: To generate the documentation for an endpoint/route.
|
||||
//! - `routes_with_openapi![...]`: To generate and add the `openapi.json` route.
|
||||
//! - `parse_openapi_routes![...]`: To generate and return a list of routes and the openapi spec.
|
||||
//! - `create_openapi_spec![...]`: To generate and return the openapi spec.
|
||||
//!
|
||||
//! The last 3 macros have very similar behavior, but differ in what they return.
|
||||
//! Here is a list of the marcos and what they return:
|
||||
//! - `routes_with_openapi![...]`: `Vec<rocket::Route>` (adds route for `openapi.json`)
|
||||
//! - `parse_openapi_routes![...]`: `(Vec<rocket::Route>, okapi::openapi3::OpenApi)`
|
||||
//! - `create_openapi_spec![...]`: `okapi::openapi3::OpenApi`
|
||||
//!
|
||||
|
||||
mod openapi_attr;
|
||||
mod routes_with_openapi;
|
||||
mod openapi_spec;
|
||||
mod parse_routes;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::Ident;
|
||||
|
||||
/// A proc macro to be used in tandem with one of `Rocket`'s endpoint macros. It requires that all
|
||||
@@ -38,10 +47,71 @@ pub fn openapi(args: TokenStream, mut input: TokenStream) -> TokenStream {
|
||||
|
||||
/// A replacement macro for `rocket::routes`. The key differences are that this macro will add an
|
||||
/// additional element to the resulting `Vec<rocket::Route>`, which serves a static file called
|
||||
/// `openapi.json`. This file can then be used to display the routes in the swagger ui.
|
||||
/// `openapi.json`. This file can then be used to display the routes in the Swagger/RapiDoc UI.
|
||||
#[proc_macro]
|
||||
pub fn routes_with_openapi(input: TokenStream) -> TokenStream {
|
||||
routes_with_openapi::parse(input)
|
||||
let settings = quote! {
|
||||
::rocket_okapi::settings::OpenApiSettings::new()
|
||||
};
|
||||
let spec =
|
||||
openapi_spec::create_openapi_spec(input.clone()).unwrap_or_else(|e| e.to_compile_error());
|
||||
let routes = parse_routes::parse_routes(input, true).unwrap_or_else(|e| e.to_compile_error());
|
||||
(quote! {
|
||||
{
|
||||
let settings = #settings;
|
||||
let spec: ::okapi::openapi3::OpenApi = #spec(settings.clone());
|
||||
let routes: Vec<::rocket::Route> = #routes(spec, settings);
|
||||
routes
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A replacement macro for `rocket::routes`. This parses the routes and provides
|
||||
/// a tuple with 2 parts `(Vec<rocket::Route>, OpenApi)`:
|
||||
/// - `Vec<rocket::Route>`: A list of all the routes that `rocket::routes![]` would have provided.
|
||||
/// - `OpenApi`: The `okapi::openapi3::OpenApi` spec for all the routes.
|
||||
///
|
||||
/// NOTE: This marco is different from `routes_with_openapi` in that it does not add
|
||||
/// the `openapi.json` file to the list of routes. This is done so the `OpenApi` spec can be changed
|
||||
/// before serving it.
|
||||
#[proc_macro]
|
||||
pub fn parse_openapi_routes(input: TokenStream) -> TokenStream {
|
||||
let settings = quote! {
|
||||
::rocket_okapi::settings::OpenApiSettings::new()
|
||||
};
|
||||
let spec =
|
||||
openapi_spec::create_openapi_spec(input.clone()).unwrap_or_else(|e| e.to_compile_error());
|
||||
let routes = parse_routes::parse_routes(input, false).unwrap_or_else(|e| e.to_compile_error());
|
||||
(quote! {
|
||||
{
|
||||
let settings = #settings;
|
||||
let spec: ::okapi::openapi3::OpenApi = #spec(settings.clone());
|
||||
let routes: Vec<::rocket::Route> = #routes(spec.clone(), settings);
|
||||
(routes, spec)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Generate `OpenApi` spec only, no parsing of routes.
|
||||
/// This can be used in cases where you are only interested in openAPI spec, but not in the routes.
|
||||
/// A use case could be inside of `build.rs` scripts or where you want to alter OpenAPI object
|
||||
/// at runtime.
|
||||
#[proc_macro]
|
||||
pub fn create_openapi_spec(input: TokenStream) -> TokenStream {
|
||||
let settings = quote! {
|
||||
::rocket_okapi::settings::OpenApiSettings::new()
|
||||
};
|
||||
let spec = openapi_spec::create_openapi_spec(input).unwrap_or_else(|e| e.to_compile_error());
|
||||
(quote! {
|
||||
{
|
||||
let settings = #settings;
|
||||
let spec: ::okapi::openapi3::OpenApi = #spec(settings);
|
||||
(spec)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn get_add_operation_fn_name(route_fn_name: &Ident) -> Ident {
|
||||
|
||||
@@ -5,10 +5,11 @@ use crate::get_add_operation_fn_name;
|
||||
use darling::FromMeta;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use rocket_http::Method;
|
||||
use std::collections::BTreeMap as Map;
|
||||
use syn::{AttributeArgs, FnArg, Ident, ItemFn, ReturnType, Type, TypeTuple};
|
||||
use syn::{parse_macro_input, AttributeArgs, FnArg, Ident, ItemFn, ReturnType, Type, TypeTuple};
|
||||
|
||||
#[derive(Debug, Default, FromMeta)]
|
||||
#[darling(default)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use darling::{Error, FromMeta};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, quote_spanned};
|
||||
use rocket_http::{ext::IntoOwned, uri::Origin, MediaType, Method};
|
||||
use std::str::FromStr;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
+8
-15
@@ -1,21 +1,16 @@
|
||||
use crate::get_add_operation_fn_name;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{parse::Parser, punctuated::Punctuated, token::Comma, Path, Result};
|
||||
|
||||
pub fn parse(routes: TokenStream) -> TokenStream {
|
||||
parse_inner(routes)
|
||||
.unwrap_or_else(|e| e.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
fn parse_inner(routes: TokenStream) -> Result<TokenStream2> {
|
||||
/// Parses routes and returns a function that takes `OpenApiSettings` and returns `OpenApi` spec.
|
||||
pub fn create_openapi_spec(routes: TokenStream) -> Result<TokenStream2> {
|
||||
let paths = <Punctuated<Path, Comma>>::parse_terminated.parse(routes)?;
|
||||
let add_operations = create_add_operations(paths.clone());
|
||||
let add_operations = create_add_operations(paths);
|
||||
Ok(quote! {
|
||||
{
|
||||
let settings = ::rocket_okapi::settings::OpenApiSettings::new();
|
||||
let mut gen = ::rocket_okapi::gen::OpenApiGenerator::new(settings.clone());
|
||||
|settings: ::rocket_okapi::settings::OpenApiSettings| -> ::okapi::openapi3::OpenApi {
|
||||
let mut gen = ::rocket_okapi::gen::OpenApiGenerator::new(settings);
|
||||
#add_operations
|
||||
let mut spec = gen.into_openapi();
|
||||
let mut info = ::okapi::openapi3::Info {
|
||||
@@ -36,15 +31,13 @@ fn parse_inner(routes: TokenStream) -> Result<TokenStream2> {
|
||||
if !env!("CARGO_PKG_HOMEPAGE").is_empty() {
|
||||
info.contact = Some(::okapi::openapi3::Contact{
|
||||
name: Some("Homepage".to_owned()),
|
||||
url: Some(env!("CARGO_PKG_REPOSITORY").to_owned()),
|
||||
url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||
..okapi::openapi3::Contact::default()
|
||||
});
|
||||
}
|
||||
spec.info = info;
|
||||
|
||||
let mut routes = ::rocket::routes![#paths];
|
||||
routes.push(::rocket_okapi::handlers::OpenApiHandler::new(spec).into_route(&settings.json_path));
|
||||
routes
|
||||
spec
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{parse::Parser, punctuated::Punctuated, token::Comma, Path, Result};
|
||||
|
||||
/// Parses routes and returns a function that takes `OpenApi` and `OpenApiSettings` and
|
||||
/// returns `Vec<rocket::Route>`.
|
||||
/// It optionally adds the `openapi.json` route to the list of routes.
|
||||
pub fn parse_routes(routes: TokenStream, add_openapi_json: bool) -> Result<TokenStream2> {
|
||||
let paths = <Punctuated<Path, Comma>>::parse_terminated.parse(routes)?;
|
||||
// This returns a function so the spec does not have to be generated multiple times.
|
||||
Ok(quote! {
|
||||
|spec: ::okapi::openapi3::OpenApi, settings: ::rocket_okapi::settings::OpenApiSettings|
|
||||
-> Vec<::rocket::Route> {
|
||||
let mut routes = ::rocket::routes![#paths];
|
||||
if #add_openapi_json {
|
||||
routes.push(
|
||||
::rocket_okapi::handlers::OpenApiHandler::new(spec)
|
||||
.into_route(&settings.json_path)
|
||||
);
|
||||
}
|
||||
routes
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,11 @@ This project follows the [Semantic Versioning standard](https://semver.org/).
|
||||
viewer. (Based on #33)
|
||||
- Added RapiDoc v9.0.0
|
||||
- Forbid unsafe code in this crate. (#36)
|
||||
- Retrieve OpenApi object after generating. (#28)
|
||||
- Create `mount_endpoints_and_merged_docs` marco in order to streamline code structure for
|
||||
bigger projects. (#30)
|
||||
- Added new example for structuring bigger projects.
|
||||
- Allowed changing path where OpenApi file is hosted.
|
||||
|
||||
### Changed
|
||||
- Swagger UI is now only available under the feature `swagger`.
|
||||
|
||||
+70
-1
@@ -42,7 +42,7 @@
|
||||
//! }
|
||||
//!
|
||||
//! fn get_docs() -> SwaggerUIConfig {
|
||||
//! use rocket_okapi::swagger_ui::UrlObject;
|
||||
//! use rocket_okapi::settings::UrlObject;
|
||||
//!
|
||||
//! SwaggerUIConfig {
|
||||
//! url: "/my_resource/openapi.json".to_string(),
|
||||
@@ -98,3 +98,72 @@ pub struct OperationInfo {
|
||||
/// Contains information to be showed in the documentation about this endpoint.
|
||||
pub operation: okapi::openapi3::Operation,
|
||||
}
|
||||
|
||||
/// Convert OpenApi object to routable endpoint.
|
||||
pub fn get_openapi_route(
|
||||
spec: okapi::openapi3::OpenApi,
|
||||
settings: &settings::OpenApiSettings,
|
||||
) -> rocket::Route {
|
||||
handlers::OpenApiHandler::new(spec).into_route(&settings.json_path)
|
||||
}
|
||||
|
||||
/// Mount endpoints and mount merged OpenAPI documentation.
|
||||
///
|
||||
/// This marco just makes to code look cleaner and improves readability
|
||||
/// for bigger codebases.
|
||||
///
|
||||
/// The macro expects the following arguments:
|
||||
/// - rocket_builder: `Rocket<Build>`,
|
||||
/// - docs_path: `&str`, `String` or [`Uri`](rocket::http::uri::Uri).
|
||||
/// Anything accepted by [`mount()`](https://docs.rs/rocket/0.5.0-rc.1/rocket/struct.Rocket.html#method.mount)
|
||||
/// - openapi_settings: (optional) `OpenApiSettings`,
|
||||
/// - List of (0 or more):
|
||||
/// - path: `&str`, `String` or [`Uri`](rocket::http::uri::Uri).
|
||||
/// Anything accepted by `mount()`
|
||||
/// - route_and_docs: `(Vec<rocket::Route>, OpenApi)`
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust,ignore
|
||||
/// let custom_route_spec = (vec![], custom_spec());
|
||||
/// mount_endpoints_and_merged_docs! {
|
||||
/// building_rocket, "/".to_owned(),
|
||||
/// "/" => custom_route_spec,
|
||||
/// "/v1/post" => post::get_routes_and_docs(),
|
||||
/// "/v1/message" => message::get_routes_and_docs(),
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! mount_endpoints_and_merged_docs {
|
||||
($rocket_builder:ident, $docs_path:expr, $openapi_settings:ident,
|
||||
$($path:expr => $route_and_docs:expr),* $(,)*) => {{
|
||||
let mut openapi_list: Vec<(_, okapi::openapi3::OpenApi)> = Vec::new();
|
||||
$({
|
||||
let (routes, openapi) = $route_and_docs;
|
||||
$rocket_builder = $rocket_builder.mount($path, routes);
|
||||
openapi_list.push(($path, openapi));
|
||||
})*
|
||||
// Combine all OpenApi documentation into one struct.
|
||||
let openapi_docs = match okapi::merge::marge_spec_list(&openapi_list){
|
||||
Ok(docs) => docs,
|
||||
Err(err) => panic!("Could not merge OpenAPI spec: {}", err),
|
||||
};
|
||||
// Add OpenApi route
|
||||
$rocket_builder = $rocket_builder.mount(
|
||||
$docs_path,
|
||||
vec![rocket_okapi::get_openapi_route(
|
||||
openapi_docs,
|
||||
&$openapi_settings,
|
||||
)],
|
||||
);
|
||||
}};
|
||||
|
||||
($rocket_builder:ident, $docs_path:expr,
|
||||
$($path:expr => $route_and_docs:expr),* $(,)*) => {
|
||||
let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
mount_endpoints_and_merged_docs!{
|
||||
$rocket_builder, $docs_path, openapi_settings,
|
||||
$($path:expr => $route_and_docs:expr),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ macro_rules! static_file {
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::collections::HashMap;
|
||||
/// use df_ls_diagnostics::hash_map;
|
||||
/// use rocket_okapi::hash_map;
|
||||
///
|
||||
/// let my_hash_map = hash_map!{
|
||||
/// "token_name".to_owned() => "CREATURE",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use schemars::gen::SchemaSettings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Settings which are used to customise the behaviour of the `OpenApiGenerator`.
|
||||
/// Settings which are used to customize the behavior of the `OpenApiGenerator`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpenApiSettings {
|
||||
/// Settings to customise how JSON Schemas are generated.
|
||||
/// Settings to customize how JSON Schemas are generated.
|
||||
pub schema_settings: SchemaSettings,
|
||||
/// The path to the json file that contains the API specification. Then default is
|
||||
/// `openapi.json`.
|
||||
|
||||
Reference in New Issue
Block a user