pub struct RouteArgs {
pub method: Option<syn::Ident>,
pub path: Option<syn::LitStr>,
pub error_status: Option<syn::ExprArray>,
pub tags: Option<syn::ExprArray>,
pub description: Option<syn::LitStr>,
}
impl syn::parse::Parse for RouteArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut method: Option<syn::Ident> = None;
let mut path: Option<syn::LitStr> = None;
let mut error_status: Option<syn::ExprArray> = None;
let mut tags: Option<syn::ExprArray> = None;
let mut description: Option<syn::LitStr> = None;
while !input.is_empty() {
let lookahead = input.lookahead1();
if lookahead.peek(syn::Ident) {
let ident: syn::Ident = input.parse()?;
let ident_str = ident.to_string().to_lowercase();
match ident_str.as_str() {
"get" | "post" | "put" | "patch" | "delete" | "head" | "options" => {
method = Some(ident);
}
"path" => {
input.parse::<syn::Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
path = Some(lit);
}
"error_status" => {
input.parse::<syn::Token![=]>()?;
let array: syn::ExprArray = input.parse()?;
error_status = Some(array);
}
"tags" => {
input.parse::<syn::Token![=]>()?;
let array: syn::ExprArray = input.parse()?;
tags = Some(array);
}
"description" => {
input.parse::<syn::Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
description = Some(lit);
}
_ => {
return Err(lookahead.error());
}
}
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
} else {
break;
}
} else {
return Err(lookahead.error());
}
}
Ok(RouteArgs {
method,
path,
error_status,
tags,
description,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("get", true, Some("get"), None, None)]
#[case("post", true, Some("post"), None, None)]
#[case("put", true, Some("put"), None, None)]
#[case("patch", true, Some("patch"), None, None)]
#[case("delete", true, Some("delete"), None, None)]
#[case("head", true, Some("head"), None, None)]
#[case("options", true, Some("options"), None, None)]
#[case("path = \"/api\"", true, None, Some("/api"), None)]
#[case("path = \"/users\"", true, None, Some("/users"), None)]
#[case("path = \"/api/v1\"", true, None, Some("/api/v1"), None)]
#[case("get, path = \"/api\"", true, Some("get"), Some("/api"), None)]
#[case("post, path = \"/users\"", true, Some("post"), Some("/users"), None)]
#[case("path = \"/api\", get", true, Some("get"), Some("/api"), None)]
#[case("error_status = [400]", true, None, None, Some(vec![400]))]
#[case("error_status = [400, 404]", true, None, None, Some(vec![400, 404]))]
#[case("error_status = [400, 404, 500]", true, None, None, Some(vec![400, 404, 500]))]
#[case("get, error_status = [400]", true, Some("get"), None, Some(vec![400]))]
#[case("post, error_status = [400, 404]", true, Some("post"), None, Some(vec![400, 404]))]
#[case("path = \"/api\", error_status = [400]", true, None, Some("/api"), Some(vec![400]))]
#[case("get, path = \"/api\", error_status = [400]", true, Some("get"), Some("/api"), Some(vec![400]))]
#[case("post, path = \"/users\", error_status = [400, 404]", true, Some("post"), Some("/users"), Some(vec![400, 404]))]
#[case("path = \"/api\", get, error_status = [400]", true, Some("get"), Some("/api"), Some(vec![400]))]
#[case("", true, None, None, None)]
#[case("invalid", false, None, None, None)]
#[case("path", false, None, None, None)]
#[case("error_status", false, None, None, None)]
#[case("get, invalid", false, None, None, None)]
#[case("path =", false, None, None, None)]
#[case("error_status =", false, None, None, None)]
#[case("123", false, None, None, None)]
#[case("\"string\"", false, None, None, None)]
#[case("=", false, None, None, None)]
#[case("[", false, None, None, None)]
#[case("]", false, None, None, None)]
#[case(",", false, None, None, None)]
#[case("get, 123", false, None, None, None)]
#[case("get, =", false, None, None, None)]
fn test_route_args_parse(
#[case] input: &str,
#[case] should_parse: bool,
#[case] expected_method: Option<&str>,
#[case] expected_path: Option<&str>,
#[case] expected_error_status: Option<Vec<u16>>,
) {
let result = syn::parse_str::<RouteArgs>(input);
match (should_parse, result) {
(true, Ok(route_args)) => {
if let Some(exp_method) = expected_method {
assert!(
route_args.method.is_some(),
"Expected method {} but got None for input: {}",
exp_method,
input
);
assert_eq!(
route_args.method.as_ref().unwrap().to_string(),
exp_method,
"Method mismatch for input: {}",
input
);
} else {
assert!(
route_args.method.is_none(),
"Expected no method but got {:?} for input: {}",
route_args.method,
input
);
}
if let Some(exp_path) = expected_path {
assert!(
route_args.path.is_some(),
"Expected path {} but got None for input: {}",
exp_path,
input
);
assert_eq!(
route_args.path.as_ref().unwrap().value(),
exp_path,
"Path mismatch for input: {}",
input
);
} else {
assert!(
route_args.path.is_none(),
"Expected no path but got {:?} for input: {}",
route_args.path,
input
);
}
if let Some(exp_status) = expected_error_status {
assert!(
route_args.error_status.is_some(),
"Expected error_status {:?} but got None for input: {}",
exp_status,
input
);
let array = route_args.error_status.as_ref().unwrap();
let mut status_codes = Vec::new();
for elem in &array.elems {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit_int),
..
}) = elem
&& let Ok(code) = lit_int.base10_parse::<u16>()
{
status_codes.push(code);
}
}
assert_eq!(
status_codes, exp_status,
"Error status mismatch for input: {}",
input
);
} else {
assert!(
route_args.error_status.is_none(),
"Expected no error_status but got {:?} for input: {}",
route_args.error_status,
input
);
}
}
(false, Err(_)) => {
}
(true, Err(e)) => {
panic!(
"Expected successful parse but got error: {} for input: {}",
e, input
);
}
(false, Ok(_)) => {
panic!("Expected parse error but got success for input: {}", input);
}
}
}
#[rstest]
#[case("tags = [\"users\"]", true, vec!["users"])]
#[case("tags = [\"users\", \"admin\"]", true, vec!["users", "admin"])]
#[case("tags = [\"api\", \"v1\", \"users\"]", true, vec!["api", "v1", "users"])]
#[case("get, tags = [\"users\"]", true, vec!["users"])]
#[case("post, tags = [\"users\", \"create\"]", true, vec!["users", "create"])]
#[case("path = \"/api\", tags = [\"api\"]", true, vec!["api"])]
#[case("get, path = \"/users\", tags = [\"users\"]", true, vec!["users"])]
#[case("tags = []", true, vec![])]
fn test_route_args_parse_tags(
#[case] input: &str,
#[case] should_parse: bool,
#[case] expected_tags: Vec<&str>,
) {
let result = syn::parse_str::<RouteArgs>(input);
match (should_parse, result) {
(true, Ok(route_args)) => {
if expected_tags.is_empty() {
if let Some(tags_array) = &route_args.tags {
assert!(tags_array.elems.is_empty());
}
} else {
assert!(
route_args.tags.is_some(),
"Expected tags but got None for input: {}",
input
);
let tags_array = route_args.tags.as_ref().unwrap();
let mut parsed_tags = Vec::new();
for elem in &tags_array.elems {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = elem
{
parsed_tags.push(lit_str.value());
}
}
assert_eq!(
parsed_tags, expected_tags,
"Tags mismatch for input: {}",
input
);
}
}
(false, Err(_)) => {
}
(true, Err(e)) => {
panic!(
"Expected successful parse but got error: {} for input: {}",
e, input
);
}
(false, Ok(_)) => {
panic!("Expected parse error but got success for input: {}", input);
}
}
}
}