use syn::parse::ParseStream;
use super::keywords::option as keyword;
use super::list;
use super::type_name;
#[derive(Debug, Default)]
pub struct RouteOptions {
pub name: Option<String>,
pub pages: Vec<String>,
pub action: Option<syn::Path>,
pub query: Option<syn::Type>,
pub query_string: Option<syn::Path>,
pub policies: Vec<String>,
}
pub fn parse(input: ParseStream<'_>) -> syn::Result<RouteOptions> {
if !input.peek(syn::token::Brace) {
return Ok(RouteOptions::default());
}
let content;
syn::braced!(content in input);
let mut options = RouteOptions::default();
let mut saw_page = false;
let mut saw_pages = false;
let mut saw_policy = false;
let mut saw_policies = false;
while !content.is_empty() {
let lookahead = content.lookahead1();
if lookahead.peek(keyword::name) {
let _: keyword::name = content.parse()?;
let _: syn::Token![:] = content.parse()?;
options.name = Some(list::dotted_name(&content)?);
} else if lookahead.peek(keyword::page) {
if saw_pages {
return Err(both_page_forms(content.span()));
}
let _: keyword::page = content.parse()?;
let _: syn::Token![:] = content.parse()?;
let lit: syn::LitStr = content.parse()?;
options.pages.push(lit.value());
saw_page = true;
} else if lookahead.peek(keyword::pages) {
if saw_page {
return Err(both_page_forms(content.span()));
}
let _: keyword::pages = content.parse()?;
let _: syn::Token![:] = content.parse()?;
options.pages = list::strings(&content)?;
if options.pages.is_empty() {
return Err(syn::Error::new(
content.span(),
"`pages:` must list at least one page, e.g. `pages: [\"Home\"]`",
));
}
saw_pages = true;
} else if lookahead.peek(keyword::action) {
if options.query.is_some() {
return Err(both_action_and_query(content.span()));
}
let _: keyword::action = content.parse()?;
let _: syn::Token![:] = content.parse()?;
options.action = Some(content.parse()?);
} else if lookahead.peek(keyword::query) {
if options.action.is_some() {
return Err(both_action_and_query(content.span()));
}
let _: keyword::query = content.parse()?;
let _: syn::Token![:] = content.parse()?;
options.query = Some(content.parse()?);
} else if lookahead.peek(keyword::query_string) {
if options.query.is_none() {
return Err(syn::Error::new(
content.span(),
"`query_string:` requires `query:` on the same route \
(a query-string contract is a query route's input)",
));
}
let _: keyword::query_string = content.parse()?;
let _: syn::Token![:] = content.parse()?;
options.query_string = Some(content.parse()?);
} else if lookahead.peek(keyword::policy) {
if saw_policies {
return Err(both_policy_forms(content.span()));
}
let _: keyword::policy = content.parse()?;
let _: syn::Token![:] = content.parse()?;
let path: syn::Path = content.parse()?;
options.policies.push(type_name::final_segment(&path));
saw_policy = true;
} else if lookahead.peek(keyword::policies) {
if saw_policy {
return Err(both_policy_forms(content.span()));
}
let _: keyword::policies = content.parse()?;
let _: syn::Token![:] = content.parse()?;
let paths = list::paths(&content)?;
if paths.is_empty() {
return Err(syn::Error::new(
content.span(),
"`policies:` must list at least one policy, e.g. `policies: [LinkPolicy]`",
));
}
options.policies = paths.iter().map(type_name::final_segment).collect();
saw_policies = true;
} else {
return Err(lookahead.error());
}
let _: Option<syn::Token![,]> = content.parse()?;
}
Ok(options)
}
fn both_page_forms(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(span, "a route may declare `page:` or `pages:`, not both")
}
fn both_policy_forms(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(
span,
"a route may declare `policy:` or `policies:`, not both",
)
}
fn both_action_and_query(span: proc_macro2::Span) -> syn::Error {
syn::Error::new(span, "a route may declare `action:` or `query:`, not both")
}
#[cfg(test)]
mod tests {
use super::parse;
use syn::parse::Parser as _;
#[test]
fn no_block_yields_defaults() {
let options = parse.parse2(quote::quote!()).unwrap();
assert!(options.name.is_none());
assert!(options.pages.is_empty());
}
#[test]
fn name_page_and_action_are_parsed() {
let options = parse
.parse2(quote::quote!({ name: links.store, page: "Links/Create", action: StoreLinkRequest }))
.unwrap();
assert_eq!(options.name.as_deref(), Some("links.store"));
assert_eq!(options.pages, vec!["Links/Create".to_string()]);
assert!(options.action.is_some());
}
#[test]
fn pages_list_is_parsed() {
let options = parse
.parse2(quote::quote!({ pages: ["Home", "Welcome"] }))
.unwrap();
assert_eq!(options.pages.len(), 2);
}
#[test]
fn page_and_pages_together_are_rejected() {
let error = parse
.parse2(quote::quote!({ page: "Home", pages: ["Home"] }))
.unwrap_err();
assert!(error.to_string().contains("not both"));
}
#[test]
fn empty_pages_list_is_rejected() {
let error = parse.parse2(quote::quote!({ pages: [] })).unwrap_err();
assert!(error.to_string().contains("at least one page"));
}
#[test]
fn action_and_query_together_are_rejected() {
let error = parse
.parse2(quote::quote!({ action: StoreLink, query: LinkResource }))
.unwrap_err();
assert!(error.to_string().contains("not both"));
}
#[test]
fn query_string_without_query_is_rejected() {
let error = parse
.parse2(quote::quote!({ query_string: SearchRequest }))
.unwrap_err();
assert!(error.to_string().contains("requires `query:`"));
}
#[test]
fn query_string_with_query_is_accepted() {
let options = parse
.parse2(quote::quote!({ query: Vec<LinkResource>, query_string: SearchRequest }))
.unwrap();
assert!(options.query.is_some());
assert!(options.query_string.is_some());
}
#[test]
fn an_unknown_option_is_rejected() {
let error = parse.parse2(quote::quote!({ nope: 1 })).unwrap_err();
assert!(!error.to_string().is_empty());
}
}