Skip to main content

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}