use proc_macro2::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{Attribute, Error, Expr, ExprLit, Lit, Token};
pub struct ParsedReadOnly {
pub cache_policy: TokenStream,
}
pub fn extract_read_only(attrs: &[Attribute]) -> Result<Option<ParsedReadOnly>, Error> {
let mut found: Option<ParsedReadOnly> = None;
for attr in attrs {
if !attr.path().is_ident("read_only") {
continue;
}
let mut cache_policy = quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::UntilWrite
};
if matches!(attr.meta, syn::Meta::Path(_)) {
found = Some(ParsedReadOnly { cache_policy });
continue;
}
let parser = Punctuated::<Expr, Token![,]>::parse_terminated;
let exprs = attr.parse_args_with(parser)?;
let mut explicit_cache: Option<TokenStream> = None;
let mut explicit_ttl: Option<u64> = None;
for expr in exprs.iter() {
if let Expr::Assign(assign) = expr
&& let Expr::Path(left) = &*assign.left
&& let Some(ident) = left.path.get_ident()
{
match ident.to_string().as_str() {
"cache" => {
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) = &*assign.right
{
explicit_cache = Some(parse_cache_value(lit)?);
continue;
} else {
return Err(Error::new_spanned(
&assign.right,
"cache must be a string literal: \"no_cache\", \"until_write\", or \"ttl\"",
));
}
}
"ttl" => {
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) = &*assign.right
{
let duration =
lit.value().parse::<humantime::Duration>().map_err(|e| {
Error::new_spanned(
lit,
format!("invalid ttl duration `{}`: {}", lit.value(), e),
)
})?;
explicit_ttl = Some(duration.as_nanos() as u64);
continue;
} else {
return Err(Error::new_spanned(
&assign.right,
"ttl must be a string literal duration (e.g. \"30s\")",
));
}
}
_ => {}
}
}
return Err(Error::new_spanned(
expr,
"Unknown read_only parameter. Valid parameters are: cache, ttl",
));
}
match (explicit_cache, explicit_ttl) {
(Some(c), None) => {
let c_str = c.to_string();
if c_str.contains("Ttl") {
return Err(Error::new_spanned(
attr,
"cache = \"ttl\" requires a ttl = \"<duration>\" parameter",
));
}
cache_policy = c;
}
(None, Some(nanos)) => {
cache_policy = quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::Ttl(#nanos)
};
}
(Some(c), Some(nanos)) => {
let c_str = c.to_string();
if !c_str.contains("Ttl") {
return Err(Error::new_spanned(
attr,
"ttl parameter is only valid with cache = \"ttl\"",
));
}
cache_policy = quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::Ttl(#nanos)
};
}
(None, None) => {}
}
found = Some(ParsedReadOnly { cache_policy });
}
Ok(found)
}
fn parse_cache_value(lit: &syn::LitStr) -> Result<TokenStream, Error> {
match lit.value().as_str() {
"no_cache" | "no-cache" => Ok(quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::NoCache
}),
"until_write" | "until-write" => Ok(quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::UntilWrite
}),
"ttl" => Ok(quote! {
golem_rust::golem_agentic::golem::agent::common::CachePolicy::Ttl(0)
}),
other => Err(Error::new_spanned(
lit,
format!(
"invalid cache value `{}`. Valid values are: no_cache, until_write, ttl",
other
),
)),
}
}