use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{
Attribute, Error, Expr, ExprCall, ExprLit, ExprMethodCall, ExprPath, ExprStruct, Ident, Lit,
Meta, Path, Token, Type, TypePath, parenthesized, token,
};
pub(crate) struct SubscriberArgs {
pub(crate) source: SourceArg,
pub(crate) publish: Option<PublishArg>,
pub(crate) workers: Option<WorkersArg>,
pub(crate) on_failure: Option<FailureArg>,
pub(crate) start_at: Option<Expr>,
}
pub(crate) enum PublishArg {
Declared,
Default(Expr),
}
pub(crate) enum SourceArg {
OpenName,
OpenKind(Type),
Fixed(Expr),
}
const CLAUSES: &[&str] = &[
"on_failure",
"publish",
"publish_raw",
"start_at",
"workers",
];
fn peeks_clause(input: ParseStream) -> bool {
let fork = input.fork();
let Ok(keyword) = fork.parse::<Ident>() else {
return false;
};
if !CLAUSES.contains(&keyword.to_string().as_str()) {
return false;
}
fork.is_empty() || fork.peek(Token![,]) || fork.peek(token::Paren)
}
pub(crate) struct WorkersArg {
pub(crate) count: Expr,
pub(crate) by_key: Option<Ident>,
}
pub(crate) struct FailureArg {
pub(crate) panic: Option<FailurePolicyArg>,
pub(crate) decode: Option<FailurePolicyArg>,
}
pub(crate) enum FailurePolicyArg {
FailFast,
Drop,
Retry,
RetryAfter(Expr),
Skip,
Value(Expr),
}
impl Parse for FailurePolicyArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let expr: Expr = input.parse()?;
if let Expr::Path(ExprPath {
path, qself: None, ..
}) = &expr
&& let Some(keyword) = path.get_ident()
{
let name = keyword.to_string();
match name.as_str() {
"fail_fast" => return Ok(Self::FailFast),
"drop" => return Ok(Self::Drop),
"retry" => return Ok(Self::Retry),
"skip" => return Ok(Self::Skip),
_ => {}
}
if name.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
return Err(Error::new(
keyword.span(),
"expected `fail_fast`, `drop`, `retry`, `retry_after(<duration>)`, `skip`, \
or a `FailurePolicy` expression (a constant, a function call)",
));
}
}
if let Expr::Call(call) = &expr
&& let Expr::Path(ExprPath {
path, qself: None, ..
}) = &*call.func
&& path.is_ident("retry_after")
{
if call.args.len() != 1 {
return Err(Error::new_spanned(
call,
"retry_after(..) takes exactly one duration argument",
));
}
return Ok(Self::RetryAfter(call.args[0].clone()));
}
Ok(Self::Value(expr))
}
}
impl Parse for FailureArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut panic = None;
let mut decode = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let value: FailurePolicyArg = input.parse()?;
if key == "panic" {
if panic.is_some() {
return Err(Error::new(
key.span(),
"duplicate `panic` in on_failure(..)",
));
}
panic = Some(value);
} else if key == "decode" {
if decode.is_some() {
return Err(Error::new(
key.span(),
"duplicate `decode` in on_failure(..)",
));
}
decode = Some(value);
} else {
return Err(Error::new(
key.span(),
"expected `panic = ..` or `decode = ..`",
));
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
} else {
break;
}
}
if panic.is_none() && decode.is_none() {
return Err(Error::new(
input.span(),
"on_failure(..) needs at least one of `panic = ..` or `decode = ..`",
));
}
Ok(Self { panic, decode })
}
}
impl Parse for SubscriberArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let (source, named_here) = parse_source(input)?;
let mut publish = None;
let mut workers = None;
let mut on_failure = None;
let mut start_at = None;
let mut need_comma = named_here;
while !input.is_empty() {
if need_comma {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
}
need_comma = true;
let keyword: Ident = input.parse()?;
if keyword == "on_failure" {
if on_failure.is_some() {
return Err(Error::new(keyword.span(), "duplicate on_failure(..)"));
}
let content;
parenthesized!(content in input);
on_failure = Some(content.parse()?);
} else if keyword == "publish" {
if publish.is_some() {
return Err(Error::new(keyword.span(), "duplicate publish clause"));
}
publish = Some(parse_publish(input, &keyword)?);
} else if keyword == "publish_raw" {
return Err(Error::new(
keyword.span(),
"publish_raw(..) is retired: the reply's wire follows the reply type - use \
publish(\"dest\") and return a #[derive(Serialized)] type to publish its \
bytes as they are",
));
} else if keyword == "start_at" {
if start_at.is_some() {
return Err(Error::new(keyword.span(), "duplicate start_at(..)"));
}
let content;
parenthesized!(content in input);
if content.is_empty() {
return Err(Error::new(
keyword.span(),
"start_at(..) needs a position constructor; without the clause the \
subscription simply opens at the broker's default",
));
}
start_at = Some(content.parse()?);
} else if keyword == "workers" {
if workers.is_some() {
return Err(Error::new(keyword.span(), "duplicate workers(..)"));
}
let content;
parenthesized!(content in input);
workers = Some(parse_workers(&content)?);
} else {
return Err(Error::new(
keyword.span(),
"expected `publish` / `publish(\"reply-topic\")`, `workers(n[, by_key])`, \
`on_failure(panic = .., decode = ..)`, or `start_at(<position>)`",
));
}
}
Ok(Self {
source,
publish,
workers,
on_failure,
start_at,
})
}
}
fn parse_publish(input: ParseStream, keyword: &Ident) -> syn::Result<PublishArg> {
if !input.peek(token::Paren) {
return Ok(PublishArg::Declared);
}
let content;
parenthesized!(content in input);
if content.is_empty() {
return Err(Error::new(
keyword.span(),
"publish() names nothing: write `publish` to send the reply where its type declares, \
or `publish(\"dest\")` to name the destination of a reply type that declares none",
));
}
Ok(PublishArg::Default(content.parse()?))
}
fn parse_source(input: ParseStream) -> syn::Result<(SourceArg, bool)> {
if input.is_empty() || peeks_clause(input) {
return Ok((SourceArg::OpenName, false));
}
let expr: Expr = input.parse()?;
Ok((source_arg(expr), true))
}
fn source_arg(expr: Expr) -> SourceArg {
match expr {
Expr::Path(ExprPath {
attrs,
qself: None,
path,
}) if attrs.is_empty() => SourceArg::OpenKind(Type::Path(TypePath {
attrs: Vec::new(),
qself: None,
path,
})),
other => SourceArg::Fixed(other),
}
}
fn parse_workers(content: ParseStream) -> syn::Result<WorkersArg> {
let count: Expr = content.parse()?;
let mut by_key = None;
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
let marker: Ident = content.parse()?;
if marker != "by_key" {
return Err(Error::new(
marker.span(),
"expected `by_key`: workers(n) or workers(n, by_key)",
));
}
by_key = Some(marker);
}
Ok(WorkersArg { count, by_key })
}
pub(crate) fn source_tokens(source: &SourceArg) -> syn::Result<(TokenStream2, TokenStream2)> {
let kind = match source {
SourceArg::OpenName => quote!(::ruststream::Name),
SourceArg::OpenKind(ty) => quote!(#ty),
SourceArg::Fixed(expr) => {
if let Expr::Lit(ExprLit {
lit: Lit::Str(name),
..
}) = expr
{
return Ok((
quote!(::ruststream::Name),
quote!(::ruststream::Name::new(#name)),
));
}
let ty = source_type(expr)?;
return Ok((quote!(#ty), quote!(#expr)));
}
};
Ok((
quote!(::ruststream::Unnamed<#kind>),
quote!(::ruststream::Unnamed::<#kind>::new()),
))
}
pub(crate) fn position_type(expr: &Expr) -> syn::Result<Type> {
source_type(expr).map_err(|_| {
Error::new_spanned(
expr,
"expected a position constructor `Type::latest()` / `Type::new(..)` / `Type { .. }`, \
or a builder chain on one - a free function does not expose its type to the macro",
)
})
}
fn source_type(expr: &Expr) -> syn::Result<Type> {
match expr {
Expr::Call(ExprCall { func, .. }) => match &**func {
Expr::Path(ExprPath {
path, qself: None, ..
}) => type_from_constructor_path(path),
_ => Err(unsupported_source(expr)),
},
Expr::Struct(ExprStruct { path, .. }) => Ok(Type::Path(TypePath {
attrs: Vec::new(),
qself: None,
path: path.clone(),
})),
Expr::MethodCall(ExprMethodCall { receiver, .. }) => source_type(receiver),
_ => Err(unsupported_source(expr)),
}
}
fn type_from_constructor_path(path: &Path) -> syn::Result<Type> {
let n = path.segments.len();
if n < 2 {
return Err(Error::new_spanned(
path,
"expected `Type::new(..)`: the path must name a type and an associated constructor - \
a free function does not expose its type to the macro",
));
}
let segments = path.segments.iter().take(n - 1).cloned().collect();
Ok(Type::Path(TypePath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: path.leading_colon,
segments,
},
}))
}
pub(crate) fn publish_result_reply(ty: &Type) -> Option<&Type> {
let Type::Path(TypePath {
qself: None, path, ..
}) = ty
else {
return None;
};
let last = path.segments.last()?;
if last.ident != "Result" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
let mut args = args.args.iter();
let (Some(syn::GenericArgument::Type(ok)), Some(syn::GenericArgument::Type(err)), None) =
(args.next(), args.next(), args.next())
else {
return None;
};
let Type::Path(TypePath {
qself: None,
path: err_path,
..
}) = err
else {
return None;
};
(err_path.segments.last()?.ident == "HandlerOutcome").then_some(ok)
}
pub(crate) fn vec_element(ty: &Type) -> Option<&Type> {
let Type::Path(TypePath {
qself: None, path, ..
}) = ty
else {
return None;
};
let last = path.segments.last()?;
if last.ident != "Vec" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
let mut args = args.args.iter();
let (Some(syn::GenericArgument::Type(elem)), None) = (args.next(), args.next()) else {
return None;
};
Some(elem)
}
fn unsupported_source(expr: &Expr) -> Error {
Error::new_spanned(
expr,
"expected a string literal name, `Type::new(..)`, `Type { .. }`, or a builder chain on \
one of those - a free function does not expose its type to the macro",
)
}
pub(crate) fn doc_description(attrs: &[Attribute]) -> TokenStream2 {
let lines: Vec<String> = attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.filter_map(|attr| match &attr.meta {
Meta::NameValue(nv) => match &nv.value {
Expr::Lit(ExprLit {
lit: Lit::Str(text),
..
}) => Some(text.value().trim().to_owned()),
_ => None,
},
_ => None,
})
.collect();
if lines.is_empty() {
quote!(::core::option::Option::None)
} else {
let joined = lines.join("\n");
quote!(::core::option::Option::Some(#joined))
}
}