Compare commits
No commits in common. "add-nixpkg-and-hydra" and "main" have entirely different histories.
add-nixpkg
...
main
22 changed files with 20 additions and 3253 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
target
|
||||
Cargo.lock
|
||||
.vscode
|
||||
.direnv
|
||||
|
|
1508
Cargo.lock
generated
1508
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,15 @@
|
|||
[workspace]
|
||||
members = ["axum-controller", "axum-controller-macros"]
|
||||
resolver = "3"
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Tristan Druyen <ek36g2vcc@mozmail.com>"]
|
||||
categories = ["web-programming"]
|
||||
description = "Helper macro's for better readability of axum handlers"
|
||||
description = "A controller & route macro for axum"
|
||||
edition = "2024"
|
||||
homepage = "https://git.vlt81.de/vault81/axum-controller"
|
||||
keywords = ["axum", "controller", "macro", "routing"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
repository = "https://git.vlt81.de/vault81/axum-controller"
|
||||
version = "0.2.1"
|
||||
version = "0.1.1"
|
||||
|
|
|
@ -15,7 +15,7 @@ version.workspace = true
|
|||
prettyplease = "0.2"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["extra-traits", "parsing", "printing"] }
|
||||
syn = { version = "2", features = ["parsing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { version = "0.8", features = [] }
|
||||
|
|
|
@ -130,9 +130,8 @@ pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
.into_iter()
|
||||
.map(move |route| {
|
||||
quote! {
|
||||
.typed_route(#struct_name :: #route)
|
||||
|
||||
}
|
||||
.typed_route(#struct_name :: #route)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -140,23 +139,6 @@ pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
.nest(#route, __nested_router)
|
||||
};
|
||||
|
||||
let nested_router_qoute = quote! {
|
||||
axum::Router::new()
|
||||
#nesting_call
|
||||
};
|
||||
let unnested_router_quote = quote! {
|
||||
__nested_router
|
||||
};
|
||||
let maybe_nesting_call = if let syn::Expr::Lit(lit) = route {
|
||||
if lit.eq(&syn::parse_quote!("/")) {
|
||||
unnested_router_quote
|
||||
} else {
|
||||
nested_router_qoute
|
||||
}
|
||||
} else {
|
||||
nested_router_qoute
|
||||
};
|
||||
|
||||
let middleware_calls = args
|
||||
.middlewares
|
||||
.clone()
|
||||
|
@ -169,14 +151,15 @@ pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
// one where it's #state
|
||||
let from_controller_into_router_impl = quote! {
|
||||
impl #struct_name {
|
||||
fn into_router(state: #state) -> axum::Router<#state> {
|
||||
fn into_router(&self, state: #state) -> axum::Router<#state> {
|
||||
let __nested_router = axum::Router::new()
|
||||
#(#route_calls)*
|
||||
#(#middleware_calls)*
|
||||
.with_state(state)
|
||||
;
|
||||
|
||||
#maybe_nesting_call
|
||||
axum::Router::new()
|
||||
#nesting_call
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -12,12 +12,11 @@ repository.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum-controller-macros = { path = "../axum-controller-macros", version = "0.2.1" }
|
||||
axum-typed-routing = { path = "../vendor/axum-typed-routing", version = "0.2.0"}
|
||||
axum-controller-macros = { path = "../axum-controller-macros", version = "0.1.1" }
|
||||
axum-typed-routing = { git = "https://github.com/jvdwrf/axum-typed-routing?ref=160684a406d616974d851bbfc6d0d9ffa65367e5", version = "0.2.0" } # version with axum 0.8 compat isn't pushed sadly
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.8"
|
||||
axum-typed-routing = { path = "../vendor/axum-typed-routing", version = "0.2.0", features = ["aide"]}
|
||||
axum-test = { version = "17", features = [] }
|
||||
json = "0.12"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
@ -39,5 +39,5 @@ impl ExampleController {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
let _router: axum::Router<AppState> = ExampleController::into_router(AppState());
|
||||
let _router: axum::Router<AppState>= ExampleController.into_router(AppState());
|
||||
}
|
||||
|
|
8
flake.lock
generated
8
flake.lock
generated
|
@ -84,16 +84,16 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1741010256,
|
||||
"narHash": "sha256-WZNlK/KX7Sni0RyqLSqLPbK8k08Kq7H7RijPJbq9KHM=",
|
||||
"lastModified": 1741073343,
|
||||
"narHash": "sha256-8qmLpDUmaiBGLZkFfVyK5/T5fyTXXGdzCRdqAtO0gf4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ba487dbc9d04e0634c64e3b1f0d25839a0a68246",
|
||||
"rev": "72bccb2960235fd31de456566789c324a251f297",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"ref": "nixos-unstable-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
18
flake.nix
18
flake.nix
|
@ -10,7 +10,7 @@
|
|||
];
|
||||
};
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
|
@ -53,6 +53,7 @@
|
|||
];
|
||||
in
|
||||
{
|
||||
apps.devshell = self.outputs.devShells.${system}.default.flakeApp;
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs;
|
||||
[
|
||||
|
@ -89,18 +90,5 @@
|
|||
export MALLOC_CONF=thp:always,metadata_thp:always
|
||||
'';
|
||||
};
|
||||
packages = {
|
||||
# default = pkgs.callPackage ./package.nix { };
|
||||
};
|
||||
}) // {
|
||||
hydraJobs =
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
# packages = self.packages."${system}";
|
||||
devShells = self.devShells."${system}";
|
||||
in
|
||||
{
|
||||
inherit devShells;
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
24
package.nix
24
package.nix
|
@ -1,24 +0,0 @@
|
|||
{ lib
|
||||
, fetchFromGitHub
|
||||
, rustPlatform
|
||||
,
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "axum-controller";
|
||||
version = "0.0.1";
|
||||
|
||||
src = ./.;
|
||||
|
||||
useFetchCargoVendor = true;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
meta = {
|
||||
# description = "";
|
||||
# homepage = "";
|
||||
# license = lib.licenses.unlicense;
|
||||
maintainers = [ ];
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"files":{"Cargo.toml":"e56e1669f5c26818c13c68258f40e6d9156d1410ab3f9330219902f0efbcbc4b","README.md":"cdb9d483d904c1c10c86358dd089a805880d4c7b5ac4316589c74e4bf2cdc870","src/compilation.rs":"ea1a35cb02f32ef4a25204546caa9a3aa3c0e6d225666a8966bac87147328c55","src/lib.rs":"482d132fa15cc582e7826e44924de37659713f8c75db1410df962529545e893c","src/parsing.rs":"d81011c3a7d438d25c1607abf57501320c5ce4c9c3b867d78340947bf10e730a"},"package":null}
|
56
vendor/axum-typed-routing-macros/Cargo.toml
vendored
56
vendor/axum-typed-routing-macros/Cargo.toml
vendored
|
@ -1,56 +0,0 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
autobenches = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autolib = false
|
||||
autotests = false
|
||||
build = false
|
||||
categories = ["web-programming"]
|
||||
description = "Typed routing macros for axum"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/jvdwrf/axum-typed-routing"
|
||||
keywords = ["axum", "handler", "macro", "routing", "typed"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "axum-typed-routing-macros"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/jvdwrf/axum-typed-routing"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
name = "axum_typed_routing_macros"
|
||||
path = "src/lib.rs"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
|
||||
[dependencies.syn]
|
||||
features = ["full"]
|
||||
version = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
schemars = "0.8"
|
||||
|
||||
[dev-dependencies.aide]
|
||||
features = ["axum", "axum-json", "axum-query"]
|
||||
version = "0.14"
|
||||
|
||||
[dev-dependencies.axum]
|
||||
features = []
|
||||
version = "0.8"
|
||||
|
||||
[dev-dependencies.serde]
|
||||
features = ["derive"]
|
||||
version = "1.0"
|
1
vendor/axum-typed-routing-macros/README.md
vendored
1
vendor/axum-typed-routing-macros/README.md
vendored
|
@ -1 +0,0 @@
|
|||
Macro's for `axum-typed-routing`.
|
471
vendor/axum-typed-routing-macros/src/compilation.rs
vendored
471
vendor/axum-typed-routing-macros/src/compilation.rs
vendored
|
@ -1,471 +0,0 @@
|
|||
use quote::ToTokens;
|
||||
use syn::{spanned::Spanned, LitBool, LitInt, Pat, PatType};
|
||||
|
||||
use crate::parsing::{OapiOptions, Responses, Security, StrArray};
|
||||
|
||||
use self::parsing::PathParam;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct CompiledRoute {
|
||||
pub method: Method,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub path_params: Vec<(Slash, PathParam)>,
|
||||
pub query_params: Vec<(Ident, Box<Type>)>,
|
||||
pub state: Type,
|
||||
pub route_lit: LitStr,
|
||||
pub oapi_options: Option<OapiOptions>,
|
||||
}
|
||||
|
||||
impl CompiledRoute {
|
||||
pub fn to_axum_path_string(&self) -> String {
|
||||
let mut path = String::new();
|
||||
|
||||
for (_slash, param) in &self.path_params {
|
||||
path.push('/');
|
||||
match param {
|
||||
PathParam::Capture(lit, _brace_1, _, _, _brace_2) => {
|
||||
path.push('{');
|
||||
path.push_str(&lit.value());
|
||||
path.push('}');
|
||||
}
|
||||
PathParam::WildCard(lit, _brace_1, _, _, _, _brace_2) => {
|
||||
path.push('{');
|
||||
path.push('*');
|
||||
path.push_str(&lit.value());
|
||||
path.push('}');
|
||||
}
|
||||
PathParam::Static(lit) => path.push_str(&lit.value()),
|
||||
}
|
||||
// if colon.is_some() {
|
||||
// path.push(':');
|
||||
// }
|
||||
// path.push_str(&ident.value());
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Removes the arguments in `route` from `args`, and merges them in the output.
|
||||
pub fn from_route(mut route: Route, function: &ItemFn, with_aide: bool) -> syn::Result<Self> {
|
||||
if !with_aide && route.oapi_options.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"Use `api_route` instead of `route` to use OpenAPI options",
|
||||
));
|
||||
} else if with_aide && route.oapi_options.is_none() {
|
||||
route.oapi_options = Some(OapiOptions {
|
||||
summary: None,
|
||||
description: None,
|
||||
id: None,
|
||||
hidden: None,
|
||||
tags: None,
|
||||
security: None,
|
||||
responses: None,
|
||||
transform: None,
|
||||
});
|
||||
}
|
||||
|
||||
let sig = &function.sig;
|
||||
let mut arg_map = sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
syn::FnArg::Receiver(_) => None,
|
||||
syn::FnArg::Typed(pat_type) => Some(pat_type),
|
||||
})
|
||||
.filter_map(|pat_type| match &*pat_type.pat {
|
||||
syn::Pat::Ident(ident) => Some((ident.ident.clone(), pat_type.ty.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for (_slash, path_param) in &mut route.path_params {
|
||||
match path_param {
|
||||
PathParam::Capture(_lit, _, ident, ty, _) => {
|
||||
let (new_ident, new_ty) = arg_map.remove_entry(ident).ok_or_else(|| {
|
||||
syn::Error::new(
|
||||
ident.span(),
|
||||
format!("path parameter `{}` not found in function arguments", ident),
|
||||
)
|
||||
})?;
|
||||
*ident = new_ident;
|
||||
*ty = new_ty;
|
||||
}
|
||||
PathParam::WildCard(_lit, _, _star, ident, ty, _) => {
|
||||
let (new_ident, new_ty) = arg_map.remove_entry(ident).ok_or_else(|| {
|
||||
syn::Error::new(
|
||||
ident.span(),
|
||||
format!("path parameter `{}` not found in function arguments", ident),
|
||||
)
|
||||
})?;
|
||||
*ident = new_ident;
|
||||
*ty = new_ty;
|
||||
}
|
||||
PathParam::Static(_lit) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for ident in route.query_params {
|
||||
let (ident, ty) = arg_map.remove_entry(&ident).ok_or_else(|| {
|
||||
syn::Error::new(
|
||||
ident.span(),
|
||||
format!(
|
||||
"query parameter `{}` not found in function arguments",
|
||||
ident
|
||||
),
|
||||
)
|
||||
})?;
|
||||
query_params.push((ident, ty));
|
||||
}
|
||||
|
||||
if let Some(options) = route.oapi_options.as_mut() {
|
||||
options.merge_with_fn(function)
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
route_lit: route.route_lit,
|
||||
method: route.method,
|
||||
path_params: route.path_params,
|
||||
query_params,
|
||||
state: route.state.unwrap_or_else(|| guess_state_type(sig)),
|
||||
oapi_options: route.oapi_options,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path_extractor(&self) -> Option<TokenStream2> {
|
||||
if !self.path_params.iter().any(|(_, param)| param.captures()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path_iter = self
|
||||
.path_params
|
||||
.iter()
|
||||
.filter_map(|(_slash, path_param)| path_param.capture());
|
||||
let idents = path_iter.clone().map(|item| item.0);
|
||||
let types = path_iter.clone().map(|item| item.1);
|
||||
Some(quote! {
|
||||
::axum::extract::Path((#(#idents,)*)): ::axum::extract::Path<(#(#types,)*)>,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_extractor(&self) -> Option<TokenStream2> {
|
||||
if self.query_params.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let idents = self.query_params.iter().map(|item| &item.0);
|
||||
Some(quote! {
|
||||
::axum::extract::Query(__QueryParams__ {
|
||||
#(#idents,)*
|
||||
}): ::axum::extract::Query<__QueryParams__>,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_params_struct(&self, with_aide: bool) -> Option<TokenStream2> {
|
||||
match self.query_params.is_empty() {
|
||||
true => None,
|
||||
false => {
|
||||
let idents = self.query_params.iter().map(|item| &item.0);
|
||||
let types = self.query_params.iter().map(|item| &item.1);
|
||||
let derive = match with_aide {
|
||||
true => quote! { #[derive(::serde::Deserialize, ::schemars::JsonSchema)] },
|
||||
false => quote! { #[derive(::serde::Deserialize)] },
|
||||
};
|
||||
Some(quote! {
|
||||
#derive
|
||||
struct __QueryParams__ {
|
||||
#(#idents: #types,)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extracted_idents(&self) -> Vec<Ident> {
|
||||
let mut idents = Vec::new();
|
||||
for (_slash, path_param) in &self.path_params {
|
||||
if let Some((ident, _ty)) = path_param.capture() {
|
||||
idents.push(ident.clone());
|
||||
}
|
||||
// if let Some((_colon, ident, _ty)) = colon {
|
||||
// idents.push(ident.clone());
|
||||
// }
|
||||
}
|
||||
for (ident, _ty) in &self.query_params {
|
||||
idents.push(ident.clone());
|
||||
}
|
||||
idents
|
||||
}
|
||||
|
||||
/// The arguments not used in the route.
|
||||
/// Map the identifier to `___arg___{i}: Type`.
|
||||
pub fn remaining_pattypes_numbered(
|
||||
&self,
|
||||
args: &Punctuated<FnArg, Comma>,
|
||||
) -> Punctuated<PatType, Comma> {
|
||||
args.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, item)| {
|
||||
if let FnArg::Typed(pat_type) = item {
|
||||
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
|
||||
if self.path_params.iter().any(|(_slash, path_param)| {
|
||||
if let Some((path_ident, _ty)) = path_param.capture() {
|
||||
path_ident == &pat_ident.ident
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) || self
|
||||
.query_params
|
||||
.iter()
|
||||
.any(|(query_ident, _)| query_ident == &pat_ident.ident)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_pat_type = pat_type.clone();
|
||||
let ident = format_ident!("___arg___{}", i);
|
||||
new_pat_type.pat = Box::new(parse_quote!(#ident));
|
||||
Some(new_pat_type)
|
||||
} else {
|
||||
unimplemented!("Self type is not supported")
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn ide_documentation_for_aide_methods(&self) -> TokenStream2 {
|
||||
let Some(options) = &self.oapi_options else {
|
||||
return quote! {};
|
||||
};
|
||||
let summary = options.summary.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("summary", ident.span());
|
||||
quote!( let x = x.#method(""); )
|
||||
});
|
||||
let description = options.description.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("description", ident.span());
|
||||
quote!( let x = x.#method(""); )
|
||||
});
|
||||
let id = options.id.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("id", ident.span());
|
||||
quote!( let x = x.#method(""); )
|
||||
});
|
||||
let hidden = options.hidden.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("hidden", ident.span());
|
||||
quote!( let x = x.#method(false); )
|
||||
});
|
||||
let tags = options.tags.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("tag", ident.span());
|
||||
quote!( let x = x.#method(""); )
|
||||
});
|
||||
let security = options.security.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("security_requirement_scopes", ident.span());
|
||||
quote!( let x = x.#method("", [""]); )
|
||||
});
|
||||
let responses = options.responses.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("response", ident.span());
|
||||
quote!( let x = x.#method::<0, String>(); )
|
||||
});
|
||||
let transform = options.transform.as_ref().map(|(ident, _)| {
|
||||
let method = Ident::new("with", ident.span());
|
||||
quote!( let x = x.#method(|x|x); )
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
#[allow(clippy::no_effect)]
|
||||
fn ____ide_documentation_for_aide____(x: ::aide::transform::TransformOperation) {
|
||||
#summary
|
||||
#description
|
||||
#id
|
||||
#hidden
|
||||
#tags
|
||||
#security
|
||||
#responses
|
||||
#transform
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_oapi_summary(&self) -> Option<LitStr> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(summary) = &oapi_options.summary {
|
||||
return Some(summary.1.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_oapi_description(&self) -> Option<LitStr> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(description) = &oapi_options.description {
|
||||
return Some(description.1.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_oapi_hidden(&self) -> Option<LitBool> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(hidden) = &oapi_options.hidden {
|
||||
return Some(hidden.1.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_oapi_tags(&self) -> Vec<LitStr> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(tags) = &oapi_options.tags {
|
||||
return tags.1 .0.clone();
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn get_oapi_id(&self, sig: &Signature) -> Option<LitStr> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(id) = &oapi_options.id {
|
||||
return Some(id.1.clone());
|
||||
}
|
||||
}
|
||||
Some(LitStr::new(&sig.ident.to_string(), sig.ident.span()))
|
||||
}
|
||||
|
||||
pub fn get_oapi_transform(&self) -> syn::Result<Option<TokenStream2>> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some(transform) = &oapi_options.transform {
|
||||
if transform.1.inputs.len() != 1 {
|
||||
return Err(syn::Error::new(
|
||||
transform.1.span(),
|
||||
"expected a single identifier",
|
||||
));
|
||||
}
|
||||
|
||||
let pat = transform.1.inputs.first().unwrap();
|
||||
let body = &transform.1.body;
|
||||
|
||||
if let Pat::Ident(pat_ident) = pat {
|
||||
let ident = &pat_ident.ident;
|
||||
return Ok(Some(quote! {
|
||||
let #ident = __op__;
|
||||
let __op__ = #body;
|
||||
}));
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
pat.span(),
|
||||
"expected a single identifier without type",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn get_oapi_responses(&self) -> Vec<(LitInt, Type)> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some((_ident, Responses(responses))) = &oapi_options.responses {
|
||||
return responses.clone();
|
||||
}
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn get_oapi_security(&self) -> Vec<(LitStr, Vec<LitStr>)> {
|
||||
if let Some(oapi_options) = &self.oapi_options {
|
||||
if let Some((_ident, Security(security))) = &oapi_options.security {
|
||||
return security
|
||||
.iter()
|
||||
.map(|(scheme, StrArray(scopes))| (scheme.clone(), scopes.clone()))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub(crate) fn to_doc_comments(&self) -> TokenStream2 {
|
||||
let mut doc = format!(
|
||||
"# Handler information
|
||||
- Method: `{}`
|
||||
- Path: `{}`
|
||||
- State: `{}`",
|
||||
self.method.to_axum_method_name(),
|
||||
self.route_lit.value(),
|
||||
self.state.to_token_stream(),
|
||||
);
|
||||
|
||||
if let Some(options) = &self.oapi_options {
|
||||
let summary = options
|
||||
.summary
|
||||
.as_ref()
|
||||
.map(|(_, summary)| format!("\"{}\"", summary.value()))
|
||||
.unwrap_or("None".to_string());
|
||||
let description = options
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|(_, description)| format!("\"{}\"", description.value()))
|
||||
.unwrap_or("None".to_string());
|
||||
let id = options
|
||||
.id
|
||||
.as_ref()
|
||||
.map(|(_, id)| format!("\"{}\"", id.value()))
|
||||
.unwrap_or("None".to_string());
|
||||
let hidden = options
|
||||
.hidden
|
||||
.as_ref()
|
||||
.map(|(_, hidden)| hidden.value().to_string())
|
||||
.unwrap_or("None".to_string());
|
||||
let tags = options
|
||||
.tags
|
||||
.as_ref()
|
||||
.map(|(_, tags)| tags.to_string())
|
||||
.unwrap_or("[]".to_string());
|
||||
let security = options
|
||||
.security
|
||||
.as_ref()
|
||||
.map(|(_, security)| security.to_string())
|
||||
.unwrap_or("{}".to_string());
|
||||
|
||||
doc = format!(
|
||||
"{doc}
|
||||
|
||||
## OpenAPI
|
||||
- Summary: `{summary}`
|
||||
- Description: `{description}`
|
||||
- Operation id: `{id}`
|
||||
- Tags: `{tags}`
|
||||
- Security: `{security}`
|
||||
- Hidden: `{hidden}`
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
quote!(
|
||||
#[doc = #doc]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_state_type(sig: &syn::Signature) -> Type {
|
||||
for arg in &sig.inputs {
|
||||
if let FnArg::Typed(pat_type) = arg {
|
||||
// Returns `T` if the type of the last segment is exactly `State<T>`.
|
||||
if let Type::Path(ty) = &*pat_type.ty {
|
||||
let last_segment = ty.path.segments.last().unwrap();
|
||||
if last_segment.ident == "State" {
|
||||
if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
|
||||
if args.args.len() == 1 {
|
||||
if let GenericArgument::Type(ty) = args.args.first().unwrap() {
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parse_quote! { () }
|
||||
}
|
244
vendor/axum-typed-routing-macros/src/lib.rs
vendored
244
vendor/axum-typed-routing-macros/src/lib.rs
vendored
|
@ -1,244 +0,0 @@
|
|||
use compilation::CompiledRoute;
|
||||
use parsing::{Method, Route};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
|
||||
use std::collections::HashMap;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
token::{Comma, Slash},
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, PathArguments, Signature, Type,
|
||||
};
|
||||
#[macro_use]
|
||||
extern crate quote;
|
||||
#[macro_use]
|
||||
extern crate syn;
|
||||
|
||||
mod compilation;
|
||||
mod parsing;
|
||||
|
||||
/// A macro that generates statically-typed routes for axum handlers.
|
||||
///
|
||||
/// # Syntax
|
||||
/// ```ignore
|
||||
/// #[route(<METHOD> "<PATH>" [with <STATE>])]
|
||||
/// ```
|
||||
/// - `METHOD` is the HTTP method, such as `GET`, `POST`, `PUT`, etc.
|
||||
/// - `PATH` is the path of the route, with optional path parameters and query parameters,
|
||||
/// e.g. `/item/:id?amount&offset`.
|
||||
/// - `STATE` is the type of axum-state, passed to the handler. This is optional, and if not
|
||||
/// specified, the state type is guessed based on the parameters of the handler.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use axum::extract::{State, Json};
|
||||
/// use axum_typed_routing_macros::route;
|
||||
///
|
||||
/// #[route(GET "/item/:id?amount&offset")]
|
||||
/// async fn item_handler(
|
||||
/// id: u32,
|
||||
/// amount: Option<u32>,
|
||||
/// offset: Option<u32>,
|
||||
/// State(state): State<String>,
|
||||
/// Json(json): Json<u32>,
|
||||
/// ) -> String {
|
||||
/// todo!("handle request")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # State type
|
||||
/// Normally, the state-type is guessed based on the parameters of the function:
|
||||
/// If the function has a parameter of type `[..]::State<T>`, then `T` is used as the state type.
|
||||
/// This should work for most cases, however when not sufficient, the state type can be specified
|
||||
/// explicitly using the `with` keyword:
|
||||
/// ```ignore
|
||||
/// #[route(GET "/item/:id?amount&offset" with String)]
|
||||
/// ```
|
||||
///
|
||||
/// # Internals
|
||||
/// The macro expands to a function with signature `fn() -> (&'static str, axum::routing::MethodRouter<S>)`.
|
||||
/// The first element of the tuple is the path, and the second is axum's `MethodRouter`.
|
||||
///
|
||||
/// The path and query are extracted using axum's `extract::Path` and `extract::Query` extractors, as the first
|
||||
/// and second parameters of the function. The remaining parameters are the parameters of the handler.
|
||||
#[proc_macro_attribute]
|
||||
pub fn route(attr: TokenStream, mut item: TokenStream) -> TokenStream {
|
||||
match _route(attr, item.clone(), false) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(err) => {
|
||||
let err: TokenStream = err.to_compile_error().into();
|
||||
item.extend(err);
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`macro@route`], but with support for OpenApi using `aide`. See [`macro@route`] for more
|
||||
/// information and examples.
|
||||
///
|
||||
/// # Syntax
|
||||
/// ```ignore
|
||||
/// #[api_route(<METHOD> "<PATH>" [with <STATE>] [{
|
||||
/// summary: "<SUMMARY>",
|
||||
/// description: "<DESCRIPTION>",
|
||||
/// id: "<ID>",
|
||||
/// tags: ["<TAG>", ..],
|
||||
/// hidden: <bool>,
|
||||
/// security: { <SCHEME>: ["<SCOPE>", ..], .. },
|
||||
/// responses: { <CODE>: <TYPE>, .. },
|
||||
/// transform: |op| { .. },
|
||||
/// }])]
|
||||
/// ```
|
||||
/// - `summary` is the OpenApi summary. If not specified, the first line of the function's doc-comments
|
||||
/// - `description` is the OpenApi description. If not specified, the rest of the function's doc-comments
|
||||
/// - `id` is the OpenApi operationId. If not specified, the function's name is used.
|
||||
/// - `tags` are the OpenApi tags.
|
||||
/// - `hidden` sets whether docs should be hidden for this route.
|
||||
/// - `security` is the OpenApi security requirements.
|
||||
/// - `responses` are the OpenApi responses.
|
||||
/// - `transform` is a closure that takes an `TransformOperation` and returns an `TransformOperation`.
|
||||
///
|
||||
/// This may override the other options. (see the crate `aide` for more information).
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use axum::extract::{State, Json};
|
||||
/// use axum_typed_routing_macros::api_route;
|
||||
///
|
||||
/// #[api_route(GET "/item/:id?amount&offset" with String {
|
||||
/// summary: "Get an item",
|
||||
/// description: "Get an item by id",
|
||||
/// id: "get-item",
|
||||
/// tags: ["items"],
|
||||
/// hidden: false,
|
||||
/// security: { "bearer": ["read:items"] },
|
||||
/// responses: { 200: String },
|
||||
/// transform: |op| op.tag("private"),
|
||||
/// })]
|
||||
/// async fn item_handler(
|
||||
/// id: u32,
|
||||
/// amount: Option<u32>,
|
||||
/// offset: Option<u32>,
|
||||
/// State(state): State<String>,
|
||||
/// ) -> String {
|
||||
/// todo!("handle request")
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn api_route(attr: TokenStream, mut item: TokenStream) -> TokenStream {
|
||||
match _route(attr, item.clone(), true) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(err) => {
|
||||
let err: TokenStream = err.to_compile_error().into();
|
||||
item.extend(err);
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _route(attr: TokenStream, item: TokenStream, with_aide: bool) -> syn::Result<TokenStream2> {
|
||||
// Parse the route and function
|
||||
let route = syn::parse::<Route>(attr)?;
|
||||
let function = syn::parse::<ItemFn>(item)?;
|
||||
|
||||
// Now we can compile the route
|
||||
let route = CompiledRoute::from_route(route, &function, with_aide)?;
|
||||
let path_extractor = route.path_extractor();
|
||||
let query_extractor = route.query_extractor();
|
||||
let query_params_struct = route.query_params_struct(with_aide);
|
||||
let state_type = &route.state;
|
||||
let axum_path = route.to_axum_path_string();
|
||||
let http_method = route.method.to_axum_method_name();
|
||||
let remaining_numbered_pats = route.remaining_pattypes_numbered(&function.sig.inputs);
|
||||
let extracted_idents = route.extracted_idents();
|
||||
let remaining_numbered_idents = remaining_numbered_pats.iter().map(|pat_type| &pat_type.pat);
|
||||
let route_docs = route.to_doc_comments();
|
||||
|
||||
// Get the variables we need for code generation
|
||||
let fn_name = &function.sig.ident;
|
||||
let fn_output = &function.sig.output;
|
||||
let vis = &function.vis;
|
||||
let asyncness = &function.sig.asyncness;
|
||||
let (impl_generics, ty_generics, where_clause) = &function.sig.generics.split_for_impl();
|
||||
let ty_generics = ty_generics.as_turbofish();
|
||||
let fn_docs = function
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("doc"));
|
||||
|
||||
let (aide_ident_docs, inner_fn_call, method_router_ty) = if with_aide {
|
||||
let http_method = format_ident!("{}_with", http_method);
|
||||
let summary = route
|
||||
.get_oapi_summary()
|
||||
.map(|summary| quote! { .summary(#summary) });
|
||||
let description = route
|
||||
.get_oapi_description()
|
||||
.map(|description| quote! { .description(#description) });
|
||||
let hidden = route
|
||||
.get_oapi_hidden()
|
||||
.map(|hidden| quote! { .hidden(#hidden) });
|
||||
let tags = route.get_oapi_tags();
|
||||
let id = route
|
||||
.get_oapi_id(&function.sig)
|
||||
.map(|id| quote! { .id(#id) });
|
||||
let transform = route.get_oapi_transform()?;
|
||||
let responses = route.get_oapi_responses();
|
||||
let response_code = responses.iter().map(|response| &response.0);
|
||||
let response_type = responses.iter().map(|response| &response.1);
|
||||
let security = route.get_oapi_security();
|
||||
let schemes = security.iter().map(|sec| &sec.0);
|
||||
let scopes = security.iter().map(|sec| &sec.1);
|
||||
|
||||
(
|
||||
route.ide_documentation_for_aide_methods(),
|
||||
quote! {
|
||||
::aide::axum::routing::#http_method(
|
||||
__inner__function__ #ty_generics,
|
||||
|__op__| {
|
||||
let __op__ = __op__
|
||||
#summary
|
||||
#description
|
||||
#hidden
|
||||
#id
|
||||
#(.tag(#tags))*
|
||||
#(.security_requirement_scopes::<Vec<&'static str>, _>(#schemes, vec![#(#scopes),*]))*
|
||||
#(.response::<#response_code, #response_type>())*
|
||||
;
|
||||
#transform
|
||||
__op__
|
||||
}
|
||||
)
|
||||
},
|
||||
quote! { ::aide::axum::routing::ApiMethodRouter },
|
||||
)
|
||||
} else {
|
||||
(
|
||||
quote!(),
|
||||
quote! { ::axum::routing::#http_method(__inner__function__ #ty_generics) },
|
||||
quote! { ::axum::routing::MethodRouter },
|
||||
)
|
||||
};
|
||||
|
||||
// Generate the code
|
||||
Ok(quote! {
|
||||
#(#fn_docs)*
|
||||
#route_docs
|
||||
#vis fn #fn_name #impl_generics() -> (&'static str, #method_router_ty<#state_type>) #where_clause {
|
||||
|
||||
#query_params_struct
|
||||
|
||||
#aide_ident_docs
|
||||
#asyncness fn __inner__function__ #impl_generics(
|
||||
#path_extractor
|
||||
#query_extractor
|
||||
#remaining_numbered_pats
|
||||
) #fn_output #where_clause {
|
||||
#function
|
||||
|
||||
#fn_name #ty_generics(#(#extracted_idents,)* #(#remaining_numbered_idents,)* ).await
|
||||
}
|
||||
|
||||
(#axum_path, #inner_fn_call)
|
||||
}
|
||||
})
|
||||
}
|
412
vendor/axum-typed-routing-macros/src/parsing.rs
vendored
412
vendor/axum-typed-routing-macros/src/parsing.rs
vendored
|
@ -1,412 +0,0 @@
|
|||
use core::panic;
|
||||
|
||||
use quote::ToTokens;
|
||||
use syn::{
|
||||
token::{Brace, Star},
|
||||
Attribute, Expr, ExprClosure, Lit, LitBool, LitInt,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct RouteParser {
|
||||
path_params: Vec<(Slash, PathParam)>,
|
||||
query_params: Vec<Ident>,
|
||||
}
|
||||
|
||||
impl RouteParser {
|
||||
fn new(lit: LitStr) -> syn::Result<Self> {
|
||||
let val = lit.value();
|
||||
let span = lit.span();
|
||||
let split_route = val.split('?').collect::<Vec<_>>();
|
||||
if split_route.len() > 2 {
|
||||
return Err(syn::Error::new(span, "expected at most one '?'"));
|
||||
}
|
||||
|
||||
let path = split_route[0];
|
||||
if !path.starts_with('/') {
|
||||
return Err(syn::Error::new(span, "expected path to start with '/'"));
|
||||
}
|
||||
let path = path.strip_prefix('/').unwrap();
|
||||
|
||||
let mut path_params = Vec::new();
|
||||
#[allow(clippy::never_loop)]
|
||||
for path_param in path.split('/') {
|
||||
path_params.push((
|
||||
Slash(span),
|
||||
PathParam::new(path_param, span, Box::new(parse_quote!(()))),
|
||||
));
|
||||
}
|
||||
|
||||
let path_param_len = path_params.len();
|
||||
for (i, (_slash, path_param)) in path_params.iter().enumerate() {
|
||||
match path_param {
|
||||
PathParam::WildCard(_, _, _, _, _, _) => {
|
||||
if i != path_param_len - 1 {
|
||||
return Err(syn::Error::new(
|
||||
span,
|
||||
"wildcard path param must be the last path param",
|
||||
));
|
||||
}
|
||||
}
|
||||
PathParam::Capture(_, _, _, _, _) => (),
|
||||
PathParam::Static(lit) => {
|
||||
if lit.value() == "*" && i != path_param_len - 1 {
|
||||
return Err(syn::Error::new(
|
||||
span,
|
||||
"wildcard path param must be the last path param",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
if split_route.len() == 2 {
|
||||
let query = split_route[1];
|
||||
for query_param in query.split('&') {
|
||||
query_params.push(Ident::new(query_param, span));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path_params,
|
||||
query_params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PathParam {
|
||||
WildCard(LitStr, Brace, Star, Ident, Box<Type>, Brace),
|
||||
Capture(LitStr, Brace, Ident, Box<Type>, Brace),
|
||||
Static(LitStr),
|
||||
}
|
||||
|
||||
impl PathParam {
|
||||
pub fn captures(&self) -> bool {
|
||||
matches!(self, Self::Capture(..) | Self::WildCard(..))
|
||||
}
|
||||
|
||||
// pub fn lit(&self) -> &LitStr {
|
||||
// match self {
|
||||
// Self::Capture(lit, _, _, _) => lit,
|
||||
// Self::WildCard(lit, _, _, _) => lit,
|
||||
// Self::Static(lit) => lit,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn capture(&self) -> Option<(&Ident, &Type)> {
|
||||
match self {
|
||||
Self::Capture(_, _, ident, ty, _) => Some((ident, ty)),
|
||||
Self::WildCard(_, _, _, ident, ty, _) => Some((ident, ty)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new(str: &str, span: Span, ty: Box<Type>) -> Self {
|
||||
if str.starts_with(':') {
|
||||
let str = str.strip_prefix(':').unwrap();
|
||||
Self::Capture(
|
||||
LitStr::new(str, span),
|
||||
Brace(span),
|
||||
Ident::new(str, span),
|
||||
ty,
|
||||
Brace(span),
|
||||
)
|
||||
} else if str.starts_with('*') && str.len() > 1 {
|
||||
let str = str.strip_prefix('*').unwrap();
|
||||
Self::WildCard(
|
||||
LitStr::new(str, span),
|
||||
Brace(span),
|
||||
Star(span),
|
||||
Ident::new(str, span),
|
||||
ty,
|
||||
Brace(span),
|
||||
)
|
||||
} else {
|
||||
Self::Static(LitStr::new(str, span))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OapiOptions {
|
||||
pub summary: Option<(Ident, LitStr)>,
|
||||
pub description: Option<(Ident, LitStr)>,
|
||||
pub id: Option<(Ident, LitStr)>,
|
||||
pub hidden: Option<(Ident, LitBool)>,
|
||||
pub tags: Option<(Ident, StrArray)>,
|
||||
pub security: Option<(Ident, Security)>,
|
||||
pub responses: Option<(Ident, Responses)>,
|
||||
pub transform: Option<(Ident, ExprClosure)>,
|
||||
}
|
||||
|
||||
pub struct Security(pub Vec<(LitStr, StrArray)>);
|
||||
impl Parse for Security {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
braced!(inner in input);
|
||||
|
||||
let mut arr = Vec::new();
|
||||
while !inner.is_empty() {
|
||||
let scheme = inner.parse::<LitStr>()?;
|
||||
let _ = inner.parse::<Token![:]>()?;
|
||||
let scopes = inner.parse::<StrArray>()?;
|
||||
let _ = inner.parse::<Token![,]>().ok();
|
||||
arr.push((scheme, scopes));
|
||||
}
|
||||
|
||||
Ok(Self(arr))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Security {
|
||||
fn to_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
s.push('{');
|
||||
for (i, (scheme, scopes)) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push_str(", ");
|
||||
}
|
||||
s.push_str(&scheme.value());
|
||||
s.push_str(": ");
|
||||
s.push_str(&scopes.to_string());
|
||||
}
|
||||
s.push('}');
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Responses(pub Vec<(LitInt, Type)>);
|
||||
impl Parse for Responses {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
braced!(inner in input);
|
||||
|
||||
let mut arr = Vec::new();
|
||||
while !inner.is_empty() {
|
||||
let status = inner.parse::<LitInt>()?;
|
||||
let _ = inner.parse::<Token![:]>()?;
|
||||
let ty = inner.parse::<Type>()?;
|
||||
let _ = inner.parse::<Token![,]>().ok();
|
||||
arr.push((status, ty));
|
||||
}
|
||||
|
||||
Ok(Self(arr))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Responses {
|
||||
fn to_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
s.push('{');
|
||||
for (i, (status, ty)) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push_str(", ");
|
||||
}
|
||||
s.push_str(&status.to_string());
|
||||
s.push_str(": ");
|
||||
s.push_str(&ty.to_token_stream().to_string());
|
||||
}
|
||||
s.push('}');
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StrArray(pub Vec<LitStr>);
|
||||
impl Parse for StrArray {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
bracketed!(inner in input);
|
||||
let mut arr = Vec::new();
|
||||
while !inner.is_empty() {
|
||||
arr.push(inner.parse::<LitStr>()?);
|
||||
inner.parse::<Token![,]>().ok();
|
||||
}
|
||||
Ok(Self(arr))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for StrArray {
|
||||
fn to_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
s.push('[');
|
||||
for (i, lit) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push_str(", ");
|
||||
}
|
||||
s.push('"');
|
||||
s.push_str(&lit.value());
|
||||
s.push('"');
|
||||
}
|
||||
s.push(']');
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for OapiOptions {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut this = Self {
|
||||
summary: None,
|
||||
description: None,
|
||||
id: None,
|
||||
hidden: None,
|
||||
tags: None,
|
||||
security: None,
|
||||
responses: None,
|
||||
transform: None,
|
||||
};
|
||||
|
||||
while !input.is_empty() {
|
||||
let ident = input.parse::<Ident>()?;
|
||||
let _ = input.parse::<Token![:]>()?;
|
||||
match ident.to_string().as_str() {
|
||||
"summary" => this.summary = Some((ident, input.parse()?)),
|
||||
"description" => this.description = Some((ident, input.parse()?)),
|
||||
"id" => this.id = Some((ident, input.parse()?)),
|
||||
"hidden" => this.hidden = Some((ident, input.parse()?)),
|
||||
"tags" => this.tags = Some((ident, input.parse()?)),
|
||||
"security" => this.security = Some((ident, input.parse()?)),
|
||||
"responses" => this.responses = Some((ident, input.parse()?)),
|
||||
"transform" => this.transform = Some((ident, input.parse()?)),
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"unexpected field, expected one of (summary, description, id, hidden, tags, security, responses, transform)",
|
||||
))
|
||||
}
|
||||
}
|
||||
let _ = input.parse::<Token![,]>().ok();
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl OapiOptions {
|
||||
pub fn merge_with_fn(&mut self, function: &ItemFn) {
|
||||
if self.description.is_none() {
|
||||
self.description = doc_iter(&function.attrs)
|
||||
.skip(2)
|
||||
.map(|item| item.value())
|
||||
.reduce(|mut acc, item| {
|
||||
acc.push('\n');
|
||||
acc.push_str(&item);
|
||||
acc
|
||||
})
|
||||
.map(|item| (parse_quote!(description), parse_quote!(#item)))
|
||||
}
|
||||
if self.summary.is_none() {
|
||||
self.summary = doc_iter(&function.attrs)
|
||||
.next()
|
||||
.map(|item| (parse_quote!(summary), item.clone()))
|
||||
}
|
||||
if self.id.is_none() {
|
||||
let id = &function.sig.ident;
|
||||
self.id = Some((parse_quote!(id), LitStr::new(&id.to_string(), id.span())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn doc_iter(attrs: &[Attribute]) -> impl Iterator<Item = &LitStr> + '_ {
|
||||
attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("doc"))
|
||||
.map(|attr| {
|
||||
let Meta::NameValue(meta) = &attr.meta else {
|
||||
panic!("doc attribute is not a name-value attribute");
|
||||
};
|
||||
let Expr::Lit(lit) = &meta.value else {
|
||||
panic!("doc attribute is not a string literal");
|
||||
};
|
||||
let Lit::Str(lit_str) = &lit.lit else {
|
||||
panic!("doc attribute is not a string literal");
|
||||
};
|
||||
lit_str
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Route {
|
||||
pub method: Method,
|
||||
pub path_params: Vec<(Slash, PathParam)>,
|
||||
pub query_params: Vec<Ident>,
|
||||
pub state: Option<Type>,
|
||||
pub route_lit: LitStr,
|
||||
pub oapi_options: Option<OapiOptions>,
|
||||
}
|
||||
|
||||
impl Parse for Route {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let method = input.parse::<Method>()?;
|
||||
let route_lit = input.parse::<LitStr>()?;
|
||||
let route_parser = RouteParser::new(route_lit.clone())?;
|
||||
let state = match input.parse::<kw::with>() {
|
||||
Ok(_) => Some(input.parse::<Type>()?),
|
||||
Err(_) => None,
|
||||
};
|
||||
let oapi_options = input
|
||||
.peek(Brace)
|
||||
.then(|| {
|
||||
let inner;
|
||||
braced!(inner in input);
|
||||
inner.parse::<OapiOptions>()
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(Route {
|
||||
method,
|
||||
path_params: route_parser.path_params,
|
||||
query_params: route_parser.query_params,
|
||||
state,
|
||||
route_lit,
|
||||
oapi_options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Method {
|
||||
Get(Span),
|
||||
Post(Span),
|
||||
Put(Span),
|
||||
Delete(Span),
|
||||
Head(Span),
|
||||
Connect(Span),
|
||||
Options(Span),
|
||||
Trace(Span),
|
||||
}
|
||||
|
||||
impl Parse for Method {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let ident = input.parse::<Ident>()?;
|
||||
match ident.to_string().to_uppercase().as_str() {
|
||||
"GET" => Ok(Self::Get(ident.span())),
|
||||
"POST" => Ok(Self::Post(ident.span())),
|
||||
"PUT" => Ok(Self::Put(ident.span())),
|
||||
"DELETE" => Ok(Self::Delete(ident.span())),
|
||||
"HEAD" => Ok(Self::Head(ident.span())),
|
||||
"CONNECT" => Ok(Self::Connect(ident.span())),
|
||||
"OPTIONS" => Ok(Self::Options(ident.span())),
|
||||
"TRACE" => Ok(Self::Trace(ident.span())),
|
||||
_ => Err(input
|
||||
.error("expected one of (GET, POST, PUT, DELETE, HEAD, CONNECT, OPTIONS, TRACE)")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn to_axum_method_name(&self) -> Ident {
|
||||
match self {
|
||||
Self::Get(span) => Ident::new("get", *span),
|
||||
Self::Post(span) => Ident::new("post", *span),
|
||||
Self::Put(span) => Ident::new("put", *span),
|
||||
Self::Delete(span) => Ident::new("delete", *span),
|
||||
Self::Head(span) => Ident::new("head", *span),
|
||||
Self::Connect(span) => Ident::new("connect", *span),
|
||||
Self::Options(span) => Ident::new("options", *span),
|
||||
Self::Trace(span) => Ident::new("trace", *span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod kw {
|
||||
syn::custom_keyword!(with);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"files":{"Cargo.toml":"97fb5b77f33d2b60a0b82b39fba7ab48f87effdfac5adb5e3df5018d153f8b4b","examples/aide.rs":"a72124d7923cd4dfa76a98bfc10b163c7037048d52819061898c5cfc19c9e0dd","examples/basic.rs":"ef981bcf041580f073808521738f22b864625779c6df628d546a53dbc2e0c95c","src/lib.rs":"3d5a1c1407e5097fd504985b6fb2665b4d1a140db6ed4b15963d64c781a96320","tests/main.rs":"4bed7ddf18d079122144ef8d5d8960cad1f0b51095e23f8c15b00df543c3c49e"},"package":null}
|
81
vendor/axum-typed-routing/Cargo.toml
vendored
81
vendor/axum-typed-routing/Cargo.toml
vendored
|
@ -1,81 +0,0 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
autobenches = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autolib = false
|
||||
autotests = false
|
||||
build = false
|
||||
categories = ["web-programming"]
|
||||
description = "Typed routing macros for axum"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/jvdwrf/axum-typed-routing"
|
||||
keywords = ["axum", "handler", "macro", "routing", "typed"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "axum-typed-routing"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/jvdwrf/axum-typed-routing"
|
||||
version = "0.2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["aide"]
|
||||
|
||||
[features]
|
||||
aide = ["dep:aide"]
|
||||
default = []
|
||||
|
||||
[lib]
|
||||
name = "axum_typed_routing"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "aide"
|
||||
path = "examples/aide.rs"
|
||||
features = ["aide"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
path = "examples/basic.rs"
|
||||
|
||||
[[test]]
|
||||
name = "main"
|
||||
path = "tests/main.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
axum-macros = "0.5"
|
||||
|
||||
[dependencies.aide]
|
||||
features = ["axum"]
|
||||
optional = true
|
||||
version = "0.14"
|
||||
|
||||
[dependencies.axum-typed-routing-macros]
|
||||
path = "../axum-typed-routing-macros"
|
||||
version = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
json = "0.12"
|
||||
schemars = "0.8"
|
||||
|
||||
[dev-dependencies.axum-test]
|
||||
features = []
|
||||
version = "17"
|
||||
|
||||
[dev-dependencies.serde]
|
||||
features = ["derive"]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
features = ["full"]
|
||||
version = "1"
|
28
vendor/axum-typed-routing/examples/aide.rs
vendored
28
vendor/axum-typed-routing/examples/aide.rs
vendored
|
@ -1,28 +0,0 @@
|
|||
#![allow(unused)]
|
||||
use aide::axum::ApiRouter;
|
||||
use axum::extract::{Json, State};
|
||||
use axum_typed_routing::TypedApiRouter;
|
||||
use axum_typed_routing_macros::api_route;
|
||||
|
||||
#[api_route(GET "/item/:id?amount&offset" {
|
||||
summary: "Get an item",
|
||||
description: "Get an item by id",
|
||||
id: "get-item",
|
||||
tags: ["items"],
|
||||
hidden: false
|
||||
})]
|
||||
async fn item_handler(
|
||||
id: u32,
|
||||
amount: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
State(state): State<String>,
|
||||
Json(json): Json<u32>,
|
||||
) -> String {
|
||||
todo!("handle request")
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let router: ApiRouter = ApiRouter::new()
|
||||
.typed_api_route(item_handler)
|
||||
.with_state("state".to_string());
|
||||
}
|
20
vendor/axum-typed-routing/examples/basic.rs
vendored
20
vendor/axum-typed-routing/examples/basic.rs
vendored
|
@ -1,20 +0,0 @@
|
|||
#![allow(unused)]
|
||||
use axum::extract::{Json, State};
|
||||
use axum_typed_routing::{route, TypedRouter};
|
||||
|
||||
#[route(GET "/item/:id?amount&offset")]
|
||||
async fn item_handler(
|
||||
id: u32,
|
||||
amount: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
State(state): State<String>,
|
||||
Json(json): Json<u32>,
|
||||
) -> String {
|
||||
todo!("handle request")
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let router: axum::Router = axum::Router::new()
|
||||
.typed_route(item_handler)
|
||||
.with_state("state".to_string());
|
||||
}
|
125
vendor/axum-typed-routing/src/lib.rs
vendored
125
vendor/axum-typed-routing/src/lib.rs
vendored
|
@ -1,125 +0,0 @@
|
|||
//!
|
||||
//! ## Basic usage
|
||||
//! The following example demonstrates the basic usage of the library.
|
||||
//! On top of any regular handler, you can add the [`route`] macro to create a typed route.
|
||||
//! Any path- or query-parameters in the url will be type-checked at compile-time, and properly
|
||||
//! extracted into the handler.
|
||||
//!
|
||||
//! The following example shows how the path parameter `id`, and query parameters `amount` and
|
||||
//! `offset` are type-checked and extracted into the handler.
|
||||
//!
|
||||
//! ```
|
||||
#![doc = include_str!("../examples/basic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! Some valid url's as get-methods are:
|
||||
//! - `/item/1?amount=2&offset=3`
|
||||
//! - `/item/1?amount=2`
|
||||
//! - `/item/1?offset=3`
|
||||
//! - `/item/500`
|
||||
//!
|
||||
//! By marking the `amount` and `offset` parameters as `Option<T>`, they become optional.
|
||||
//!
|
||||
//! ## Example with `aide`
|
||||
//! When the `aide` feature is enabled, it's possible to automatically generate OpenAPI
|
||||
//! documentation for the routes. The [`api_route`] macro is used in place of the [`route`] macro.
|
||||
//!
|
||||
//! Please read the [`aide`] documentation for more information on usage.
|
||||
//! ```
|
||||
#![doc = include_str!("../examples/aide.rs")]
|
||||
//! ```
|
||||
|
||||
use axum::routing::MethodRouter;
|
||||
|
||||
type TypedHandler<S = ()> = fn() -> (&'static str, MethodRouter<S>);
|
||||
pub use axum_typed_routing_macros::route;
|
||||
|
||||
/// A trait that allows typed routes, created with the [`route`] macro to
|
||||
/// be added to an axum router.
|
||||
///
|
||||
/// Typed handlers are of the form `fn() -> (&'static str, MethodRouter<S>)`, where
|
||||
/// `S` is the state type. The first element of the tuple is the path, and the second
|
||||
/// is the method router.
|
||||
pub trait TypedRouter: Sized {
|
||||
/// The state type of the router.
|
||||
type State: Clone + Send + Sync + 'static;
|
||||
|
||||
/// Add a typed route to the router, usually created with the [`route`] macro.
|
||||
///
|
||||
/// Typed handlers are of the form `fn() -> (&'static str, MethodRouter<S>)`, where
|
||||
/// `S` is the state type. The first element of the tuple is the path, and the second
|
||||
/// is the method router.
|
||||
fn typed_route(self, handler: TypedHandler<Self::State>) -> Self;
|
||||
}
|
||||
|
||||
impl<S> TypedRouter for axum::Router<S>
|
||||
where
|
||||
S: Send + Sync + Clone + 'static,
|
||||
{
|
||||
type State = S;
|
||||
|
||||
fn typed_route(self, handler: TypedHandler<Self::State>) -> Self {
|
||||
let (path, method_router) = handler();
|
||||
self.route(path, method_router)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "aide")]
|
||||
pub use aide_support::*;
|
||||
#[cfg(feature = "aide")]
|
||||
mod aide_support {
|
||||
use crate::{TypedHandler, TypedRouter};
|
||||
use aide::{
|
||||
axum::{routing::ApiMethodRouter, ApiRouter},
|
||||
transform::TransformPathItem,
|
||||
};
|
||||
|
||||
type TypedApiHandler<S = ()> = fn() -> (&'static str, ApiMethodRouter<S>);
|
||||
|
||||
pub use axum_typed_routing_macros::api_route;
|
||||
|
||||
impl<S> TypedRouter for ApiRouter<S>
|
||||
where
|
||||
S: Send + Sync + Clone + 'static,
|
||||
{
|
||||
type State = S;
|
||||
|
||||
fn typed_route(self, handler: TypedHandler<Self::State>) -> Self {
|
||||
let (path, method_router) = handler();
|
||||
self.route(path, method_router)
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`TypedRouter`], but with support for `aide`.
|
||||
pub trait TypedApiRouter: TypedRouter {
|
||||
/// Same as [`TypedRouter::typed_route`], but with support for `aide`.
|
||||
fn typed_api_route(self, handler: TypedApiHandler<Self::State>) -> Self;
|
||||
|
||||
/// Same as [`TypedApiRouter::typed_api_route`], but with a custom path transform for
|
||||
/// use with `aide`.
|
||||
fn typed_api_route_with(
|
||||
self,
|
||||
handler: TypedApiHandler<Self::State>,
|
||||
transform: impl FnOnce(TransformPathItem) -> TransformPathItem,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl<S> TypedApiRouter for ApiRouter<S>
|
||||
where
|
||||
S: Send + Sync + Clone + 'static,
|
||||
{
|
||||
fn typed_api_route(self, handler: TypedApiHandler<Self::State>) -> Self {
|
||||
let (path, method_router) = handler();
|
||||
self.api_route(path, method_router)
|
||||
}
|
||||
|
||||
fn typed_api_route_with(
|
||||
self,
|
||||
handler: TypedApiHandler<Self::State>,
|
||||
transform: impl FnOnce(TransformPathItem) -> TransformPathItem,
|
||||
) -> Self {
|
||||
let (path, method_router) = handler();
|
||||
self.api_route_with(path, method_router, transform)
|
||||
}
|
||||
}
|
||||
}
|
232
vendor/axum-typed-routing/tests/main.rs
vendored
232
vendor/axum-typed-routing/tests/main.rs
vendored
|
@ -1,232 +0,0 @@
|
|||
#![allow(unused)]
|
||||
#![allow(clippy::extra_unused_type_parameters)]
|
||||
|
||||
use std::net::TcpListener;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Form, Json,
|
||||
};
|
||||
use axum_test::TestServer;
|
||||
use axum_typed_routing::TypedRouter;
|
||||
use axum_typed_routing_macros::route;
|
||||
|
||||
/// This is a handler that is documented!
|
||||
#[route(GET "/hello/:id?user_id&name")]
|
||||
async fn generic_handler_with_complex_options<T: 'static>(
|
||||
mut id: u32,
|
||||
user_id: String,
|
||||
name: String,
|
||||
State(state): State<String>,
|
||||
hello: State<String>,
|
||||
Json(mut json): Json<u32>,
|
||||
) -> String {
|
||||
format!("Hello, {id} - {user_id} - {name}!")
|
||||
}
|
||||
|
||||
#[route(POST "/one")]
|
||||
async fn one(state: State<String>) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
#[route(POST "/two")]
|
||||
async fn two() -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
#[route(GET "/three/:id")]
|
||||
async fn three(id: u32) -> String {
|
||||
format!("Hello {id}!")
|
||||
}
|
||||
|
||||
#[route(GET "/four?id")]
|
||||
async fn four(id: u32) -> String {
|
||||
format!("Hello {id:?}!")
|
||||
// String::from("Hello 123!")
|
||||
}
|
||||
|
||||
// Tests that hyphens are allowed in route names
|
||||
#[route(GET "/foo-bar")]
|
||||
async fn foo_bar() {}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_normal() {
|
||||
let router: axum::Router = axum::Router::new()
|
||||
.typed_route(generic_handler_with_complex_options::<u32>)
|
||||
.typed_route(one)
|
||||
.with_state("state".to_string())
|
||||
.typed_route(two)
|
||||
.typed_route(three)
|
||||
.typed_route(four);
|
||||
|
||||
let server = TestServer::new(router).unwrap();
|
||||
|
||||
let response = server.post("/one").await;
|
||||
response.assert_status_ok();
|
||||
response.assert_text("Hello!");
|
||||
|
||||
let response = server.post("/two").await;
|
||||
response.assert_status_ok();
|
||||
response.assert_text("Hello!");
|
||||
|
||||
let response = server.get("/three/123").await;
|
||||
response.assert_status_ok();
|
||||
response.assert_text("Hello 123!");
|
||||
|
||||
let response = server.get("/four").add_query_param("id", 123).await;
|
||||
response.assert_status_ok();
|
||||
response.assert_text("Hello 123!");
|
||||
|
||||
let response = server
|
||||
.get("/hello/123")
|
||||
.add_query_param("user_id", 321.to_string())
|
||||
.add_query_param("name", "John".to_string())
|
||||
.json(&100)
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
response.assert_text("Hello, 123 - 321 - John!");
|
||||
|
||||
let (path, method_router) = generic_handler_with_complex_options::<u32>();
|
||||
assert_eq!(path, "/hello/{id}");
|
||||
}
|
||||
|
||||
#[route(GET "/*")]
|
||||
async fn wildcard() {}
|
||||
|
||||
#[route(GET "/*capture")]
|
||||
async fn wildcard_capture(capture: String) -> Json<String> {
|
||||
Json(capture)
|
||||
}
|
||||
|
||||
#[route(GET "/")]
|
||||
async fn root() {}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wildcard() {
|
||||
let router: axum::Router = axum::Router::new().typed_route(wildcard_capture);
|
||||
|
||||
let server = TestServer::new(router).unwrap();
|
||||
|
||||
let response = server.get("/foo/bar").await;
|
||||
response.assert_status_ok();
|
||||
assert_eq!(response.json::<String>(), "foo/bar");
|
||||
}
|
||||
|
||||
#[cfg(feature = "aide")]
|
||||
mod aide_support {
|
||||
use super::*;
|
||||
use aide::{axum::ApiRouter, openapi::OpenApi, transform::TransformOperation};
|
||||
use axum_typed_routing::TypedApiRouter;
|
||||
use axum_typed_routing_macros::api_route;
|
||||
|
||||
/// get-summary
|
||||
///
|
||||
/// get-description
|
||||
#[api_route(GET "/hello")]
|
||||
async fn get_hello(state: State<String>) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
/// post-summary
|
||||
///
|
||||
/// post-description
|
||||
#[api_route(POST "/hello")]
|
||||
async fn post_hello(state: State<String>) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aide() {
|
||||
let router: aide::axum::ApiRouter = aide::axum::ApiRouter::new()
|
||||
.typed_route(one)
|
||||
.typed_api_route(get_hello)
|
||||
.with_state("state".to_string());
|
||||
|
||||
let (path, method_router) = get_hello();
|
||||
assert_eq!(path, "/hello");
|
||||
|
||||
let (path, method_router) = post_hello();
|
||||
assert_eq!(path, "/hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_and_description_are_generated_from_doc_comments() {
|
||||
let router = ApiRouter::new()
|
||||
.typed_api_route(get_hello)
|
||||
.typed_api_route(post_hello);
|
||||
let mut api = OpenApi::default();
|
||||
router.finish_api(&mut api);
|
||||
|
||||
let get_op = path_item(&api, "/hello").get.as_ref().unwrap();
|
||||
let post_op = path_item(&api, "/hello").post.as_ref().unwrap();
|
||||
|
||||
assert_eq!(get_op.summary, Some(" get-summary".to_string()));
|
||||
assert_eq!(get_op.description, Some(" get-description".to_string()));
|
||||
assert!(get_op.tags.is_empty());
|
||||
|
||||
assert_eq!(post_op.summary, Some(" post-summary".to_string()));
|
||||
assert_eq!(post_op.description, Some(" post-description".to_string()));
|
||||
assert!(post_op.tags.is_empty());
|
||||
}
|
||||
|
||||
/// unused-summary
|
||||
///
|
||||
/// unused-description
|
||||
#[api_route(GET "/hello" {
|
||||
summary: "MySummary",
|
||||
description: "MyDescription",
|
||||
hidden: false,
|
||||
id: "MyRoute",
|
||||
tags: ["MyTag1", "MyTag2"],
|
||||
security: {
|
||||
"MySecurity1": ["MyScope1", "MyScope2"],
|
||||
"MySecurity2": [],
|
||||
},
|
||||
responses: {
|
||||
300: String,
|
||||
},
|
||||
transform: |x| x.summary("OverriddenSummary"),
|
||||
})]
|
||||
async fn get_gello_with_attributes(state: State<String>) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_from_attributes() {
|
||||
let router = ApiRouter::new().typed_api_route(get_gello_with_attributes);
|
||||
let mut api = OpenApi::default();
|
||||
router.finish_api(&mut api);
|
||||
|
||||
let get_op = path_item(&api, "/hello").get.as_ref().unwrap();
|
||||
|
||||
assert_eq!(get_op.summary, Some("OverriddenSummary".to_string()));
|
||||
assert_eq!(get_op.description, Some("MyDescription".to_string()));
|
||||
assert_eq!(
|
||||
get_op.tags,
|
||||
vec!["MyTag1".to_string(), "MyTag2".to_string()]
|
||||
);
|
||||
assert_eq!(get_op.operation_id, Some("MyRoute".to_string()));
|
||||
}
|
||||
|
||||
/// summary
|
||||
///
|
||||
/// description
|
||||
/// description
|
||||
#[api_route(GET "/hello")]
|
||||
async fn get_gello_without_attributes(state: State<String>) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
fn path_item<'a>(api: &'a OpenApi, path: &str) -> &'a aide::openapi::PathItem {
|
||||
api.paths
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|(p, _)| *p == path)
|
||||
.unwrap()
|
||||
.1
|
||||
.as_item()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue