diff --git a/crates/oapi-macros/Cargo.toml b/crates/oapi-macros/Cargo.toml index 790a8f5ae..86d8bf836 100644 --- a/crates/oapi-macros/Cargo.toml +++ b/crates/oapi-macros/Cargo.toml @@ -31,6 +31,7 @@ uuid = { workspace = true, optional = true } default = [] chrono = [] decimal = [] +decimal-float = [] ulid = ["dep:ulid"] url = ["dep:url"] uuid = ["dep:uuid"] diff --git a/crates/oapi-macros/src/schema_type.rs b/crates/oapi-macros/src/schema_type.rs index d8337dc81..83129f548 100644 --- a/crates/oapi-macros/src/schema_type.rs +++ b/crates/oapi-macros/src/schema_type.rs @@ -28,6 +28,7 @@ impl SchemaType<'_> { #[cfg(not(any( feature = "chrono", feature = "decimal", + feature = "decimal-float", feature = "ulid", feature = "url", feature = "uuid", @@ -40,6 +41,7 @@ impl SchemaType<'_> { #[cfg(any( feature = "chrono", feature = "decimal", + feature = "decimal-float", feature = "ulid", feature = "url", feature = "uuid", @@ -53,7 +55,7 @@ impl SchemaType<'_> { primitive = is_primitive_chrono(name); } - #[cfg(feature = "decimal")] + #[cfg(any(feature = "decimal", feature = "decimal-float"))] if !primitive { primitive = is_primitive_rust_decimal(name); } @@ -143,7 +145,7 @@ fn is_primitive_chrono(name: &str) -> bool { } #[inline] -#[cfg(feature = "decimal")] +#[cfg(any(feature = "decimal", feature = "decimal-float"))] fn is_primitive_rust_decimal(name: &str) -> bool { matches!(name, "Decimal") } @@ -173,8 +175,10 @@ impl ToTokens for SchemaType<'_> { "NaiveDate" => tokens.extend(quote!(#oapi::oapi::SchemaType::String)), #[cfg(any(feature = "chrono", feature = "time"))] "Date" | "Duration" => tokens.extend(quote! { #oapi::oapi::SchemaType::String }), - #[cfg(feature = "decimal")] + #[cfg(all(feature = "decimal", not(feature = "decimal-float")))] "Decimal" => tokens.extend(quote! { #oapi::oapi::SchemaType::String }), + #[cfg(all(not(feature = "decimal"), feature = "decimal-float"))] + "Decimal" => tokens.extend(quote! { utoipa::openapi::SchemaType::Number }), #[cfg(feature = "ulid")] "Ulid" => tokens.extend(quote! { #oapi::oapi::SchemaType::String }), #[cfg(feature = "uuid")] @@ -242,9 +246,10 @@ impl Type<'_> { #[cfg(not(any( feature = "chrono", + feature = "decimal-float", feature = "ulid", - feature = "url", feature = "uuid", + feature = "url", feature = "time" )))] { @@ -253,9 +258,10 @@ impl Type<'_> { #[cfg(any( feature = "chrono", + feature = "decimal-float", feature = "ulid", - feature = "url", feature = "uuid", + feature = "url", feature = "time" ))] { @@ -265,7 +271,10 @@ impl Type<'_> { if !known_format { known_format = matches!(name, "DateTime" | "Date" | "NaiveDate" | "NaiveDateTime"); } - + #[cfg(feature = "decimal-float")] + if !known_format { + known_format = matches!(name, "Decimal"); + } #[cfg(feature = "ulid")] if !known_format { known_format = matches!(name, "Ulid"); diff --git a/crates/oapi/Cargo.toml b/crates/oapi/Cargo.toml index df4477f21..85d619b7b 100644 --- a/crates/oapi/Cargo.toml +++ b/crates/oapi/Cargo.toml @@ -20,6 +20,7 @@ rapidoc = [] redoc = [] chrono = ["salvo-oapi-macros/chrono"] decimal = ["salvo-oapi-macros/decimal"] +decimal_float = ["salvo-oapi-macros/decimal-float"] yaml = ["dep:serde_yaml"] ulid = ["salvo-oapi-macros/ulid"] uuid = ["salvo-oapi-macros/uuid"] diff --git a/crates/oapi/docs/lib.md b/crates/oapi/docs/lib.md index 4159eadc7..ea3f47ee3 100644 --- a/crates/oapi/docs/lib.md +++ b/crates/oapi/docs/lib.md @@ -4,26 +4,28 @@ you can use to annotate your code to have items documented. # Crate Features - **yaml** Enables **serde_yaml** serialization of OpenAPI objects. + - **chrono** Add support for [chrono](https://crates.io/crates/chrono) `DateTime`, `Date`, `NaiveDate` and `Duration` types. By default these types are parsed to `string` types with additional `format` information. `format: date-time` for `DateTime` and `format: date` for `Date` and `NaiveDate` according [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14) as `ISO-8601`. To override default `string` representation users have to use `value_type` attribute to override the type. See [docs](https://docs.rs/salvo_oapi/latest/salvo_oapi/derive.ToSchema.html) for more details. -- **time** Add support for [time](https://crates.io/crates/time) `OffsetDateTime`, `PrimitiveDateTime`, `Date`, and `Duration` types. - By default these types are parsed as `string`. `OffsetDateTime` and `PrimitiveDateTime` will use `date-time` format. `Date` will use - `date` format and `Duration` will not have any format. To override default `string` representation users have to use `value_type` attribute - to override the type. See [docs](https://docs.rs/salvo_oapi/latest/salvo_oapi/derive.ToSchema.html) for more details. -- **decimal** Add support for [rust_decimal](https://crates.io/crates/rust_decimal) `Decimal` type. **By default** - it is interpreted as `String`. If you wish to change the format you need to override the type. - See the `value_type` in [`ToSchema` derive docs][to_schema_derive]. -- **uuid** Add support for [uuid](https://github.com/uuid-rs/uuid). `Uuid` type will be presented as `String` with - format `uuid` in OpenAPI spec. -- **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with - format `ulid` in OpenAPI spec. -- **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with - format `uri` in OpenAPI spec. + +- **time** Add support for [time](https://crates.io/crates/time) `OffsetDateTime`, `PrimitiveDateTime`, `Date`, and `Duration` types. By default these types are parsed as `string`. `OffsetDateTime` and `PrimitiveDateTime` will use `date-time` format. `Date` will use `date` format and `Duration` will not have any format. To override default `string` representation users have to use `value_type` attribute to override the type. See [docs](https://docs.rs/salvo_oapi/latest/salvo_oapi/derive.ToSchema.html) for more details. + +- **decimal** Add support for [rust_decimal](https://crates.io/crates/rust_decimal) `Decimal` type. **By default** it is interpreted as `String`. If you wish to change the format you need to override the type. See the `value_type` in [`ToSchema` derive docs][to_schema_derive]. + +- **decimal-float** Add support for [rust_decimal](https://crates.io/crates/rust_decimal) `Decimal` type. **By default** it is interpreted as `Number`. This feature is mutually exclusive with **decimal** and allow to change the default type used in your documentation for `Decimal` much like `serde_with_float` feature exposed by rust_decimal. + +- **uuid** Add support for [uuid](https://github.com/uuid-rs/uuid). `Uuid` type will be presented as `String` with format `uuid` in OpenAPI spec. + +- **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with format `ulid` in OpenAPI spec. + +- **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with format `uri` in OpenAPI spec. + - **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`. + - **indexmap** Add support for [indexmap](https://crates.io/crates/indexmap). When enabled `IndexMap` will be rendered as a map similar to `BTreeMap` and `HashMap`. diff --git a/crates/oapi/src/openapi/schema/mod.rs b/crates/oapi/src/openapi/schema/mod.rs index e8bf13dac..7bb72c895 100644 --- a/crates/oapi/src/openapi/schema/mod.rs +++ b/crates/oapi/src/openapi/schema/mod.rs @@ -132,6 +132,12 @@ impl From for AdditionalProperties { } } +impl From for AdditionalProperties { + fn from(value: Array) -> Self { + Self::RefOr(RefOr::T(Schema::Array(value))) + } +} + impl From for AdditionalProperties { fn from(value: Ref) -> Self { Self::RefOr(RefOr::Ref(value)) @@ -423,6 +429,20 @@ mod tests { }) ); + let json_value = Object::new().additional_properties(Array::new(Object::new().schema_type(SchemaType::Number))); + assert_json_eq!( + json_value, + json!({ + "type": "object", + "additionalProperties": { + "items": { + "type": "number", + }, + "type": "array", + } + }) + ); + let json_value = Object::new().additional_properties(Ref::from_schema_name("ComplexModel")); assert_json_eq!( json_value, diff --git a/examples/acme-http01-quinn/src/main.rs b/examples/acme-http01-quinn/src/main.rs index bb168839f..a35996add 100644 --- a/examples/acme-http01-quinn/src/main.rs +++ b/examples/acme-http01-quinn/src/main.rs @@ -14,7 +14,8 @@ async fn main() { .acme() .cache_path("temp/letsencrypt") .add_domain("test.salvo.rs") - .http01_challege(&mut router).quinn("0.0.0.0:443"); + .http01_challege(&mut router) + .quinn("0.0.0.0:443"); let acceptor = listener.join(TcpListener::new("0.0.0.0:80")).bind().await; Server::new(acceptor).serve(router).await; } diff --git a/examples/cors/src/main.rs b/examples/cors/src/main.rs index e49068532..5a0b161fb 100644 --- a/examples/cors/src/main.rs +++ b/examples/cors/src/main.rs @@ -1,9 +1,8 @@ -use salvo::cors::Cors; use salvo::catcher::Catcher; +use salvo::cors::Cors; use salvo::http::Method; use salvo::prelude::*; - #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); @@ -22,7 +21,9 @@ async fn backend_server() { .allow_headers("authorization") .into_handler(); - let router = Router::with_hoop(cors.clone()).push(Router::with_path("hello").post(hello)).options(handler::empty()); + let router = Router::with_hoop(cors.clone()) + .push(Router::with_path("hello").post(hello)) + .options(handler::empty()); let acceptor = TcpListener::new("0.0.0.0:5600").bind().await; let service = Service::new(router).catcher(Catcher::default().hoop(cors)); @@ -62,4 +63,4 @@ document.getElementById("btn").addEventListener("click", function() { -"#; \ No newline at end of file +"#; diff --git a/examples/db-sea-orm/src/main.rs b/examples/db-sea-orm/src/main.rs index 18dfcddab..2d0eff0f8 100644 --- a/examples/db-sea-orm/src/main.rs +++ b/examples/db-sea-orm/src/main.rs @@ -24,7 +24,8 @@ struct AppState { #[handler] async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { let state = depot - .obtain::().map_err(|_|StatusError::internal_server_error())?; + .obtain::() + .map_err(|_| StatusError::internal_server_error())?; let form = req .parse_form::() .await @@ -46,7 +47,7 @@ async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res async fn list(req: &mut Request, depot: &mut Depot) -> Result> { let state = depot .obtain::() - .map_err(|_|StatusError::internal_server_error())?; + .map_err(|_| StatusError::internal_server_error())?; let page = req.query("page").unwrap_or(1); let posts_per_page = req.query("posts_per_page").unwrap_or(DEFAULT_POSTS_PER_PAGE); let paginator = post::Entity::find() @@ -75,7 +76,7 @@ async fn list(req: &mut Request, depot: &mut Depot) -> Result> { async fn new(depot: &mut Depot) -> Result> { let state = depot .obtain::() - .map_err(|_|StatusError::internal_server_error())?; + .map_err(|_| StatusError::internal_server_error())?; let ctx = tera::Context::new(); let body = state .templates @@ -88,7 +89,7 @@ async fn new(depot: &mut Depot) -> Result> { async fn edit(req: &mut Request, depot: &mut Depot) -> Result> { let state = depot .obtain::() - .map_err(|_|StatusError::internal_server_error())?; + .map_err(|_| StatusError::internal_server_error())?; let id = req.param::("id").unwrap_or_default(); let post: post::Model = post::Entity::find_by_id(id) .one(&state.conn) @@ -110,7 +111,7 @@ async fn edit(req: &mut Request, depot: &mut Depot) -> Result> { async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { let state = depot .obtain::() - .map_err(|_|StatusError::internal_server_error())?; + .map_err(|_| StatusError::internal_server_error())?; let id = req.param::("id").unwrap_or_default(); let form = req .parse_form::() @@ -132,7 +133,7 @@ async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res async fn delete(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { let state = depot .obtain::() - .map_err(|_|StatusError::internal_server_error())?; + .map_err(|_| StatusError::internal_server_error())?; let id = req.param::("id").unwrap_or_default(); let post: post::ActiveModel = post::Entity::find_by_id(id) .one(&state.conn) diff --git a/examples/extract-data/src/main.rs b/examples/extract-data/src/main.rs index cb4ea35ef..ba5bd0546 100644 --- a/examples/extract-data/src/main.rs +++ b/examples/extract-data/src/main.rs @@ -38,13 +38,11 @@ async fn edit(req: &mut Request) -> String { } #[derive(Serialize, Deserialize, Extractible, Debug)] -#[salvo( - extract( - default_source(from = "query"), - default_source(from = "param"), - default_source(from = "body") - ) -)] +#[salvo(extract( + default_source(from = "query"), + default_source(from = "param"), + default_source(from = "body") +))] struct BadMan<'a> { id: i64, username: &'a str, @@ -53,14 +51,12 @@ struct BadMan<'a> { lovers: Vec, } #[derive(Serialize, Deserialize, Extractible, Debug)] -#[salvo( - extract( - default_source(from = "query"), - default_source(from = "param"), - default_source(from = "body"), - rename_all = "camelCase" - ) -)] +#[salvo(extract( + default_source(from = "query"), + default_source(from = "param"), + default_source(from = "body"), + rename_all = "camelCase" +))] struct GoodMan<'a> { id: i64, username: &'a str, diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index f844cd1c3..f1c648817 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -28,8 +28,9 @@ async fn main() { } fn route() -> Router { - Router::new().get(hello) - .push(Router::with_path("a").get(hello2)) + Router::new() + .get(hello) + .push(Router::with_path("a").get(hello2)) .push(Router::with_path("你好").get(hello_zh)) .push(Router::with_path("hello3").get(hello3)) } diff --git a/examples/rate-limiter-dynamic/src/main.rs b/examples/rate-limiter-dynamic/src/main.rs index 8a1296ef2..f75b7b592 100644 --- a/examples/rate-limiter-dynamic/src/main.rs +++ b/examples/rate-limiter-dynamic/src/main.rs @@ -4,8 +4,8 @@ use std::hash::Hash; use once_cell::sync::Lazy; use salvo::prelude::*; -use salvo::Error; use salvo::rate_limiter::{CelledQuota, MokaStore, QuotaGetter, RateIssuer, RateLimiter, SlidingGuard}; +use salvo::Error; static USER_QUOTAS: Lazy> = Lazy::new(|| { let mut map = HashMap::new(); diff --git a/examples/request-id/src/main.rs b/examples/request-id/src/main.rs index 5d1802e46..d3faa05a7 100644 --- a/examples/request-id/src/main.rs +++ b/examples/request-id/src/main.rs @@ -12,4 +12,4 @@ async fn main() { let acceptor = TcpListener::new("0.0.0.0:5800").bind().await; let router = Router::new().hoop(RequestId::new()).get(hello); Server::new(acceptor).serve(router).await; -} \ No newline at end of file +} diff --git a/examples/todos-utoipa/src/main.rs b/examples/todos-utoipa/src/main.rs index 6e0399001..abe35ad56 100644 --- a/examples/todos-utoipa/src/main.rs +++ b/examples/todos-utoipa/src/main.rs @@ -274,7 +274,7 @@ mod tests { .send(super::route()) .await; - assert_eq!(res .status_code.unwrap(), StatusCode::BAD_REQUEST); + assert_eq!(res.status_code.unwrap(), StatusCode::BAD_REQUEST); } fn test_todo() -> Todo { diff --git a/examples/with-listenfd/src/main.rs b/examples/with-listenfd/src/main.rs index e55ef264e..08ee1ee4a 100644 --- a/examples/with-listenfd/src/main.rs +++ b/examples/with-listenfd/src/main.rs @@ -1,8 +1,8 @@ use std::net::SocketAddr; use listenfd::ListenFd; +use salvo::conn::tcp::TcpAcceptor; use salvo::prelude::*; -use salvo::conn::{tcp::TcpAcceptor, }; #[handler] async fn hello() -> &'static str { @@ -10,7 +10,7 @@ async fn hello() -> &'static str { } #[tokio::main] -async fn main() -> Result<(), salvo::Error> { +async fn main() -> Result<(), salvo::Error> { tracing_subscriber::fmt().init(); let router = Router::new().get(hello); @@ -27,7 +27,8 @@ async fn main() -> Result<(), salvo::Error> { std::env::var("HOST").unwrap_or("0.0.0.0".into()), std::env::var("PORT").unwrap_or("8080".into()) ) - .parse().unwrap(); + .parse() + .unwrap(); (addr, tokio::net::TcpListener::bind(addr).await.unwrap()) };