dwara_core/extensions/mod.rs
1//! Extension points: the OSS/Ent edition boundary as a type boundary (DW-004).
2//!
3//! dwara is open-core. The five traits in this module ([`rate_limiter::RateLimiter`],
4//! [`config_source::ConfigSource`], [`cache::CacheStore`],
5//! [`analytics::AnalyticsSink`], [`secrets::SecretSource`]) are the swappable
6//! state backend contracts. The OSS edition ships the local in-memory /
7//! file / env implementations that live alongside each trait; additional
8//! backends may be provided separately in future editions and must
9//! implement the same traits and slot in
10//! via `dyn` injection WITHOUT changing trait signatures or call sites.
11//!
12//! Because every trait is used as `Arc<dyn Trait>` at runtime, all of them
13//! are dyn-compatible (object safe). They are `async` via the `async-trait`
14//! crate: native RPITIT is not yet dyn-compatible on stable Rust, and
15//! dyn-compatibility is the load-bearing requirement here.
16//!
17//! # Failure model
18//!
19//! All trait methods share one error type, [`ExtensionsError`]. It is
20//! non-exhaustive and carries a human-readable message; the variants map to
21//! failure classes (I/O, invalid data, backend failure) rather than to
22//! per-trait taxonomies, so a new backend can express its failures without
23//! a breaking change to the shared enum. Transient-vs-permanent and retry
24//! expectations are documented per trait.
25
26pub mod analytics;
27pub mod cache;
28pub mod config_source;
29// DW-054: Config convergence backend (ent feature only). The module
30// compiles only when the `ent` cargo feature is enabled; OSS builds
31// never pull in the redis dependency. The trait is the seam for
32// etcd/Consul backends (deferred); v1 ships a Redis implementation.
33#[cfg(feature = "ent")]
34pub mod config_convergence;
35pub mod licensing;
36pub mod rate_limiter;
37// DW-031: Distributed Redis-backed GCRA rate limiter (ent feature only).
38// The module compiles only when the `ent` cargo feature is enabled; OSS
39// builds never pull in the redis dependency.
40#[cfg(feature = "ent")]
41pub mod redis_rate_limiter;
42// DW-155: Distributed Redis-backed consumer request quotas (ent feature
43// only). The module compiles only when the `ent` cargo feature is
44// enabled; OSS builds never pull in the redis dependency. The local
45// SQLite-backed quota checker (state::quotas) remains the OSS default.
46#[cfg(feature = "ent")]
47pub mod redis_quotas;
48// DW-068: Redis-backed distributed cache (Enterprise). Feature-gated
49// behind the `ent` cargo feature (default OFF).
50#[cfg(feature = "ent")]
51pub mod redis_cache;
52// DW-069: Vault/KMS SecretSource (Enterprise). Feature-gated
53// behind the `ent` cargo feature (default OFF).
54pub mod secrets;
55#[cfg(feature = "ent")]
56pub mod vault_secrets;
57// SEC-06 (#210): Distributed Redis-backed HMAC nonce store (ent
58// feature only). The module compiles only when the `ent` cargo
59// feature is enabled; OSS builds never pull in the redis dependency.
60// The OSS in-process nonce cache (signed_url::NonceCache) remains
61// the default; this store is opt-in when a Redis URL is configured.
62#[cfg(feature = "ent")]
63pub mod nonce;
64
65/// Shared error type for all extension traits.
66///
67/// Non-exhaustive by design: Ent backends may surface new failure classes
68/// via [`ExtensionsError::Backend`] rather than new variants.
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ExtensionsError {
72 /// The backing store or file could not be read/written.
73 Io(String),
74 /// Input or stored data was malformed (e.g. config parse failure).
75 Invalid(String),
76 /// The backend itself failed (connection lost, sealed, ...).
77 Backend(String),
78 /// The operation is not supported by this implementation.
79 Unsupported(String),
80}
81
82impl std::fmt::Display for ExtensionsError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 ExtensionsError::Io(m) => write!(f, "extension io error: {m}"),
86 ExtensionsError::Invalid(m) => write!(f, "extension invalid-data error: {m}"),
87 ExtensionsError::Backend(m) => write!(f, "extension backend error: {m}"),
88 ExtensionsError::Unsupported(m) => write!(f, "extension unsupported operation: {m}"),
89 }
90 }
91}
92
93impl std::error::Error for ExtensionsError {}
94
95impl From<std::io::Error> for ExtensionsError {
96 /// I/O failures map to the [`ExtensionsError::Io`] class, carrying the
97 /// OS error message. Call sites needing more context (e.g. the file
98 /// path) still hand-format — this conversion covers the common case
99 /// (#128, DW-004 review).
100 fn from(e: std::io::Error) -> Self {
101 ExtensionsError::Io(e.to_string())
102 }
103}
104
105impl From<crate::config::ConfigError> for ExtensionsError {
106 /// Config parse failures map to [`ExtensionsError::Invalid`],
107 /// preserving `ConfigError`'s path-precise Display ("config error at
108 /// {path}: {message}").
109 fn from(e: crate::config::ConfigError) -> Self {
110 ExtensionsError::Invalid(e.to_string())
111 }
112}