hydracache_macros/lib.rs
1use proc_macro::TokenStream;
2use syn::{parse_macro_input, DeriveInput};
3
4mod cacheable;
5mod config;
6mod entity;
7mod paths;
8mod policy;
9mod prepared_policy;
10
11/// Derive `CacheEntity` metadata for database result-cache helpers.
12///
13/// # Example
14///
15/// ```text
16/// use hydracache_db::{CacheEntity, HydraCacheEntity};
17///
18/// #[derive(HydraCacheEntity)]
19/// #[hydracache(entity = "user", collection = "users")]
20/// struct User {
21/// #[hydracache(id)]
22/// id: i64,
23/// name: String,
24/// }
25///
26/// assert_eq!(User::cache_key_for(&42), "user:42");
27/// ```
28///
29/// `#[hydracache(id = Type)]` on the struct remains supported when the id type
30/// cannot be inferred from one named field.
31#[proc_macro_derive(HydraCacheEntity, attributes(hydracache))]
32pub fn derive_hydracache_entity(input: TokenStream) -> TokenStream {
33 entity::expand(parse_macro_input!(input as DeriveInput))
34 .unwrap_or_else(syn::Error::into_compile_error)
35 .into()
36}
37
38/// Build a `QueryCachePolicy` with less boilerplate.
39///
40/// # Example
41///
42/// ```text
43/// use hydracache_db::query_cache_policy;
44///
45/// let user_id = 42_i64;
46/// let policy = query_cache_policy!(
47/// preset = read_mostly,
48/// name = "load-user",
49/// entity = User,
50/// id = user_id,
51/// refresh_ahead_secs = 10,
52/// stale_while_revalidate_secs = 300,
53/// );
54///
55/// let search = query_cache_policy!(
56/// name = "search-users",
57/// key_segments = ["tenant", tenant_id, "q", query, "page", page],
58/// tag_segments = [["tenant", tenant_id], ["users"]],
59/// ttl_secs = 30,
60/// );
61/// ```
62#[proc_macro]
63pub fn query_cache_policy(input: TokenStream) -> TokenStream {
64 policy::expand(input.into())
65 .unwrap_or_else(syn::Error::into_compile_error)
66 .into()
67}
68
69/// Build a reusable `PreparedQueryPolicy` with less boilerplate.
70///
71/// # Example
72///
73/// ```text
74/// use hydracache_db::prepared_query_policy;
75///
76/// let load_user = prepared_query_policy!(
77/// per_entity = User,
78/// name = "load-user",
79/// ttl_secs = 300,
80/// );
81/// ```
82#[proc_macro]
83pub fn prepared_query_policy(input: TokenStream) -> TokenStream {
84 prepared_policy::expand(input.into())
85 .unwrap_or_else(syn::Error::into_compile_error)
86 .into()
87}
88
89/// Cache an ordinary async function with explicit local-cache metadata.
90///
91/// The decorated function must be async, return `Result<T, E>`, and receive the
92/// cache as an explicit argument referenced by `cache = ...`. The generated
93/// wrapper returns `hydracache::CacheResult<T>` because cache errors can also be
94/// produced outside the user loader.
95///
96/// # Example
97///
98/// ```text
99/// use hydracache::{cacheable, HydraCache};
100///
101/// #[cacheable(
102/// cache = cache,
103/// key_segments = ["profile", profile_id],
104/// tag_segments = [["profile", profile_id], ["profiles"]],
105/// ttl_secs = 60
106/// )]
107/// async fn load_profile(
108/// cache: &HydraCache,
109/// profile_id: u64,
110/// ) -> Result<Profile, LoadError> {
111/// repo_load_profile(profile_id).await
112/// }
113/// ```
114#[proc_macro_attribute]
115pub fn cacheable(args: TokenStream, item: TokenStream) -> TokenStream {
116 cacheable::expand_attribute(args.into(), item.into())
117 .unwrap_or_else(syn::Error::into_compile_error)
118 .into()
119}
120
121/// Cache an ordinary fallible async loader with explicit local-cache metadata.
122///
123/// The macro builds `CacheOptions` and calls `HydraCache::get_or_load`.
124/// `cache`, `key`, and `load` are required. `tag = ...` can be repeated,
125/// `tags = ...` accepts any iterable accepted by `CacheOptions::tags`, and
126/// either `ttl = Duration` or `ttl_secs = u64` can be supplied.
127///
128/// # Example
129///
130/// ```text
131/// use hydracache::{cacheable_loader, CacheKeyBuilder, HydraCache, TagSet};
132///
133/// let cache = HydraCache::local().build();
134/// let user_id = 42_u64;
135/// let key = CacheKeyBuilder::new().entity("user", user_id).build_string();
136///
137/// let value = cacheable_loader!(
138/// cache = cache,
139/// key = key.as_str(),
140/// tags = TagSet::new().tag("users").entity("user", user_id),
141/// ttl_secs = 60,
142/// load = move || async move { Ok::<_, std::io::Error>(user_id) },
143/// )
144/// .await?;
145/// ```
146#[proc_macro]
147pub fn cacheable_loader(input: TokenStream) -> TokenStream {
148 cacheable::expand_loader(input.into())
149 .unwrap_or_else(syn::Error::into_compile_error)
150 .into()
151}
152
153/// Cache an ordinary async loader that cannot fail in application terms.
154///
155/// The macro builds `CacheOptions` and calls `HydraCache::get_or_insert_with`.
156/// Use it when the loader returns `T` instead of `Result<T, E>`.
157///
158/// # Example
159///
160/// ```text
161/// use hydracache::{cacheable_infallible, HydraCache};
162///
163/// let cache = HydraCache::local().build();
164///
165/// let value = cacheable_infallible!(
166/// cache = cache,
167/// key = "expensive:42",
168/// tags = ["expensive"],
169/// ttl_secs = 60,
170/// load = || async { 42_u64 },
171/// )
172/// .await?;
173/// ```
174#[proc_macro]
175pub fn cacheable_infallible(input: TokenStream) -> TokenStream {
176 cacheable::expand_infallible(input.into())
177 .unwrap_or_else(syn::Error::into_compile_error)
178 .into()
179}