├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── axum_openapi_derive ├── .gitignore ├── Cargo.toml └── src │ ├── describe_schema.rs │ ├── handler.rs │ ├── lib.rs │ └── routes.rs ├── examples └── petstore.rs └── src ├── describe_impl.rs ├── global_collect.rs ├── lib.rs ├── openapi_adapters.rs ├── openapi_impl.rs ├── openapi_traits.rs ├── operation_impl.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum_openapi" 3 | version = "0.1.0" 4 | edition = "2018" 5 | license = "MIT" 6 | resolver = "2" 7 | 8 | [features] 9 | default = ["skip_serializing_defaults"] 10 | skip_serializing_defaults = ["openapiv3/skip_serializing_defaults"] 11 | 12 | macro-based = ["inventory", "once_cell", "axum_openapi_derive/macro-based"] 13 | 14 | [dependencies] 15 | openapiv3 = "0.5" 16 | 17 | axum = "0.1" 18 | serde = "1.0" 19 | hyper = "0.14" 20 | serde_yaml = "0.8" 21 | 22 | inventory = { version = "0.1", optional = true } 23 | once_cell = { version = "1.0", optional = true } 24 | 25 | axum_openapi_derive = { path = "./axum_openapi_derive" } 26 | 27 | [dev-dependencies] 28 | axum = { version = "0.1", features = [] } 29 | serde = { version = "1.0", features = ["derive"] } 30 | tokio = { version = "1.9", features = ["full"] } 31 | hyper = { version = "0.14", features = ["server", "tcp", "http1", "http2"] } 32 | 33 | [workspace] 34 | members = [".", "openapi_derive", "axum_openapi_derive"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jakob Hellermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WIP crate for auto-generating [openapi](https://swagger.io/specification/) descriptions for web services written using the [axum](https://github.com/tokio-rs/axum/) framework. 2 | 3 | 4 | ## Example usage: 5 | 6 | ```rust,no_run 7 | use axum::prelude::*; 8 | use std::net::SocketAddr; 9 | 10 | use axum_openapi::prelude::*; 11 | use axum_openapi::{openapi_json_endpoint, openapi_yaml_endpoint}; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let app = route("/pets", get(find_pets).post(add_pet)) 16 | .route("/pets/:id", get(find_pet_by_id).delete(delete_pet)); 17 | let openapi = app.openapi(); 18 | 19 | let app = app 20 | .route("/openapi.yaml", openapi_yaml_endpoint(openapi.clone())) 21 | .route("/openapi.json", openapi_json_endpoint(openapi)); 22 | 23 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 24 | hyper::server::Server::bind(&addr) 25 | .serve(app.into_make_service()) 26 | .await 27 | .unwrap(); 28 | } 29 | 30 | async fn find_pets(/* */) { /* */ } 31 | async fn add_pet(/* */) { /* */ } 32 | async fn find_pet_by_id(/* */) { /* */ } 33 | async fn delete_pet(/* */) { /* */ } 34 | ``` 35 | 36 | See the full example at [./examples/petstore.rs](https://github.com/jakobhellermann/axum_openapi/blob/main/examples/petstore.rs). -------------------------------------------------------------------------------- /axum_openapi_derive/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target 3 | -------------------------------------------------------------------------------- /axum_openapi_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum_openapi_derive" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [features] 10 | macro-based = [] 11 | 12 | [dependencies] 13 | quote = "1.0" 14 | syn = { version = "1.0", features = ["full"] } 15 | proc-macro2 = "1.0" 16 | 17 | regex = { version = "1.5", default-features = false, features = ["std", "unicode-perl"] } 18 | once_cell = "1.8" 19 | 20 | [dev-dependencies] 21 | syn = { version = "1.0", features = ["full", "extra-traits"] } 22 | pretty_assertions = "0.7" 23 | -------------------------------------------------------------------------------- /axum_openapi_derive/src/describe_schema.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{DataStruct, DeriveInput}; 4 | 5 | struct Config<'a> { 6 | ident: &'a syn::Ident, 7 | axum_openapi: TokenStream, 8 | macro_exports: TokenStream, 9 | } 10 | 11 | pub fn derive_schema(item: TokenStream) -> syn::Result { 12 | let input: DeriveInput = syn::parse2(item)?; 13 | 14 | let axum_openapi = quote!(axum_openapi); 15 | let macro_exports = quote!(#axum_openapi::__macro); 16 | 17 | let config = Config { 18 | ident: &input.ident, 19 | axum_openapi, 20 | macro_exports, 21 | }; 22 | 23 | match input.data { 24 | syn::Data::Struct(data) => config.derive_schema_struct(data), 25 | syn::Data::Enum(_) => todo!(), 26 | syn::Data::Union(_) => todo!(), 27 | } 28 | } 29 | 30 | impl Config<'_> { 31 | pub fn derive_schema_struct(&self, data: DataStruct) -> syn::Result { 32 | let Config { 33 | ident, 34 | macro_exports, 35 | axum_openapi, 36 | .. 37 | } = self; 38 | let openapiv3 = quote!(#macro_exports::openapiv3); 39 | 40 | let properties = data.fields.iter().map(|field| { 41 | let ty = &field.ty; 42 | let name = field.ident.as_ref().expect("todo: tuple structs"); 43 | let name = name.to_string(); 44 | 45 | quote! { 46 | (#name.to_string(), #openapiv3::ReferenceOr::Item(Box::new(<#ty as #axum_openapi::DescribeSchema>::describe_schema()))), 47 | } 48 | }); 49 | 50 | let ref_name = ident.to_string(); 51 | 52 | Ok(quote! { 53 | impl #axum_openapi::DescribeSchema for #ident { 54 | fn describe_schema() -> #openapiv3::Schema { 55 | let properties = std::array::IntoIter::new([ 56 | #(#properties)* 57 | ]).collect(); 58 | let required = Vec::new(); 59 | let obj = #openapiv3::ObjectType { 60 | properties, 61 | required, 62 | additional_properties: None, 63 | min_properties: None, 64 | max_properties: None, 65 | }; 66 | #openapiv3::Schema { 67 | schema_data: Default::default(), 68 | schema_kind: #openapiv3::SchemaKind::Type(#openapiv3::Type::Object(obj)), 69 | } 70 | } 71 | 72 | fn ref_name() -> Option { 73 | Some(#ref_name.to_string()) 74 | } 75 | } 76 | 77 | #[cfg(feature = "macro-based")] 78 | #macro_exports::inventory::submit!(#![crate = #macro_exports] #macro_exports::SchemaDescription { 79 | schema: <#ident as #axum_openapi::DescribeSchema>::describe_schema(), 80 | name: #ref_name.to_string(), 81 | }); 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /axum_openapi_derive/src/handler.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn handler(item: TokenStream, _attr: TokenStream) -> syn::Result { 5 | let fn_item: syn::ItemFn = syn::parse2(item)?; 6 | 7 | let axum_openapi = quote!(axum_openapi); 8 | let macro_exports = quote!(#axum_openapi::__macro); 9 | 10 | let types = fn_item.sig.inputs.iter().filter_map(|arg| match arg { 11 | syn::FnArg::Receiver(_) => None, 12 | syn::FnArg::Typed(pat_ty) => Some(&pat_ty.ty), 13 | }); 14 | 15 | let fn_name = &fn_item.sig.ident; 16 | 17 | let return_ty = match &fn_item.sig.output { 18 | syn::ReturnType::Default => quote! { () }, 19 | syn::ReturnType::Type(_, ty) => quote! { #ty }, 20 | }; 21 | 22 | let submit = quote! { 23 | #macro_exports::inventory::submit!(#![crate=#macro_exports] { 24 | let mut operation = #macro_exports::openapiv3::Operation { 25 | operation_id: Some(stringify!(#fn_name).to_string()), 26 | ..Default::default() 27 | }; 28 | #(<#types as #axum_openapi::OperationParameter>::modify_op(&mut operation, true);)* 29 | <#return_ty as #axum_openapi::OperationResult>::modify_op(&mut operation); 30 | 31 | #macro_exports::OperationDescription { 32 | operation 33 | } 34 | }); 35 | }; 36 | 37 | Ok(quote! { 38 | #submit 39 | #fn_item 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /axum_openapi_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | mod describe_schema; 3 | 4 | #[cfg(feature = "macro-based")] 5 | mod handler; 6 | #[cfg(feature = "macro-based")] 7 | mod routes; 8 | 9 | #[proc_macro_derive(DescribeSchema)] 10 | pub fn derive_answer_fn(item: TokenStream) -> TokenStream { 11 | describe_schema::derive_schema(item.into()) 12 | .unwrap_or_else(|e| e.into_compile_error()) 13 | .into() 14 | } 15 | 16 | #[proc_macro_attribute] 17 | #[cfg(feature = "macro-based")] 18 | pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream { 19 | handler::handler(item.into(), attr.into()) 20 | .unwrap_or_else(|e| e.into_compile_error()) 21 | .into() 22 | } 23 | 24 | #[proc_macro] 25 | #[cfg(feature = "macro-based")] 26 | pub fn routes(tokens: TokenStream) -> TokenStream { 27 | routes::routes(tokens.into()) 28 | .unwrap_or_else(|e| e.into_compile_error()) 29 | .into() 30 | } 31 | 32 | use quote::{format_ident, quote}; 33 | use syn::{ 34 | parse::{Parse, ParseStream}, 35 | parse_macro_input, 36 | token::Comma, 37 | Ident, LitInt, Result, 38 | }; 39 | 40 | struct AllTuples { 41 | macro_ident: Ident, 42 | start: usize, 43 | end: usize, 44 | idents: Vec, 45 | } 46 | 47 | impl Parse for AllTuples { 48 | fn parse(input: ParseStream) -> Result { 49 | let macro_ident = input.parse::()?; 50 | input.parse::()?; 51 | let start = input.parse::()?.base10_parse()?; 52 | input.parse::()?; 53 | let end = input.parse::()?.base10_parse()?; 54 | input.parse::()?; 55 | let mut idents = vec![input.parse::()?]; 56 | while input.parse::().is_ok() { 57 | idents.push(input.parse::()?); 58 | } 59 | 60 | Ok(AllTuples { 61 | macro_ident, 62 | start, 63 | end, 64 | idents, 65 | }) 66 | } 67 | } 68 | 69 | #[proc_macro] 70 | pub fn all_tuples(input: TokenStream) -> TokenStream { 71 | let input = parse_macro_input!(input as AllTuples); 72 | let len = (input.start..=input.end).count(); 73 | let mut ident_tuples = Vec::with_capacity(len); 74 | for i in input.start..=input.end { 75 | let idents = input 76 | .idents 77 | .iter() 78 | .map(|ident| format_ident!("{}{}", ident, i)); 79 | if input.idents.len() < 2 { 80 | ident_tuples.push(quote! { 81 | #(#idents)* 82 | }); 83 | } else { 84 | ident_tuples.push(quote! { 85 | (#(#idents),*) 86 | }); 87 | } 88 | } 89 | 90 | let macro_ident = &input.macro_ident; 91 | let invocations = (input.start..=input.end).map(|i| { 92 | let ident_tuples = &ident_tuples[0..i]; 93 | quote! { 94 | #macro_ident!(#(#ident_tuples),*); 95 | } 96 | }); 97 | TokenStream::from(quote! { 98 | #( 99 | #invocations 100 | )* 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /axum_openapi_derive/src/routes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use once_cell::sync::Lazy; 4 | use proc_macro2::TokenStream; 5 | use quote::quote; 6 | use regex::{Captures, Regex}; 7 | use syn::{punctuated::Punctuated, spanned::Spanned}; 8 | 9 | #[derive(PartialEq, Default)] 10 | struct Routes { 11 | paths: BTreeMap>, 12 | } 13 | #[derive(Debug, PartialEq)] 14 | struct Operation { 15 | method: HttpMethod, 16 | path: Option, 17 | } 18 | 19 | #[derive(Debug, Copy, Clone, PartialEq)] 20 | pub enum HttpMethod { 21 | Get, 22 | Put, 23 | Post, 24 | Delete, 25 | Options, 26 | Head, 27 | Patch, 28 | Trace, 29 | } 30 | impl PartialEq for HttpMethod { 31 | fn eq(&self, other: &str) -> bool { 32 | let s = match self { 33 | HttpMethod::Get => "get", 34 | HttpMethod::Put => "put", 35 | HttpMethod::Post => "post", 36 | HttpMethod::Delete => "delete", 37 | HttpMethod::Options => "options", 38 | HttpMethod::Head => "head", 39 | HttpMethod::Patch => "patch", 40 | HttpMethod::Trace => "trace", 41 | }; 42 | other == s 43 | } 44 | } 45 | 46 | pub fn routes(tokens: TokenStream) -> syn::Result { 47 | let (call, routes) = parse_routes(tokens)?; 48 | 49 | let axum_openapi = quote!(axum_openapi); 50 | let macro_exports = quote!(#axum_openapi::__macro); 51 | let openapiv3 = quote!(#macro_exports::openapiv3); 52 | 53 | let submit = routes.paths.into_iter().map(|(path, operations)| { 54 | let operation = |method| { 55 | operations 56 | .iter() 57 | .find(|op| op.method == method) 58 | .map(|op| { 59 | let op_id = op 60 | .path 61 | .as_ref() 62 | .and_then(|path| path.segments.last()) 63 | .map(|segment| segment.ident.to_string()) 64 | .map_or(quote!(None), |str| quote!(Some(#str.to_string()))); 65 | 66 | quote! { Some(#openapiv3::Operation { 67 | operation_id: #op_id, 68 | ..Default::default() 69 | }) } 70 | }) 71 | .unwrap_or_else(|| quote! { None }) 72 | }; 73 | 74 | let get = operation(HttpMethod::Get); 75 | let put = operation(HttpMethod::Put); 76 | let post = operation(HttpMethod::Post); 77 | let delete = operation(HttpMethod::Delete); 78 | let options = operation(HttpMethod::Options); 79 | let head = operation(HttpMethod::Head); 80 | let patch = operation(HttpMethod::Patch); 81 | let trace = operation(HttpMethod::Trace); 82 | 83 | let path_item = quote! { 84 | #openapiv3::PathItem { 85 | get: #get, 86 | put: #put, 87 | post: #post, 88 | delete: #delete, 89 | options: #options, 90 | head: #head, 91 | patch: #patch, 92 | trace: #trace, 93 | ..Default::default() 94 | } 95 | }; 96 | quote! { 97 | #macro_exports::inventory::submit!(#![crate = #macro_exports] #macro_exports::PathDescription { 98 | path: #path.to_string(), 99 | path_item: #path_item, 100 | }); 101 | } 102 | }); 103 | 104 | Ok(quote! {{ 105 | #(#submit)* 106 | #call 107 | }}) 108 | } 109 | 110 | fn parse_routes(tokens: TokenStream) -> syn::Result<(syn::Expr, Routes)> { 111 | let call: syn::Expr = syn::parse2(tokens)?; 112 | 113 | let mut routes = Routes::default(); 114 | 115 | method_or_call(&call, |ident, args| { 116 | if ident != "route" { 117 | return Err(syn::Error::new(ident.span(), "expected two arguments")); 118 | } 119 | 120 | let mut args_iter = args.iter(); 121 | let (path, handler) = match (args_iter.next(), args_iter.next()) { 122 | (Some(path), Some(handler)) => (path, handler), 123 | _ => return Err(syn::Error::new(args.span(), "expected two arguments")), 124 | }; 125 | 126 | let path = expr_string_lit(path) 127 | .ok_or_else(|| syn::Error::new(path.span(), "expected path string literal"))?; 128 | 129 | let mut ops = Vec::new(); 130 | method_or_call(handler, |method, handler_args| { 131 | let method = method.to_string(); 132 | let method = match method.as_str() { 133 | "get" => HttpMethod::Get, 134 | "put" => HttpMethod::Put, 135 | "post" => HttpMethod::Post, 136 | "delete" => HttpMethod::Delete, 137 | "options" => HttpMethod::Options, 138 | "head" => HttpMethod::Head, 139 | "patch" => HttpMethod::Patch, 140 | "trace" => HttpMethod::Trace, 141 | _ => return Err(syn::Error::new(method.span(), "unknown http method")), 142 | }; 143 | 144 | let handler_path = handler_args 145 | .first() 146 | .ok_or_else(|| syn::Error::new(handler_args.span(), "expected one argument"))?; 147 | let handler_path = expr_path(handler_path); 148 | 149 | ops.push(Operation { 150 | method, 151 | path: handler_path.cloned(), 152 | }); 153 | 154 | Ok(()) 155 | })?; 156 | ops.reverse(); 157 | 158 | let axum_path = axum_path(&path); 159 | 160 | routes 161 | .paths 162 | .entry(axum_path) 163 | .or_default() 164 | .extend(ops.into_iter()); 165 | 166 | Ok(()) 167 | })?; 168 | 169 | Ok((call, routes)) 170 | } 171 | 172 | const REGEX_AXUM_PATH: Lazy = Lazy::new(|| Regex::new(r#":(\w+)"#).unwrap()); 173 | 174 | fn axum_path(path: &str) -> String { 175 | REGEX_AXUM_PATH 176 | .replace_all(path, |caps: &Captures| format!("{{{}}}", &caps[1])) 177 | .into_owned() 178 | } 179 | 180 | fn method_or_call( 181 | expr: &syn::Expr, 182 | mut f: impl FnMut(&syn::Ident, &Punctuated) -> syn::Result<()>, 183 | ) -> syn::Result<()> { 184 | match expr { 185 | syn::Expr::Call(call) => { 186 | let path = expr_path(&*call.func) 187 | .ok_or_else(|| syn::Error::new(call.func.span(), "expected call to function"))?; 188 | let ident = &path 189 | .segments 190 | .last() 191 | .ok_or_else(|| syn::Error::new(path.segments.span(), "empty path"))? 192 | .ident; 193 | f(ident, &call.args)?; 194 | 195 | Ok(()) 196 | } 197 | syn::Expr::MethodCall(method_call) => { 198 | f(&method_call.method, &method_call.args)?; 199 | method_or_call(&*method_call.receiver, f)?; 200 | 201 | Ok(()) 202 | } 203 | _ => Err(syn::Error::new( 204 | expr.span(), 205 | "expected method or function call", 206 | )), 207 | } 208 | } 209 | 210 | fn expr_string_lit(expr: &syn::Expr) -> Option { 211 | match expr { 212 | syn::Expr::Lit(syn::ExprLit { 213 | lit: syn::Lit::Str(lit), 214 | .. 215 | }) => Some(lit.value()), 216 | _ => None, 217 | } 218 | } 219 | 220 | fn expr_path(expr: &syn::Expr) -> Option<&syn::Path> { 221 | match expr { 222 | syn::Expr::Path(path) => Some(&path.path), 223 | _ => None, 224 | } 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::{axum_path, parse_routes, HttpMethod, Operation}; 230 | use pretty_assertions::assert_eq; 231 | use quote::quote; 232 | 233 | #[test] 234 | fn call_single() { 235 | let tokens = quote! { route("/path", get(get_handler)) }; 236 | let (_, routes) = parse_routes(tokens).unwrap(); 237 | 238 | assert_eq!( 239 | routes.paths.into_iter().collect::>(), 240 | vec![( 241 | "/path".to_string(), 242 | vec![Operation { 243 | method: HttpMethod::Get, 244 | path: Some(syn::parse_quote!(get_handler)), 245 | },] 246 | )] 247 | ); 248 | } 249 | #[test] 250 | fn call_multiple_ops() { 251 | let tokens = 252 | quote! { route("/path", get(get_handler).post(post_handler).patch(patch_handler)) }; 253 | let (_, routes) = parse_routes(tokens).unwrap(); 254 | 255 | assert_eq!( 256 | routes.paths.into_iter().collect::>(), 257 | vec![( 258 | "/path".to_string(), 259 | vec![ 260 | Operation { 261 | method: HttpMethod::Get, 262 | path: Some(syn::parse_quote!(get_handler)), 263 | }, 264 | Operation { 265 | method: HttpMethod::Post, 266 | path: Some(syn::parse_quote!(post_handler)), 267 | }, 268 | Operation { 269 | method: HttpMethod::Patch, 270 | path: Some(syn::parse_quote!(patch_handler)), 271 | }, 272 | ] 273 | ),] 274 | ); 275 | } 276 | 277 | #[test] 278 | fn full() { 279 | let tokens = quote! { route("/path", get(get_handler).post(post_handler).patch(patch_handler)).route("/path2", get(get_handler_2)) }; 280 | let (_, routes) = parse_routes(tokens).unwrap(); 281 | 282 | assert_eq!( 283 | routes.paths.into_iter().collect::>(), 284 | vec![ 285 | ( 286 | "/path".to_string(), 287 | vec![ 288 | Operation { 289 | method: HttpMethod::Get, 290 | path: Some(syn::parse_quote!(get_handler)), 291 | }, 292 | Operation { 293 | method: HttpMethod::Post, 294 | path: Some(syn::parse_quote!(post_handler)), 295 | }, 296 | Operation { 297 | method: HttpMethod::Patch, 298 | path: Some(syn::parse_quote!(patch_handler)), 299 | }, 300 | ] 301 | ), 302 | ( 303 | "/path2".to_string(), 304 | vec![Operation { 305 | method: HttpMethod::Get, 306 | path: Some(syn::parse_quote!(get_handler_2)), 307 | },] 308 | ), 309 | ] 310 | ); 311 | } 312 | 313 | #[test] 314 | fn closure() { 315 | let tokens = quote! { route("/path", get(|| async {})) }; 316 | let (_, routes) = parse_routes(tokens).unwrap(); 317 | 318 | assert_eq!( 319 | routes.paths.into_iter().collect::>(), 320 | vec![( 321 | "/path".to_string(), 322 | vec![Operation { 323 | method: HttpMethod::Get, 324 | path: None 325 | }] 326 | )] 327 | ); 328 | } 329 | 330 | #[test] 331 | fn axum_path_regular() { 332 | assert_eq!(axum_path("/path/foo"), "/path/foo"); 333 | } 334 | 335 | #[test] 336 | fn axum_path_params() { 337 | assert_eq!(axum_path("/path/:id/:bla"), "/path/{id}/{bla}"); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /examples/petstore.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use axum::prelude::*; 3 | use std::net::SocketAddr; 4 | 5 | use axum_openapi::prelude::*; 6 | use axum_openapi::{openapi_json_endpoint, openapi_yaml_endpoint}; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let app = route("/pets", get(find_pets).post(add_pet)) 11 | .route("/pets/:id", get(find_pet_by_id).delete(delete_pet)); 12 | let openapi = app.openapi(); 13 | 14 | let app = app 15 | .route("/openapi.yaml", openapi_yaml_endpoint(openapi.clone())) 16 | .route("/openapi.json", openapi_json_endpoint(openapi)); 17 | 18 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 19 | hyper::server::Server::bind(&addr) 20 | .serve(app.into_make_service()) 21 | .await 22 | .unwrap(); 23 | } 24 | 25 | mod model { 26 | use axum_openapi::DescribeSchema; 27 | 28 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 29 | pub struct Pet { 30 | #[serde(flatten)] 31 | new_pet: NewPet, 32 | #[serde(flatten)] 33 | pet_extra: PetExtra, 34 | } 35 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 36 | pub struct PetExtra { 37 | id: i64, 38 | } 39 | 40 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 41 | pub struct NewPet { 42 | name: String, 43 | tag: Option, 44 | } 45 | 46 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 47 | pub struct Error { 48 | code: i32, 49 | message: String, 50 | } 51 | } 52 | 53 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 54 | pub struct FindPetsQueryParams { 55 | tags: Option>, 56 | limit: Option, 57 | } 58 | 59 | /// Returns all pets from the system that the user has access to 60 | async fn find_pets(query_params: Option>) { 61 | println!("find_pets called"); 62 | println!("Query params: {:?}", query_params); 63 | } 64 | 65 | #[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)] 66 | pub struct AddPetRequestBody { 67 | name: String, 68 | tag: Option, 69 | } 70 | 71 | /// Creates a new pet in the store. Duplicates are allowed. 72 | async fn add_pet(request_body: axum::extract::Json) { 73 | println!("add_pet called"); 74 | println!("Request body: {:?}", request_body); 75 | } 76 | 77 | /// Returns a user based on a single ID, if the user does not have access to the pet 78 | async fn find_pet_by_id(path_params: axum::extract::UrlParams<(i64,)>) { 79 | let (id,) = path_params.0; 80 | println!("find_pet_by_id called"); 81 | println!("id = {}", id); 82 | } 83 | 84 | /// deletes a single pet based on the ID supplied 85 | async fn delete_pet(path_params: axum::extract::UrlParams<(i64,)>) { 86 | let (id,) = path_params.0; 87 | println!("delete_pet called"); 88 | println!("id = {}", id); 89 | } 90 | -------------------------------------------------------------------------------- /src/describe_impl.rs: -------------------------------------------------------------------------------- 1 | use openapiv3::*; 2 | 3 | use crate::openapi_traits::DescribeSchema; 4 | use crate::utils; 5 | 6 | impl DescribeSchema for i32 { 7 | fn describe_schema() -> Schema { 8 | utils::ty_schema(Type::Integer(IntegerType { 9 | format: VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), 10 | ..Default::default() 11 | })) 12 | } 13 | } 14 | impl DescribeSchema for i64 { 15 | fn describe_schema() -> Schema { 16 | utils::ty_schema(Type::Integer(IntegerType { 17 | format: VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), 18 | ..Default::default() 19 | })) 20 | } 21 | } 22 | impl DescribeSchema for f32 { 23 | fn describe_schema() -> Schema { 24 | utils::ty_schema(Type::Number(NumberType { 25 | format: VariantOrUnknownOrEmpty::Item(NumberFormat::Float), 26 | ..Default::default() 27 | })) 28 | } 29 | } 30 | impl DescribeSchema for f64 { 31 | fn describe_schema() -> Schema { 32 | utils::ty_schema(Type::Number(NumberType { 33 | format: VariantOrUnknownOrEmpty::Item(NumberFormat::Double), 34 | ..Default::default() 35 | })) 36 | } 37 | } 38 | impl DescribeSchema for bool { 39 | fn describe_schema() -> Schema { 40 | utils::ty_schema(Type::Boolean {}) 41 | } 42 | } 43 | impl DescribeSchema for String { 44 | fn describe_schema() -> Schema { 45 | utils::ty_schema(Type::String(StringType::default())) 46 | } 47 | } 48 | impl DescribeSchema for &str { 49 | fn describe_schema() -> Schema { 50 | utils::ty_schema(Type::String(StringType::default())) 51 | } 52 | } 53 | impl DescribeSchema for Vec { 54 | fn describe_schema() -> Schema { 55 | utils::ty_schema(Type::Array(ArrayType { 56 | items: ReferenceOr::Item(Box::new(T::describe_schema())), 57 | min_items: None, 58 | max_items: None, 59 | unique_items: false, 60 | })) 61 | } 62 | } 63 | impl DescribeSchema for [T; N] { 64 | fn describe_schema() -> Schema { 65 | utils::ty_schema(Type::Array(ArrayType { 66 | items: ReferenceOr::Item(Box::new(T::describe_schema())), 67 | min_items: Some(N), 68 | max_items: Some(N), 69 | unique_items: false, 70 | })) 71 | } 72 | } 73 | 74 | impl DescribeSchema for Option { 75 | fn describe_schema() -> Schema { 76 | let mut schema = T::describe_schema(); 77 | schema.schema_data.nullable = true; 78 | schema 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/global_collect.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{__macro, utils}; 4 | use once_cell::sync::Lazy; 5 | use openapiv3::*; 6 | 7 | pub async fn api_yaml() -> hyper::Response { 8 | utils::yaml_response(&*OPENAPI) 9 | } 10 | pub async fn api_json() -> axum::response::Json { 11 | axum::response::Json(OPENAPI.clone()) 12 | } 13 | 14 | pub const OPENAPI: Lazy = Lazy::new(openapi); 15 | 16 | fn openapi() -> openapiv3::OpenAPI { 17 | let handler_ops: HashMap<&str, &Operation> = inventory::iter::<__macro::OperationDescription>() 18 | .filter_map(|op| { 19 | let op_id = op.operation.operation_id.as_deref()?; 20 | Some((op_id, &op.operation)) 21 | }) 22 | .collect(); 23 | 24 | openapiv3::OpenAPI { 25 | openapi: "3.0.3".to_string(), 26 | paths: inventory::iter::<__macro::PathDescription>() 27 | .map(|path| { 28 | let mut item = path.path_item.clone(); 29 | patch_operations(&mut item, &handler_ops, &path.path); 30 | 31 | (path.path.clone(), ReferenceOr::Item(item)) 32 | }) 33 | .collect(), 34 | components: Some(openapiv3::Components { 35 | schemas: inventory::iter::<__macro::SchemaDescription>() 36 | .map(|desc| { 37 | let reference = openapiv3::ReferenceOr::Item(desc.schema.clone()); 38 | (desc.name.clone(), reference) 39 | }) 40 | .collect(), 41 | ..Default::default() 42 | }), 43 | ..Default::default() 44 | } 45 | } 46 | 47 | fn patch_operations(path_item: &mut PathItem, handler_ops: &HashMap<&str, &Operation>, path: &str) { 48 | let path_params: Vec<_> = path 49 | .split('/') 50 | .filter_map(|component| { 51 | if component.starts_with('{') && component.ends_with('}') { 52 | Some(&component[1..component.len() - 1]) 53 | } else { 54 | None 55 | } 56 | }) 57 | .collect(); 58 | 59 | let ops = std::array::IntoIter::new([ 60 | path_item.get.as_mut(), 61 | path_item.put.as_mut(), 62 | path_item.post.as_mut(), 63 | path_item.delete.as_mut(), 64 | path_item.options.as_mut(), 65 | path_item.head.as_mut(), 66 | path_item.patch.as_mut(), 67 | path_item.trace.as_mut(), 68 | ]); 69 | for (handler_op, op) in ops 70 | .into_iter() 71 | .flatten() 72 | .filter_map(|op| Some((*handler_ops.get(op.operation_id.as_deref()?)?, op))) 73 | { 74 | *op = handler_op.clone(); 75 | 76 | op.parameters 77 | .iter_mut() 78 | .filter_map(|param| match param { 79 | ReferenceOr::Item(Parameter::Path { parameter_data, .. }) => Some(parameter_data), 80 | _ => None, 81 | }) 82 | .for_each(|param| { 83 | if let Some(i) = param.name.strip_prefix("__parameter") { 84 | if let Ok(i) = i.parse::() { 85 | param.name = path_params[i].to_string(); 86 | } 87 | } 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | mod utils; 3 | 4 | mod describe_impl; 5 | mod openapi_impl; 6 | mod operation_impl; 7 | 8 | pub mod openapi_adapters; 9 | pub mod openapi_traits; 10 | 11 | pub use axum_openapi_derive::DescribeSchema; 12 | pub use openapi_traits::DescribeSchema; 13 | 14 | #[cfg(feature = "macro_based")] 15 | mod global_collect; 16 | 17 | #[cfg(feature = "macro_based")] 18 | pub use axum_openapi_derive::handler; 19 | #[cfg(feature = "macro_based")] 20 | pub use axum_openapi_derive::routes; 21 | 22 | pub mod prelude { 23 | pub use crate::openapi_adapters::HandlerExt; 24 | pub use crate::openapi_traits::{DescribeSchema, OpenapiApp}; 25 | pub use axum_openapi_derive::DescribeSchema; 26 | } 27 | 28 | #[cfg(feature = "macro_based")] 29 | pub use global_collect::{api_json, api_yaml, OPENAPI}; 30 | 31 | use axum::{ 32 | handler::{IntoService, OnMethod}, 33 | prelude::*, 34 | routing::EmptyRouter, 35 | }; 36 | use openapiv3::OpenAPI; 37 | 38 | /// [axum] handler function responding with the provided [OpenAPI] yaml file 39 | pub fn openapi_yaml_endpoint( 40 | api: OpenAPI, 41 | ) -> OnMethod + Clone, B, ()>, EmptyRouter> { 42 | get(|| async move { utils::yaml_response(&api) }) 43 | } 44 | 45 | /// [axum] handler function responding with the provided [OpenAPI] json file 46 | pub fn openapi_json_endpoint( 47 | api: OpenAPI, 48 | ) -> OnMethod + Clone, B, ()>, EmptyRouter> { 49 | get(|| async { axum::response::Json(api) }) 50 | } 51 | 52 | #[doc(hidden)] 53 | pub mod __macro { 54 | pub use openapiv3; 55 | 56 | #[cfg(feature = "macro_based")] 57 | pub use inventory; 58 | 59 | #[cfg(feature = "macro_based")] 60 | pub struct SchemaDescription { 61 | pub schema: openapiv3::Schema, 62 | pub name: String, 63 | } 64 | 65 | #[cfg(feature = "macro_based")] 66 | pub struct PathDescription { 67 | pub path: String, 68 | pub path_item: openapiv3::PathItem, 69 | } 70 | 71 | #[cfg(feature = "macro_based")] 72 | pub struct OperationDescription { 73 | pub operation: openapiv3::Operation, 74 | } 75 | 76 | #[cfg(feature = "macro_based")] 77 | inventory::collect!(SchemaDescription); 78 | #[cfg(feature = "macro_based")] 79 | inventory::collect!(PathDescription); 80 | #[cfg(feature = "macro_based")] 81 | inventory::collect!(OperationDescription); 82 | } 83 | -------------------------------------------------------------------------------- /src/openapi_adapters.rs: -------------------------------------------------------------------------------- 1 | use openapiv3::*; 2 | 3 | use std::future::Future; 4 | use std::marker::PhantomData; 5 | use std::pin::Pin; 6 | 7 | use axum::body::BoxBody; 8 | use axum::prelude::*; 9 | use hyper::Request; 10 | 11 | use crate::openapi_traits::OperationHandler; 12 | 13 | type BodyFuture<'a> = Pin> + Send + 'a>>; 14 | 15 | pub trait HandlerExt: Handler { 16 | fn ignore_openapi(self) -> IgnoreOpenapiHandler; 17 | fn with_openapi(self, supplier: F) -> WithOpenapiHandler 18 | where 19 | F: Fn() -> Operation + Clone; 20 | } 21 | impl, B, In> HandlerExt for H { 22 | fn ignore_openapi(self) -> IgnoreOpenapiHandler { 23 | IgnoreOpenapiHandler(self, PhantomData) 24 | } 25 | 26 | fn with_openapi(self, supplier: F) -> WithOpenapiHandler 27 | where 28 | F: Fn() -> Operation + Clone, 29 | { 30 | WithOpenapiHandler::new(self, supplier) 31 | } 32 | } 33 | 34 | pub struct IgnoreOpenapiHandler, B, In>(H, PhantomData (B, In)>); 35 | impl + Clone, B, In> IgnoreOpenapiHandler { 36 | pub fn new(handler: H) -> Self { 37 | Self(handler, PhantomData) 38 | } 39 | pub fn service(&self) -> &H { 40 | &self.0 41 | } 42 | } 43 | 44 | impl + Clone, B, In> Clone for IgnoreOpenapiHandler { 45 | fn clone(&self) -> Self { 46 | Self(self.0.clone(), PhantomData) 47 | } 48 | } 49 | impl + Sized, B, In> Handler for IgnoreOpenapiHandler { 50 | type Sealed = axum::handler::sealed::Hidden; 51 | 52 | fn call<'a>(self, req: Request) -> BodyFuture<'a> 53 | where 54 | Self: 'a, 55 | { 56 | self.0.call(req) 57 | } 58 | } 59 | impl, B, In> OperationHandler<()> for IgnoreOpenapiHandler { 60 | fn modify_op(&self, _: &mut OpenAPI, _: &mut Operation) {} 61 | } 62 | 63 | pub struct WithOpenapiHandler(H, F, PhantomData (B, In)>) 64 | where 65 | H: Handler, 66 | F: Fn() -> Operation + Clone; 67 | 68 | impl WithOpenapiHandler 69 | where 70 | H: Handler, 71 | F: Fn() -> Operation + Clone, 72 | { 73 | pub fn new(handler: H, supplier: F) -> Self { 74 | Self(handler, supplier, PhantomData) 75 | } 76 | pub fn service(&self) -> &H { 77 | &self.0 78 | } 79 | } 80 | impl Clone for WithOpenapiHandler 81 | where 82 | H: Handler + Clone, 83 | F: Fn() -> Operation + Clone, 84 | { 85 | fn clone(&self) -> Self { 86 | Self(self.0.clone(), self.1.clone(), PhantomData) 87 | } 88 | } 89 | impl + Sized, B, In, F> Handler for WithOpenapiHandler 90 | where 91 | F: Fn() -> Operation + Clone, 92 | { 93 | type Sealed = axum::handler::sealed::Hidden; 94 | 95 | fn call<'a>(self, req: Request) -> BodyFuture<'a> 96 | where 97 | Self: 'a, 98 | { 99 | self.0.call(req) 100 | } 101 | } 102 | impl, B, In, F> OperationHandler<()> for WithOpenapiHandler 103 | where 104 | F: Fn() -> Operation + Clone, 105 | { 106 | fn modify_op(&self, _: &mut OpenAPI, op: &mut Operation) { 107 | *op = (self.1)(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/openapi_impl.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::openapi_traits::{ 4 | OpenapiApp, OperationAtPath, OperationHandler, OperationParameter, OperationResult, 5 | }; 6 | use openapiv3::*; 7 | 8 | use axum::handler::IntoService; 9 | use axum::routing::EmptyRouter; 10 | use axum::routing::MethodFilter; 11 | use axum::routing::Route; 12 | 13 | macro_rules! impl_function_operation { 14 | ( $($param:ident),* ) => { 15 | 16 | impl OperationHandler<(Ret, $($param,)*)> for F 17 | where 18 | F: Fn($($param,)*) -> Fut, 19 | Fut: std::future::Future, 20 | Ret: OperationResult 21 | { 22 | #[allow(unused)] 23 | fn modify_op(&self, openapi: &mut OpenAPI, op: &mut Operation) { 24 | $(<$param as OperationParameter>::modify_op(openapi, op, true);)* 25 | } 26 | } 27 | }; 28 | } 29 | axum_openapi_derive::all_tuples!(impl_function_operation, 0, 16, P); 30 | 31 | impl OperationAtPath<(Params, FallbackParams)> 32 | for axum::handler::OnMethod, Fallback> 33 | where 34 | H: OperationHandler, 35 | Fallback: OperationAtPath, 36 | { 37 | fn modify_path_item(&self, openapi: &mut OpenAPI, path_item: &mut PathItem) { 38 | Fallback::modify_path_item(&self.fallback, openapi, path_item); 39 | match self.method { 40 | MethodFilter::Get => H::modify_op( 41 | &self.svc.handler, 42 | openapi, 43 | path_item.get.get_or_insert_with(Default::default), 44 | ), 45 | MethodFilter::Post => H::modify_op( 46 | &self.svc.handler, 47 | openapi, 48 | path_item.post.get_or_insert_with(Default::default), 49 | ), 50 | MethodFilter::Patch => H::modify_op( 51 | &self.svc.handler, 52 | openapi, 53 | path_item.patch.get_or_insert_with(Default::default), 54 | ), 55 | MethodFilter::Delete => H::modify_op( 56 | &self.svc.handler, 57 | openapi, 58 | path_item.delete.get_or_insert_with(Default::default), 59 | ), 60 | MethodFilter::Head => H::modify_op( 61 | &self.svc.handler, 62 | openapi, 63 | path_item.head.get_or_insert_with(Default::default), 64 | ), 65 | MethodFilter::Options => H::modify_op( 66 | &self.svc.handler, 67 | openapi, 68 | path_item.options.get_or_insert_with(Default::default), 69 | ), 70 | MethodFilter::Put => H::modify_op( 71 | &self.svc.handler, 72 | openapi, 73 | path_item.put.get_or_insert_with(Default::default), 74 | ), 75 | MethodFilter::Trace => H::modify_op( 76 | &self.svc.handler, 77 | openapi, 78 | path_item.trace.get_or_insert_with(Default::default), 79 | ), 80 | MethodFilter::Any | MethodFilter::Connect => todo!(), 81 | } 82 | } 83 | } 84 | impl OperationAtPath<()> for EmptyRouter { 85 | fn modify_path_item(&self, _: &mut OpenAPI, _: &mut PathItem) {} 86 | } 87 | 88 | impl OpenapiApp<(ServiceParams, FallbackParams)> 89 | for Route 90 | where 91 | Service: OperationAtPath, 92 | Fallback: OpenapiApp, 93 | { 94 | fn modify_openapi(&self, api: &mut OpenAPI) { 95 | let mut path_item = PathItem::default(); 96 | OpenapiApp::modify_openapi(&self.fallback, api); 97 | OperationAtPath::modify_path_item(&self.svc, api, &mut path_item); 98 | let path = axum_path_to_openapi(&self.path); 99 | api.paths.insert(path, ReferenceOr::Item(path_item)); 100 | } 101 | } 102 | impl OpenapiApp<()> for EmptyRouter { 103 | fn modify_openapi(&self, _: &mut OpenAPI) {} 104 | } 105 | 106 | fn axum_path_to_openapi(path: &str) -> String { 107 | let mut string = String::with_capacity(path.len()); 108 | let iter = path 109 | .split('/') 110 | .map(|segment| match segment.strip_prefix(':') { 111 | None => Cow::Borrowed(segment), 112 | Some(name) => Cow::Owned(format!("{{{}}}", name)), 113 | }); 114 | for segment in iter { 115 | string.push_str(&*segment); 116 | string.push('/'); 117 | } 118 | 119 | string.truncate(string.len() - 1); 120 | string 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::axum_path_to_openapi; 126 | 127 | #[test] 128 | fn axum_path() { 129 | assert_eq!(axum_path_to_openapi("/pets"), "/pets"); 130 | assert_eq!(axum_path_to_openapi("/pets/:id"), "/pets/{id}"); 131 | assert_eq!(axum_path_to_openapi("/pets/:id/"), "/pets/{id}/"); 132 | assert_eq!(axum_path_to_openapi("pets/:id/"), "pets/{id}/"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/openapi_traits.rs: -------------------------------------------------------------------------------- 1 | use openapiv3::*; 2 | 3 | /// Trait which describes a rust type as an [`openapiv3::Schema`] 4 | pub trait DescribeSchema { 5 | fn describe_schema() -> Schema; 6 | 7 | /// If this returns a string, then the schema will be written to the `components/schemas` section instead of inlined at its use. 8 | fn ref_name() -> Option { 9 | None 10 | } 11 | 12 | fn reference_or_schema() -> ReferenceOr { 13 | match Self::ref_name() { 14 | Some(ref_name) => ReferenceOr::Reference { 15 | reference: format!("#/components/schemas/{}", ref_name), 16 | }, 17 | None => ReferenceOr::Item(Self::describe_schema()), 18 | } 19 | } 20 | } 21 | 22 | /// Describes an [axum] app as [`openapiv3::OpenAPI`] 23 | /// ```rust,no_run 24 | /// use axum::prelude::*; 25 | /// use axum_openapi::prelude::*; 26 | /// # async fn index() {} 27 | /// # async fn handler() {} 28 | /// 29 | /// let app = route("/", get(index)) 30 | /// .route("/", get(handler)); 31 | /// # hyper::server::Server::bind(todo!()).serve(app.into_make_service()); 32 | /// 33 | /// println!("{:?}", app.openapi()); 34 | /// ``` 35 | pub trait OpenapiApp { 36 | fn modify_openapi(&self, api: &mut OpenAPI); 37 | 38 | fn openapi(&self) -> OpenAPI { 39 | let mut openapi = OpenAPI::default(); 40 | self.modify_openapi(&mut openapi); 41 | 42 | fix_path_params(&mut openapi); 43 | 44 | openapi 45 | } 46 | } 47 | 48 | /// Implemented for [`axum::handler::get/post/...`](axum::handler) 49 | pub trait OperationAtPath { 50 | fn modify_path_item(&self, openapi: &mut OpenAPI, path_item: &mut PathItem); 51 | } 52 | 53 | /// Describes an [`axum::handler::Handler`] as a [`openapiv3::Operation`] 54 | pub trait OperationHandler { 55 | fn modify_op(&self, openapi: &mut OpenAPI, operation: &mut Operation); 56 | } 57 | 58 | /// Implemeted for most types in [`axum::extract`], i.e. parameters to handler functions. 59 | pub trait OperationParameter { 60 | fn modify_op(openapi: &mut OpenAPI, operation: &mut Operation, required: bool); 61 | } 62 | /// Describes the return value of a handler function for an [`openapiv3::Operation`] 63 | pub trait OperationResult { 64 | fn modify_op(openapi: &mut OpenAPI, operation: &mut Operation); 65 | } 66 | 67 | fn fix_path_params(openapi: &mut OpenAPI) { 68 | openapi.paths.iter_mut().for_each(|(path, val)| { 69 | let val = match val { 70 | ReferenceOr::Reference { .. } => return, 71 | ReferenceOr::Item(item) => item, 72 | }; 73 | 74 | patch_operations(val, path); 75 | }); 76 | } 77 | 78 | fn patch_operations(path_item: &mut PathItem, path: &str) { 79 | let path_params: Vec<_> = path 80 | .split('/') 81 | .filter_map(|component| { 82 | component 83 | .strip_prefix('{') 84 | .and_then(|component| component.strip_suffix('}')) 85 | }) 86 | .collect(); 87 | 88 | let ops = std::array::IntoIter::new([ 89 | path_item.get.as_mut(), 90 | path_item.put.as_mut(), 91 | path_item.post.as_mut(), 92 | path_item.delete.as_mut(), 93 | path_item.options.as_mut(), 94 | path_item.head.as_mut(), 95 | path_item.patch.as_mut(), 96 | path_item.trace.as_mut(), 97 | ]) 98 | .into_iter() 99 | .flatten(); 100 | for op in ops { 101 | op.parameters 102 | .iter_mut() 103 | .filter_map(|param| match param { 104 | ReferenceOr::Item(Parameter::Path { parameter_data, .. }) => Some(parameter_data), 105 | _ => None, 106 | }) 107 | .for_each(|param| { 108 | if let Some(i) = param.name.strip_prefix("__parameter") { 109 | if let Ok(i) = i.parse::() { 110 | param.name = path_params[i].to_string(); 111 | } 112 | } 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/operation_impl.rs: -------------------------------------------------------------------------------- 1 | use axum_openapi_derive::all_tuples; 2 | use openapiv3::*; 3 | 4 | use crate::openapi_traits::{DescribeSchema, OperationParameter, OperationResult}; 5 | 6 | impl OperationParameter for Option { 7 | fn modify_op(openapi: &mut OpenAPI, op: &mut Operation, _: bool) { 8 | T::modify_op(openapi, op, false); 9 | } 10 | } 11 | 12 | impl OperationParameter for axum::extract::Json { 13 | fn modify_op(openapi: &mut OpenAPI, op: &mut Operation, required: bool) { 14 | if op.request_body.is_some() { 15 | todo!(); 16 | } 17 | 18 | if let Some(ref_name) = T::ref_name() { 19 | openapi 20 | .components 21 | .get_or_insert_with(Default::default) 22 | .schemas 23 | .insert(ref_name, ReferenceOr::Item(T::describe_schema())); 24 | } 25 | 26 | op.request_body = Some(ReferenceOr::Item(RequestBody { 27 | description: None, 28 | content: std::array::IntoIter::new([( 29 | "application/json".to_string(), 30 | MediaType { 31 | schema: Some(T::reference_or_schema()), 32 | example: None, 33 | examples: Default::default(), 34 | encoding: Default::default(), 35 | }, 36 | )]) 37 | .collect(), 38 | required, 39 | extensions: Default::default(), 40 | })); 41 | } 42 | } 43 | 44 | macro_rules! impl_url_params { 45 | ( $($param:ident),* ) => { 46 | 47 | #[allow(deprecated)] 48 | impl<$($param: DescribeSchema,)*> OperationParameter for axum::extract::UrlParams<($($param,)*)> { 49 | fn modify_op(_: &mut OpenAPI,op: &mut Operation, _: bool) { 50 | let parameters = vec![$(<$param as DescribeSchema>::reference_or_schema(),)*]; 51 | url_params(op, parameters) 52 | } 53 | } 54 | }; 55 | } 56 | 57 | fn url_params(op: &mut Operation, parameters: Vec>) { 58 | for (i, schema) in parameters.into_iter().enumerate() { 59 | op.parameters.push(ReferenceOr::Item(Parameter::Path { 60 | parameter_data: ParameterData { 61 | name: format!("__parameter{}", i), 62 | description: None, 63 | required: true, 64 | deprecated: None, 65 | format: ParameterSchemaOrContent::Schema(schema), 66 | example: None, 67 | examples: Default::default(), 68 | explode: None, 69 | extensions: Default::default(), 70 | }, 71 | style: PathStyle::Simple, 72 | })) 73 | } 74 | } 75 | 76 | all_tuples!(impl_url_params, 1, 6, T); 77 | 78 | impl OperationParameter for axum::extract::Query { 79 | fn modify_op(_: &mut OpenAPI, op: &mut Operation, required: bool) { 80 | let schema = T::describe_schema(); 81 | let obj = match schema.schema_kind { 82 | SchemaKind::Type(Type::Object(obj)) => obj, 83 | _ => panic!("unsupported schema for query parameters"), 84 | }; 85 | 86 | for (name, schema) in &obj.properties { 87 | op.parameters.push(ReferenceOr::Item(Parameter::Query { 88 | parameter_data: ParameterData { 89 | name: name.clone(), 90 | description: None, 91 | required, 92 | deprecated: None, 93 | format: ParameterSchemaOrContent::Schema(match schema.clone() { 94 | ReferenceOr::Reference { reference } => { 95 | ReferenceOr::Reference { reference } 96 | } 97 | ReferenceOr::Item(item) => ReferenceOr::Item(*item), 98 | }), 99 | example: None, 100 | examples: Default::default(), 101 | explode: None, 102 | extensions: Default::default(), 103 | }, 104 | allow_reserved: false, 105 | style: QueryStyle::default(), 106 | allow_empty_value: None, 107 | })) 108 | } 109 | } 110 | } 111 | 112 | impl OperationResult for () { 113 | fn modify_op(_: &mut OpenAPI, operation: &mut Operation) { 114 | operation.responses.default = Some(ReferenceOr::Item(Response { 115 | description: "Default OK response".to_string(), 116 | headers: Default::default(), 117 | content: Default::default(), 118 | links: Default::default(), 119 | extensions: Default::default(), 120 | })); 121 | } 122 | } 123 | 124 | impl OperationResult for hyper::Response { 125 | fn modify_op(_: &mut OpenAPI, _: &mut Operation) {} 126 | } 127 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use openapiv3::*; 2 | use serde::Serialize; 3 | 4 | pub fn ty_schema(ty: Type) -> Schema { 5 | Schema { 6 | schema_data: SchemaData { 7 | nullable: false, 8 | ..Default::default() 9 | }, 10 | schema_kind: SchemaKind::Type(ty), 11 | } 12 | } 13 | 14 | pub fn yaml_response(body: &T) -> hyper::Response { 15 | let bytes = match serde_yaml::to_vec(body) { 16 | Ok(res) => res, 17 | Err(err) => { 18 | return hyper::Response::builder() 19 | .status(hyper::StatusCode::INTERNAL_SERVER_ERROR) 20 | .header(hyper::header::CONTENT_TYPE, "text/plain") 21 | .body(hyper::Body::from(err.to_string())) 22 | .unwrap(); 23 | } 24 | }; 25 | 26 | let mut res = hyper::Response::new(hyper::Body::from(bytes)); 27 | res.headers_mut().insert( 28 | hyper::header::CONTENT_TYPE, 29 | hyper::header::HeaderValue::from_static("text/x-yaml"), 30 | ); 31 | res 32 | } 33 | --------------------------------------------------------------------------------