Skip to content

Support &T and Option<&T> in input#203

Open
BaxHugh wants to merge 2 commits into
jaemk:masterfrom
BaxHugh:feature/support-ref-types-in-input
Open

Support &T and Option<&T> in input#203
BaxHugh wants to merge 2 commits into
jaemk:masterfrom
BaxHugh:feature/support-ref-types-in-input

Conversation

@BaxHugh

@BaxHugh BaxHugh commented Apr 9, 2024

Copy link
Copy Markdown
Contributor

Changed some of the macro code so that

#[cached]
fn my_func(a: &T, b: Option<&T>) -> U;

works, currently only inputs of owned types works.

Closes #202.

I've now implemented this but since then I've got some other thought. Which could affect whether we want this in the long run?

Cons to having this feature:

  • There's probably lots of edge cases it doesn't support at the token parsing level (but at least most simple ref types are supported)
  • It adds a bit more complexity to macro code.

Why the feature is / might not be needed.

We're passing a reference with the expectation that it will be cloned within the body of the function. Instead why not just have users use functions which take owned data and omit any extra cloning in the impl body? If the input data needs keeping, then it would be cloned and passed to the function.
This would remove the need for additional logic to use InputT as the key type when the input type is &InputT, and would defer dealing with refs and cloning to the library user.

The alternative implementation we should/cloud instead provide.

Currently the implementation provided by the cache_proc macro calls clone() on the inputs to create the key.
for simple uses of the #[cached] macro (without complexity from other macro args) this clone is needless, as there is no later borrow, so the key could consume the inputs instead.

Removing the call to clone() I think only breaks the case when the with_cached_flag is used in the tests so in that case, an alternative impl could be provided, (a cleaner way of implementing the proc macro could help here)

Further refactoring elsewhere could make this simpler, i.e. if the cache_set method could take a &Key instead of a Key, then we wouldn't need any extra logic to support ref inputs, i.e. they key type could be a ref type (maybe?)

Conclusion given the short term

I think for now, given the current implementation, supporting the inputs being references is a good idea. If the user want to maintain ownership of the function's inputs, then passing a ref makes sense and it's one less clone that needs doing for each input (i.e. fn foo(bar_arg = bar.clone()) { ... bar_arg.clone(); ... } is wasteful.

Going forward, if there is every a refactor, it would be ideal to remove the need for cloning within the generated function body, this might look like something involving cache_set taking a &Key or by maybe just only supporting owned inputs which are consumed and not cloned more than they need to be. In the later case where the cache function must consume the inputs, then I think it would be best not to support ref inputs, and have the user clone() on their side where they need it.

@BaxHugh

BaxHugh commented Apr 9, 2024

Copy link
Copy Markdown
Contributor Author

Note: I think #196 seems relevant

@BaxHugh BaxHugh force-pushed the feature/support-ref-types-in-input branch from 16d7d23 to de3285c Compare April 26, 2024 09:14
BaxHugh added 2 commits May 20, 2024 15:34
- make_cache_key_type converts keys of Option<&T> to Option<T>
- Use the __private ToFullyOwned trait in generated code impl
@BaxHugh BaxHugh force-pushed the feature/support-ref-types-in-input branch from de3285c to 5b28035 Compare May 20, 2024 14:34
jaemk added a commit that referenced this pull request Jun 9, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; the redis/disk macro paths use the borrowed setter
  to avoid an extra clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release on publish (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 9, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; the redis/disk macro paths use the borrowed setter
  to avoid an extra clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release on publish (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 9, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; the redis/disk macro paths use the borrowed setter
  to avoid an extra clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release on publish (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 9, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; the redis/disk macro paths use the borrowed setter
  to avoid an extra clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 10, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; the redis/disk macro paths use the borrowed setter
  to avoid an extra clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 10, 2026
…ext major)

Second tranche of the next major (after the redb DiskCache rewrite). Bundles a
set of breaking and additive changes.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245)
- fix a doctest under `--no-default-features` (#260)

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 10, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat) (#146)
- `in_impl` attribute to cache methods inside `impl` blocks / `self` receivers (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; `in_impl` emits a same-visibility `{fn}_no_cache`
  cache-bypass sibling; wasm is incompatible with `redis_connection_manager` /
  `redis_async_cache`; Redis whole-second `ttl_millis` rounding; the `in_impl` shared-cache
  footgun; the async `V: Sync` clone-elision asymmetry; `CachedAsync` shared-ref default
  Send bounds; migration-guide `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh`; `#[once]` `force_refresh`; generic `in_impl`
  rejection golden; `TtlSortedCache` shared-ref get-or-set delegation

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 10, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149);
  rejected alongside a `create` block like the other store-builder attributes
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat) (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a same-visibility
  `{fn}_no_cache` cache-bypass sibling (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear` (#200)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache (#180)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236)
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; on `#[cached]` the "honored exactly" claim is
  scoped to the default in-memory store; Redis rounds `ttl_millis` up to the next whole
  second (500ms -> 1s, 1500ms -> 2s); `in_impl` emits a same-visibility `{fn}_no_cache`
  sibling (documented on all three macros); `#[concurrent_cached]` `force_refresh`
  documents the `result_fallback` interaction; wasm is incompatible with
  `redis_connection_manager` / `redis_async_cache`; the `in_impl` shared-cache footgun;
  the async `V: Sync` clone-elision asymmetry, including how a `Send + !Sync + !Clone`
  value surfaces at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh`; `#[once]` `force_refresh`; `force_refresh` +
  `in_impl`; generic `in_impl` rejection golden; compile-fail goldens for
  `force_refresh`-unparseable on `#[once]` / `#[concurrent_cached]`, generic
  `#[concurrent_cached]` without `convert`, and `ttl_millis` + `create` conflict;
  `TtlSortedCache` shared-ref get-or-set delegation
- macro-output polish: no dead `#[doc]` on the function-local `in_impl` cache static;
  documented rationale for `#[allow(dead_code)]` on the generated prime companion and
  for the `crate_path()` `::cached` fallback

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 11, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149);
  rejected alongside a `create` block like the other store-builder attributes
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl`
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- dev tooling: pr-review shards reviewers across model-sized, randomized chunks
  (multiple of each type); pr-cycle fans fix application across disjoint sub-agents;
  AGENTS.md states README is generated from src/lib.rs via cargo-readme
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; on `#[cached]` the "honored exactly" claim is
  scoped to the default in-memory store; Redis rounds `ttl_millis` up to the next whole
  second (500ms -> 1s, 1500ms -> 2s); `in_impl` emits a `#[doc(hidden)]` `{fn}_no_cache`
  sibling (documented on all three macros); `#[concurrent_cached]` `force_refresh`
  documents the `result_fallback` interaction; the `#[concurrent_cached]` attribute table
  reaches parity with `#[cached]` (ttl_millis / force_refresh / in_impl rows); wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the `in_impl`
  shared-cache footgun; the async `V: Sync` clone-elision asymmetry, including how a
  `Send + !Sync + !Clone` value surfaces at the generated set site; `CachedAsync`
  shared-ref default Send bounds; `Cached::cache_get_or_set_with` /
  `cache_try_get_or_set_with` flagged as provided defaults; migration-guide
  `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass); `#[once]`
  `force_refresh`; `force_refresh` + `in_impl`; generic `in_impl` rejection goldens for
  `#[cached]` (free fn) and `#[concurrent_cached]` (in_impl method); compile-fail goldens
  for `force_refresh`-unparseable on `#[once]` / `#[concurrent_cached]`, generic
  `#[concurrent_cached]` without `convert`, and `ttl_millis` + `create` conflict;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; `TtlSortedCache` shared-ref
  get-or-set delegation
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 11, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149);
  rejected alongside a `create` block like the other store-builder attributes
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl`
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- dev tooling: pr-review shards reviewers across model-sized, randomized chunks
  (multiple of each type); pr-cycle fans fix application across disjoint sub-agents;
  AGENTS.md states README is generated from src/lib.rs via cargo-readme
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; on `#[cached]` the "honored exactly" claim is
  scoped to the default in-memory store; Redis rounds `ttl_millis` up to the next whole
  second (500ms -> 1s, 1500ms -> 2s); `in_impl` emits a `#[doc(hidden)]` `{fn}_no_cache`
  sibling (documented on all three macros); `#[concurrent_cached]` `force_refresh`
  documents the `result_fallback` interaction; the `#[concurrent_cached]` attribute table
  reaches parity with `#[cached]` (ttl_millis / force_refresh / in_impl rows); wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the `in_impl`
  shared-cache footgun; the async `V: Sync` clone-elision asymmetry, including how a
  `Send + !Sync + !Clone` value surfaces at the generated set site; `CachedAsync`
  shared-ref default Send bounds; `Cached::cache_get_or_set_with` /
  `cache_try_get_or_set_with` flagged as provided defaults; migration-guide
  `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass); `#[once]`
  `force_refresh`; `force_refresh` + `in_impl`; generic `in_impl` rejection goldens for
  `#[cached]` (free fn) and `#[concurrent_cached]` (in_impl method); compile-fail goldens
  for `force_refresh`-unparseable on `#[once]` / `#[concurrent_cached]`, generic
  `#[concurrent_cached]` without `convert`, and `ttl_millis` + `create` conflict;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; `TtlSortedCache` shared-ref
  get-or-set delegation
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 11, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149);
  rejected alongside a `create` block like the other store-builder attributes
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl`
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- dev tooling: pr-review shards reviewers across model-sized, randomized chunks
  (multiple of each type); pr-cycle fans fix application across disjoint sub-agents;
  AGENTS.md states README is generated from src/lib.rs via cargo-readme
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; on `#[cached]` the "honored exactly" claim is
  scoped to the default in-memory store; Redis rounds `ttl_millis` up to the next whole
  second (500ms -> 1s, 1500ms -> 2s); `in_impl` emits a `#[doc(hidden)]` `{fn}_no_cache`
  sibling (documented on all three macros); `#[concurrent_cached]` `force_refresh`
  documents the `result_fallback` interaction; the `#[concurrent_cached]` attribute table
  reaches parity with `#[cached]` (ttl_millis / force_refresh / in_impl rows); wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the `in_impl`
  shared-cache footgun; the async `V: Sync` clone-elision asymmetry, including how a
  `Send + !Sync + !Clone` value surfaces at the generated set site; `CachedAsync`
  shared-ref default Send bounds; `Cached::cache_get_or_set_with` /
  `cache_try_get_or_set_with` flagged as provided defaults; migration-guide
  `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass); `#[once]`
  `force_refresh`; `force_refresh` + `in_impl`; generic `in_impl` rejection goldens for
  `#[cached]` (free fn) and `#[concurrent_cached]` (in_impl method); compile-fail goldens
  for `force_refresh`-unparseable on `#[once]` / `#[concurrent_cached]`, generic
  `#[concurrent_cached]` without `convert`, and `ttl_millis` + `create` conflict;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; `TtlSortedCache` shared-ref
  get-or-set delegation
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 11, 2026
…ext major)

Next-major batch (after the redb DiskCache rewrite). Bundles a set of
breaking and additive changes plus the doc/test follow-ups.

Macros:
- `ttl_millis` attribute for sub-second TTLs, mutually exclusive with `ttl` (#149);
  rejected alongside a `create` block like the other store-builder attributes
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl`
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- dev tooling: pr-review shards reviewers across model-sized, randomized chunks
  (multiple of each type); pr-cycle fans fix application across disjoint sub-agents;
  AGENTS.md states README is generated from src/lib.rs via cargo-readme
- documentation accuracy pass: `ttl_millis` is valid with `result_fallback` and (on the
  in-memory path) needs `time_stores`; on `#[cached]` the "honored exactly" claim is
  scoped to the default in-memory store; Redis rounds `ttl_millis` up to the next whole
  second (500ms -> 1s, 1500ms -> 2s); `in_impl` emits a `#[doc(hidden)]` `{fn}_no_cache`
  sibling (documented on all three macros); `#[concurrent_cached]` `force_refresh`
  documents the `result_fallback` interaction; the `#[concurrent_cached]` attribute table
  reaches parity with `#[cached]` (ttl_millis / force_refresh / in_impl rows); wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the `in_impl`
  shared-cache footgun; the async `V: Sync` clone-elision asymmetry, including how a
  `Send + !Sync + !Clone` value surfaces at the generated set site; `CachedAsync`
  shared-ref default Send bounds; `Cached::cache_get_or_set_with` /
  `cache_try_get_or_set_with` flagged as provided defaults; migration-guide
  `&mut V` -> `&V` coercion caveat
- added coverage: async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass); `#[once]`
  `force_refresh`; `force_refresh` + `in_impl`; generic `in_impl` rejection goldens for
  `#[cached]` (free fn) and `#[concurrent_cached]` (in_impl method); compile-fail goldens
  for `force_refresh`-unparseable on `#[once]` / `#[concurrent_cached]`, generic
  `#[concurrent_cached]` without `convert`, and `ttl_millis` + `create` conflict;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; `TtlSortedCache` shared-ref
  get-or-set delegation
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 13, 2026
…or + ttl consistency

Next-major batch (after the redb DiskCache rewrite). Bundles a set of breaking and
additive changes plus the doc/test follow-ups, including a consumer-experience pass
that made constructors and the ttl attribute consistent across the public surface.

Macros:
- `ttl` attribute now takes a `Duration` expression
  (`ttl = "core::time::Duration::from_secs(60)"`); the old whole-seconds integer
  form (`ttl = 60`) is removed and emits a migration error pointing at
  `ttl_secs`/`ttl_millis`
- `ttl_secs` attribute for whole-second TTLs and `ttl_millis` for sub-second TTLs;
  `ttl` / `ttl_secs` / `ttl_millis` are three-way mutually exclusive, and each is
  mutually exclusive with `expires` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- `refresh` is rejected alongside the other store-builder attributes when a
  `create` block is supplied, mirroring `#[concurrent_cached]`
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- every in-memory and sharded store gains a `new()` that returns a ready-to-use
  cache (zero-config stores take no args; required-field stores take them
  positionally - `LruCache::new(max_size)`, `TtlCache::new(ttl)`,
  `LruTtlCache::new(max_size, ttl)`, the `Sharded*` variants, etc.), all
  `#[must_use]`. `RedbCache` / `RedisCache` / `AsyncRedisCache` drop `new()` (it
  returned a builder, not a cache); use `builder(...)`. `new()` now consistently
  returns a usable cache everywhere it exists
- every TTL builder (non-sharded, sharded, Redis, Redb) gains `ttl_secs` /
  `ttl_millis` convenience setters alongside `ttl(Duration)`; last-writer-wins
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- docs and examples now prefer the short alias method API
  (`get`/`set`/`remove`/`clear`/`len`/...) over the `cache_*`-prefixed forms, with
  a note (in the crate docs and on the `Cached` trait) on when to use the `cache_*`
  names - a collision with another in-scope trait's method. Both forms remain; no
  method was removed
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl` and
  noting the in_impl cache is not externally invalidatable and its shared-id
  staleness footgun
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); it retries a missing release when the tag
  already exists on the remote; the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- README is generated from src/lib.rs via cargo-readme (regenerated here); dev
  tooling: pr-review shards reviewers across model-sized randomized chunks; pr-cycle
  fans fix application across disjoint sub-agents
- documentation accuracy pass: `ttl_millis` / `ttl_secs` are valid with
  `result_fallback` and (on the in-memory path) need `time_stores`; on `#[cached]`
  the "honored exactly" claim is scoped to the default in-memory store; Redis rounds
  `ttl_millis` up to the next whole second (500ms -> 1s, 1500ms -> 2s); `in_impl`
  emits a `#[doc(hidden)]` `{fn}_no_cache` sibling (all three macros);
  `#[concurrent_cached]` `force_refresh` documents the `result_fallback` interaction;
  the `#[concurrent_cached]` attribute table reaches parity with `#[cached]`; wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the async
  `V: Sync` clone-elision asymmetry and how a `Send + !Sync + !Clone` value surfaces
  at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat; redis `build()` lists
  `InvalidTtl` before `EmptyScope` to match validation order
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback

Tests:
- positive macro coverage for all three ttl spellings (`ttl` Duration expr /
  `ttl_secs` / `ttl_millis`) across `#[cached]` / `#[once]` / `#[concurrent_cached]`,
  and compile-fail UI goldens for the three-way exclusivity and the `ttl = <int>`
  migration error
- inline store coverage for every new `new()` constructor and the `ttl_secs` /
  `ttl_millis` builder setters (including override semantics)
- async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass);
  `#[once]` `force_refresh`; `force_refresh` + `in_impl`; generic-rejection goldens;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; deferred attribute/store
  combinations (max_size+ttl_millis, in_impl+ttl_millis, ttl_millis+force_refresh,
  result_fallback+ttl_millis, async once in_impl) and store(0) counter resets;
  sharded peek does not renew TTL under refresh_on_hit

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 14, 2026
…or + ttl consistency

Next-major batch (after the redb DiskCache rewrite). Bundles a set of breaking and
additive changes plus the doc/test follow-ups, including a consumer-experience pass
that made constructors and the ttl attribute consistent across the public surface.

Macros:
- `ttl` attribute now takes a `Duration` expression
  (`ttl = "core::time::Duration::from_secs(60)"`); the old whole-seconds integer
  form (`ttl = 60`) is removed and emits a migration error pointing at
  `ttl_secs`/`ttl_millis`
- `ttl_secs` attribute for whole-second TTLs and `ttl_millis` for sub-second TTLs;
  `ttl` / `ttl_secs` / `ttl_millis` are three-way mutually exclusive, and each is
  mutually exclusive with `expires` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- `refresh` is rejected alongside the other store-builder attributes when a
  `create` block is supplied, mirroring `#[concurrent_cached]`
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- every in-memory and sharded store gains a `new()` that returns a ready-to-use
  cache (zero-config stores take no args; required-field stores take them
  positionally - `LruCache::new(max_size)`, `TtlCache::new(ttl)`,
  `LruTtlCache::new(max_size, ttl)`, the `Sharded*` variants, etc.), all
  `#[must_use]`. `RedbCache` / `RedisCache` / `AsyncRedisCache` drop `new()` (it
  returned a builder, not a cache); use `builder(...)`. `new()` now consistently
  returns a usable cache everywhere it exists
- `new()` / `builder()` on each sharded `*Base` type are now defined only on the
  default-hasher specialization (`*Base<K, V, DefaultShardHasher>`, the named
  alias). They previously sat on the generic `*Base<K, V, H>` impl but always
  returned a `DefaultShardHasher` builder, so a `*Base::<_, _, CustomHasher>::
  builder()` / `::new()` turbofish compiled yet silently dropped the custom
  hasher; that turbofish now fails to compile (E0599). A custom hasher is
  specified via `ShardedX::builder().hasher(h)`, which switches the builder's
  hasher type; alias-based construction and the `.hasher()` path are unchanged
- every TTL builder (non-sharded, sharded, Redis, Redb) gains `ttl_secs` /
  `ttl_millis` convenience setters alongside `ttl(Duration)`; last-writer-wins
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- docs and examples now prefer the short alias method API
  (`get`/`set`/`remove`/`clear`/`len`/...) over the `cache_*`-prefixed forms, with
  a note (in the crate docs and on the `Cached` trait) on when to use the `cache_*`
  names - a collision with another in-scope trait's method. Both forms remain; no
  method was removed
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl` and
  noting the in_impl cache is not externally invalidatable and its shared-id
  staleness footgun
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); it retries a missing release when the tag
  already exists on the remote; the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- README is generated from src/lib.rs via cargo-readme (regenerated here); dev
  tooling: pr-review shards reviewers across model-sized randomized chunks; pr-cycle
  fans fix application across disjoint sub-agents
- documentation accuracy pass: `ttl_millis` / `ttl_secs` are valid with
  `result_fallback` and (on the in-memory path) need `time_stores`; on `#[cached]`
  the "honored exactly" claim is scoped to the default in-memory store; Redis rounds
  `ttl_millis` up to the next whole second (500ms -> 1s, 1500ms -> 2s); `in_impl`
  emits a `#[doc(hidden)]` `{fn}_no_cache` sibling (all three macros);
  `#[concurrent_cached]` `force_refresh` documents the `result_fallback` interaction;
  the `#[concurrent_cached]` attribute table reaches parity with `#[cached]`; wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the async
  `V: Sync` clone-elision asymmetry and how a `Send + !Sync + !Clone` value surfaces
  at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat; redis `build()` lists
  `InvalidTtl` before `EmptyScope` to match validation order
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback
- elevate all compiler warnings to errors workspace-wide via the Cargo `[lints]`
  table (`[workspace.lints.rust] warnings = "deny"`, opted into by the `cached`
  and `cached_proc_macro` packages), so `cargo build`/`test`/`clippy` deny
  warnings rather than only the Makefile clippy target; gate `validate_ttl` to
  `any(time_stores, disk_store, redis_store)` and an unused test `AtomicUsize`
  import to `proc_macro` to keep `--no-default-features` warning-clean

Tests:
- positive macro coverage for all three ttl spellings (`ttl` Duration expr /
  `ttl_secs` / `ttl_millis`) across `#[cached]` / `#[once]` / `#[concurrent_cached]`,
  and compile-fail UI goldens for the three-way exclusivity and the `ttl = <int>`
  migration error
- inline store coverage for every new `new()` constructor and the `ttl_secs` /
  `ttl_millis` builder setters (including override semantics)
- compile-fail UI golden asserting `*Base::<_, _, CustomHasher>::{new,builder}()`
  no longer compiles (the custom-hasher `.hasher()` path stays covered by the
  existing sharded `custom_hasher` unit test)
- async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass);
  `#[once]` `force_refresh`; `force_refresh` + `in_impl`; generic-rejection goldens;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; deferred attribute/store
  combinations (max_size+ttl_millis, in_impl+ttl_millis, ttl_millis+force_refresh,
  result_fallback+ttl_millis, async once in_impl) and store(0) counter resets;
  sharded peek does not renew TTL under refresh_on_hit

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 20, 2026
…or + ttl consistency

Next-major batch (after the redb DiskCache rewrite). Bundles a set of breaking and
additive changes plus the doc/test follow-ups, including a consumer-experience pass
that made constructors and the ttl attribute consistent across the public surface.

Macros:
- `ttl` attribute now takes a `Duration` expression
  (`ttl = "core::time::Duration::from_secs(60)"`); the old whole-seconds integer
  form (`ttl = 60`) is removed and emits a migration error pointing at
  `ttl_secs`/`ttl_millis`
- `ttl_secs` attribute for whole-second TTLs and `ttl_millis` for sub-second TTLs;
  `ttl` / `ttl_secs` / `ttl_millis` are three-way mutually exclusive, and each is
  mutually exclusive with `expires` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- `refresh` is rejected alongside the other store-builder attributes when a
  `create` block is supplied, mirroring `#[concurrent_cached]`
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- every in-memory and sharded store gains a `new()` that returns a ready-to-use
  cache (zero-config stores take no args; required-field stores take them
  positionally - `LruCache::new(max_size)`, `TtlCache::new(ttl)`,
  `LruTtlCache::new(max_size, ttl)`, the `Sharded*` variants, etc.), all
  `#[must_use]`. `RedbCache` / `RedisCache` / `AsyncRedisCache` drop `new()` (it
  returned a builder, not a cache); use `builder(...)`. `new()` now consistently
  returns a usable cache everywhere it exists
- `new()` / `builder()` on each sharded `*Base` type are now defined only on the
  default-hasher specialization (`*Base<K, V, DefaultShardHasher>`, the named
  alias). They previously sat on the generic `*Base<K, V, H>` impl but always
  returned a `DefaultShardHasher` builder, so a `*Base::<_, _, CustomHasher>::
  builder()` / `::new()` turbofish compiled yet silently dropped the custom
  hasher; that turbofish now fails to compile (E0599). A custom hasher is
  specified via `ShardedX::builder().hasher(h)`, which switches the builder's
  hasher type; alias-based construction and the `.hasher()` path are unchanged
- every TTL builder (non-sharded, sharded, Redis, Redb) gains `ttl_secs` /
  `ttl_millis` convenience setters alongside `ttl(Duration)`; last-writer-wins
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- docs and examples now prefer the short alias method API
  (`get`/`set`/`remove`/`clear`/`len`/...) over the `cache_*`-prefixed forms, with
  a note (in the crate docs and on the `Cached` trait) on when to use the `cache_*`
  names - a collision with another in-scope trait's method. Both forms remain; no
  method was removed
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl` and
  noting the in_impl cache is not externally invalidatable and its shared-id
  staleness footgun
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); it retries a missing release when the tag
  already exists on the remote; the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- README is generated from src/lib.rs via cargo-readme (regenerated here); dev
  tooling: pr-review shards reviewers across model-sized randomized chunks; pr-cycle
  fans fix application across disjoint sub-agents
- documentation accuracy pass: `ttl_millis` / `ttl_secs` are valid with
  `result_fallback` and (on the in-memory path) need `time_stores`; on `#[cached]`
  the "honored exactly" claim is scoped to the default in-memory store; Redis rounds
  `ttl_millis` up to the next whole second (500ms -> 1s, 1500ms -> 2s); `in_impl`
  emits a `#[doc(hidden)]` `{fn}_no_cache` sibling (all three macros);
  `#[concurrent_cached]` `force_refresh` documents the `result_fallback` interaction;
  the `#[concurrent_cached]` attribute table reaches parity with `#[cached]`; wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the async
  `V: Sync` clone-elision asymmetry and how a `Send + !Sync + !Clone` value surfaces
  at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat; redis `build()` lists
  `InvalidTtl` before `EmptyScope` to match validation order
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback
- elevate all compiler warnings to errors workspace-wide via the Cargo `[lints]`
  table (`[workspace.lints.rust] warnings = "deny"`, opted into by the `cached`
  and `cached_proc_macro` packages), so `cargo build`/`test`/`clippy` deny
  warnings rather than only the Makefile clippy target; gate `validate_ttl` to
  `any(time_stores, disk_store, redis_store)` and an unused test `AtomicUsize`
  import to `proc_macro` to keep `--no-default-features` warning-clean

Tests:
- positive macro coverage for all three ttl spellings (`ttl` Duration expr /
  `ttl_secs` / `ttl_millis`) across `#[cached]` / `#[once]` / `#[concurrent_cached]`,
  and compile-fail UI goldens for the three-way exclusivity and the `ttl = <int>`
  migration error
- inline store coverage for every new `new()` constructor and the `ttl_secs` /
  `ttl_millis` builder setters (including override semantics)
- compile-fail UI golden asserting `*Base::<_, _, CustomHasher>::{new,builder}()`
  no longer compiles (the custom-hasher `.hasher()` path stays covered by the
  existing sharded `custom_hasher` unit test)
- async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass);
  `#[once]` `force_refresh`; `force_refresh` + `in_impl`; generic-rejection goldens;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; deferred attribute/store
  combinations (max_size+ttl_millis, in_impl+ttl_millis, ttl_millis+force_refresh,
  result_fallback+ttl_millis, async once in_impl) and store(0) counter resets;
  sharded peek does not renew TTL under refresh_on_hit

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
jaemk added a commit that referenced this pull request Jun 20, 2026
…sistency

Make constructors and the ttl attribute consistent across the public surface, on
top of the redb DiskCache rewrite. Breaking and additive changes with doc and test
follow-ups:

Macros:
- `ttl` attribute now takes a `Duration` expression
  (`ttl = "core::time::Duration::from_secs(60)"`); the old whole-seconds integer
  form (`ttl = 60`) is removed and emits a migration error pointing at
  `ttl_secs`/`ttl_millis`
- `ttl_secs` attribute for whole-second TTLs and `ttl_millis` for sub-second TTLs;
  `ttl` / `ttl_secs` / `ttl_millis` are three-way mutually exclusive, and each is
  mutually exclusive with `expires` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- `refresh` is rejected alongside the other store-builder attributes when a
  `create` block is supplied, mirroring `#[concurrent_cached]`
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- every in-memory and sharded store gains a `new()` that returns a ready-to-use
  cache (zero-config stores take no args; required-field stores take them
  positionally - `LruCache::new(max_size)`, `TtlCache::new(ttl)`,
  `LruTtlCache::new(max_size, ttl)`, the `Sharded*` variants, etc.), all
  `#[must_use]`. `RedbCache` / `RedisCache` / `AsyncRedisCache` drop `new()` (it
  returned a builder, not a cache); use `builder(...)`. `new()` now consistently
  returns a usable cache everywhere it exists
- `new()` / `builder()` on each sharded `*Base` type are now defined only on the
  default-hasher specialization (`*Base<K, V, DefaultShardHasher>`, the named
  alias). They previously sat on the generic `*Base<K, V, H>` impl but always
  returned a `DefaultShardHasher` builder, so a `*Base::<_, _, CustomHasher>::
  builder()` / `::new()` turbofish compiled yet silently dropped the custom
  hasher; that turbofish now fails to compile (E0599). A custom hasher is
  specified via `ShardedX::builder().hasher(h)`, which switches the builder's
  hasher type; alias-based construction and the `.hasher()` path are unchanged
- every TTL builder (non-sharded, sharded, Redis, Redb) gains `ttl_secs` /
  `ttl_millis` convenience setters alongside `ttl(Duration)`; last-writer-wins
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- docs and examples now prefer the short alias method API
  (`get`/`set`/`remove`/`clear`/`len`/...) over the `cache_*`-prefixed forms, with
  a note (in the crate docs and on the `Cached` trait) on when to use the `cache_*`
  names - a collision with another in-scope trait's method. Both forms remain; no
  method was removed
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl` and
  noting the in_impl cache is not externally invalidatable and its shared-id
  staleness footgun
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); it retries a missing release when the tag
  already exists on the remote; the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- README is generated from src/lib.rs via cargo-readme (regenerated here); dev
  tooling: pr-review shards reviewers across model-sized randomized chunks; pr-cycle
  fans fix application across disjoint sub-agents
- documentation accuracy pass: `ttl_millis` / `ttl_secs` are valid with
  `result_fallback` and (on the in-memory path) need `time_stores`; on `#[cached]`
  the "honored exactly" claim is scoped to the default in-memory store; Redis rounds
  `ttl_millis` up to the next whole second (500ms -> 1s, 1500ms -> 2s); `in_impl`
  emits a `#[doc(hidden)]` `{fn}_no_cache` sibling (all three macros);
  `#[concurrent_cached]` `force_refresh` documents the `result_fallback` interaction;
  the `#[concurrent_cached]` attribute table reaches parity with `#[cached]`; wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the async
  `V: Sync` clone-elision asymmetry and how a `Send + !Sync + !Clone` value surfaces
  at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat; redis `build()` lists
  `InvalidTtl` before `EmptyScope` to match validation order
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback
- elevate all compiler warnings to errors workspace-wide via the Cargo `[lints]`
  table (`[workspace.lints.rust] warnings = "deny"`, opted into by the `cached`
  and `cached_proc_macro` packages), so `cargo build`/`test`/`clippy` deny
  warnings rather than only the Makefile clippy target; gate `validate_ttl` to
  `any(time_stores, disk_store, redis_store)` and an unused test `AtomicUsize`
  import to `proc_macro` to keep `--no-default-features` warning-clean

Tests:
- positive macro coverage for all three ttl spellings (`ttl` Duration expr /
  `ttl_secs` / `ttl_millis`) across `#[cached]` / `#[once]` / `#[concurrent_cached]`,
  and compile-fail UI goldens for the three-way exclusivity and the `ttl = <int>`
  migration error
- inline store coverage for every new `new()` constructor and the `ttl_secs` /
  `ttl_millis` builder setters (including override semantics)
- compile-fail UI golden asserting `*Base::<_, _, CustomHasher>::{new,builder}()`
  no longer compiles (the custom-hasher `.hasher()` path stays covered by the
  existing sharded `custom_hasher` unit test)
- async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass);
  `#[once]` `force_refresh`; `force_refresh` + `in_impl`; generic-rejection goldens;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; deferred attribute/store
  combinations (max_size+ttl_millis, in_impl+ttl_millis, ttl_millis+force_refresh,
  result_fallback+ttl_millis, async once in_impl) and store(0) counter resets;
  sharded peek does not renew TTL under refresh_on_hit

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proc_macro: support args which are &T and Option<&T>

1 participant