Skip to main content

ic_query/cache/
model.rs

1//! Module: cache::model
2//!
3//! Responsibility: define shared cache-validation and local inventory report contracts.
4//! Does not own: filesystem traversal, cache-family validation, or CLI output.
5//! Boundary: names family validation outcomes and exposes generic header, age,
6//! recovery-policy, and lock evidence without performing family-specific validation.
7
8use serde::Serialize;
9use std::{fmt, path::PathBuf};
10
11/// Current serialized schema version for cache-status reports.
12pub const CACHE_STATUS_REPORT_SCHEMA_VERSION: u32 = 1;
13
14///
15/// CacheValidationStatus
16///
17/// Semantic validation result for an existing family-specific cache.
18///
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum CacheValidationStatus {
23    /// The cache passed its family-specific schema, identity, and completeness checks.
24    #[serde(rename = "ok")]
25    Valid,
26    /// The cache exists but failed family-specific validation.
27    Invalid,
28}
29
30impl CacheValidationStatus {
31    /// Return the stable serialized status label.
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::Valid => "ok",
36            Self::Invalid => "invalid",
37        }
38    }
39}
40
41impl fmt::Display for CacheValidationStatus {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47///
48/// CacheRefreshAttemptStatus
49///
50/// Lifecycle state for a complete-cache refresh attempt.
51///
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CacheRefreshAttemptStatus {
56    /// A refresh started or published intermediate collection progress.
57    Running,
58    /// A refresh exhausted its source and published a complete cache.
59    Complete,
60    /// A refresh terminated without replacing the complete cache.
61    Failed,
62}
63
64impl CacheRefreshAttemptStatus {
65    /// Return the stable serialized lifecycle label.
66    #[must_use]
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::Running => "running",
70            Self::Complete => "complete",
71            Self::Failed => "failed",
72        }
73    }
74
75    #[cfg(any(
76        feature = "icrc-host",
77        feature = "nns-host",
78        feature = "sns-host",
79        test
80    ))]
81    pub(crate) fn from_label(label: &str) -> Option<Self> {
82        match label {
83            "running" => Some(Self::Running),
84            "complete" => Some(Self::Complete),
85            "failed" => Some(Self::Failed),
86            _ => None,
87        }
88    }
89}
90
91impl fmt::Display for CacheRefreshAttemptStatus {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        formatter.write_str(self.as_str())
94    }
95}
96
97///
98/// CacheHeaderStatus
99///
100/// Generic header-integrity classification for one complete cache file.
101///
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
104#[serde(rename_all = "snake_case")]
105pub enum CacheHeaderStatus {
106    /// The generic cache header is readable.
107    Readable,
108    /// The generic cache header cannot be read or parsed.
109    Invalid,
110}
111
112impl CacheHeaderStatus {
113    /// Return the stable serialized status label.
114    #[must_use]
115    pub const fn as_str(self) -> &'static str {
116        match self {
117            Self::Readable => "readable",
118            Self::Invalid => "invalid",
119        }
120    }
121}
122
123///
124/// CacheAgeStatus
125///
126/// Caller-relative age classification for one complete cache file.
127///
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum CacheAgeStatus {
132    /// The cache is within its registered family stale threshold.
133    Fresh,
134    /// The cache is older than its registered family stale threshold.
135    Stale,
136    /// The cache has a readable age but no registered family stale threshold.
137    Unmanaged,
138    /// The cache age cannot be calculated from its generic timestamp evidence.
139    Unknown,
140}
141
142impl CacheAgeStatus {
143    /// Return the stable serialized status label.
144    #[must_use]
145    pub const fn as_str(self) -> &'static str {
146        match self {
147            Self::Fresh => "fresh",
148            Self::Stale => "stale",
149            Self::Unmanaged => "unmanaged",
150            Self::Unknown => "unknown",
151        }
152    }
153}
154
155///
156/// CacheRecoveryPolicy
157///
158/// Owner policy for replacing recoverable invalid content at a canonical cache path.
159///
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
162#[serde(rename_all = "snake_case")]
163pub enum CacheRecoveryPolicy {
164    /// An ordinary owner read-through replaces recoverable invalid content.
165    Automatic,
166    /// Recovery requires an explicitly selected refresh operation.
167    Explicit,
168    /// Ordinary read-through creates a missing cache but does not replace invalid content.
169    MissingOnly,
170    /// The path does not identify a current canonical cache owner.
171    Unknown,
172}
173
174impl CacheRecoveryPolicy {
175    /// Return the stable serialized policy label.
176    #[must_use]
177    pub const fn as_str(self) -> &'static str {
178        match self {
179            Self::Automatic => "automatic",
180            Self::Explicit => "explicit",
181            Self::MissingOnly => "missing_only",
182            Self::Unknown => "unknown",
183        }
184    }
185}
186
187///
188/// CacheRefreshLockStatus
189///
190/// Generic age or validity classification for one refresh lock.
191///
192
193#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
194#[serde(rename_all = "snake_case")]
195pub enum CacheRefreshLockStatus {
196    /// The lock is within the stale threshold recorded by its owner.
197    Active,
198    /// The lock is older than the stale threshold recorded by its owner.
199    Stale,
200    /// The lock is unreadable, malformed, or future-dated.
201    Invalid,
202}
203
204impl CacheRefreshLockStatus {
205    /// Return the stable serialized status label.
206    #[must_use]
207    pub const fn as_str(self) -> &'static str {
208        match self {
209            Self::Active => "active",
210            Self::Stale => "stale",
211            Self::Invalid => "invalid",
212        }
213    }
214}
215
216///
217/// CacheStatusRequest
218///
219/// Local cache-root inspection request with a caller-supplied observation time.
220///
221
222#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct CacheStatusRequest {
224    /// User-level cache root to inspect.
225    pub cache_root: PathBuf,
226    /// Observation time used to calculate cache ages.
227    pub now_unix_secs: u64,
228}
229
230impl CacheStatusRequest {
231    /// Construct a local cache-status request.
232    #[must_use]
233    pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
234        Self {
235            cache_root: cache_root.into(),
236            now_unix_secs,
237        }
238    }
239}
240
241///
242/// CacheStatusReport
243///
244/// Bounded local inventory of known complete caches and refresh locks.
245///
246
247#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
248pub struct CacheStatusReport {
249    /// Cache-status report schema version.
250    pub schema_version: u32,
251    /// Inspected user-level cache root.
252    pub cache_root: String,
253    /// UTC timestamp at which local inspection was requested.
254    pub inspected_at: String,
255    /// Whether the cache root existed.
256    pub cache_root_found: bool,
257    /// Maximum number of cache and refresh-lock files inspected in one report.
258    pub scan_limit: usize,
259    /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
260    pub truncated: bool,
261    /// Whether the generic inventory performed family-specific semantic validation.
262    pub family_validation_performed: bool,
263    /// Number of cache rows returned.
264    pub cache_count: usize,
265    /// Number of caches with readable generic headers.
266    pub readable_header_count: usize,
267    /// Number of caches with unreadable or malformed generic headers.
268    pub invalid_header_count: usize,
269    /// Number of caches fresh under an explicit family policy.
270    pub fresh_count: usize,
271    /// Number of caches stale under an explicit family policy.
272    pub stale_count: usize,
273    /// Number of readable caches whose family has no registered age policy.
274    pub unmanaged_age_count: usize,
275    /// Number of caches whose age cannot be calculated from generic evidence.
276    pub unknown_age_count: usize,
277    /// Sum of filesystem sizes for returned cache files.
278    pub total_size_bytes: u64,
279    /// Canonically path-ordered cache rows.
280    pub caches: Vec<CacheStatusRow>,
281    /// Number of refresh-lock rows returned.
282    pub refresh_lock_count: usize,
283    /// Number of locks still active under their recorded stale policy.
284    pub active_refresh_lock_count: usize,
285    /// Number of locks older than their recorded stale policy.
286    pub stale_refresh_lock_count: usize,
287    /// Number of unreadable, malformed, or future-dated locks.
288    pub invalid_refresh_lock_count: usize,
289    /// Sum of filesystem sizes for returned refresh-lock files.
290    pub refresh_lock_size_bytes: u64,
291    /// Canonically path-ordered refresh-lock rows.
292    pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
293}
294
295///
296/// CacheStatusRow
297///
298/// Generic local metadata and caller-relative age for one complete cache file.
299///
300
301#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
302pub struct CacheStatusRow {
303    /// Stable component label inferred from cache identity or canonical path.
304    pub component: String,
305    /// Absolute cache-file path.
306    pub cache_path: String,
307    /// Cache-root-relative path.
308    pub relative_path: String,
309    /// Generic cache-header integrity without family-specific semantic validation.
310    pub header_status: CacheHeaderStatus,
311    /// Caller-relative age classification kept separate from header integrity.
312    pub age_status: CacheAgeStatus,
313    /// Owner policy for recovering invalid content at this canonical path.
314    pub recovery_policy: CacheRecoveryPolicy,
315    /// Serialized cache schema version when readable.
316    pub schema_version: Option<u32>,
317    /// Serialized network identity when present.
318    pub network: Option<String>,
319    /// Cache collection timestamp when readable.
320    pub fetched_at: Option<String>,
321    /// Caller-relative age when the timestamp is valid and not in the future.
322    pub age_seconds: Option<u64>,
323    /// Family age threshold when one is explicitly defined.
324    pub stale_after_seconds: Option<u64>,
325    /// Filesystem size of this cache file.
326    pub size_bytes: u64,
327    /// Generic header or timestamp inspection error; family-specific validation is separate.
328    pub inspection_error: Option<String>,
329}
330
331///
332/// CacheRefreshLockStatusRow
333///
334/// Local identity, ownership, age, and stale policy for one refresh lock.
335///
336
337#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
338pub struct CacheRefreshLockStatusRow {
339    /// Stable component label inferred from the recorded cache target.
340    pub component: String,
341    /// Absolute refresh-lock path.
342    pub refresh_lock_path: String,
343    /// Cache-root-relative refresh-lock path.
344    pub relative_path: String,
345    /// Generic lock age or validity classification.
346    pub status: CacheRefreshLockStatus,
347    /// Serialized refresh-lock schema version when readable.
348    pub schema_version: Option<u32>,
349    /// Serialized network identity when readable.
350    pub network: Option<String>,
351    /// Operating-system process id recorded by the lock owner when readable.
352    pub pid: Option<u32>,
353    /// Raw Unix-millisecond acquisition time when readable.
354    pub started_at_unix_ms: Option<u64>,
355    /// UTC acquisition timestamp when readable.
356    pub started_at: Option<String>,
357    /// Caller-relative lock age when the timestamp is not in the future.
358    pub age_seconds: Option<u64>,
359    /// Stale threshold recorded by the lock owner.
360    pub stale_after_seconds: Option<u64>,
361    /// Cache target recorded by the lock owner when readable.
362    pub target_path: Option<String>,
363    /// Filesystem size of this refresh-lock file.
364    pub size_bytes: u64,
365    /// Lock parse, shape, or timestamp error.
366    pub error: Option<String>,
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn typed_statuses_keep_stable_json_labels() {
375        for (status, expected) in [
376            (CacheValidationStatus::Valid, "ok"),
377            (CacheValidationStatus::Invalid, "invalid"),
378        ] {
379            assert_eq!(status.as_str(), expected);
380            assert_eq!(status.to_string(), expected);
381            assert_eq!(
382                serde_json::to_value(status).expect("serialize cache validation status"),
383                serde_json::json!(expected)
384            );
385        }
386        for (status, expected) in [
387            (CacheRefreshAttemptStatus::Running, "running"),
388            (CacheRefreshAttemptStatus::Complete, "complete"),
389            (CacheRefreshAttemptStatus::Failed, "failed"),
390        ] {
391            assert_eq!(status.as_str(), expected);
392            assert_eq!(status.to_string(), expected);
393            assert_eq!(
394                CacheRefreshAttemptStatus::from_label(expected),
395                Some(status)
396            );
397            assert_eq!(
398                serde_json::to_value(status).expect("serialize refresh-attempt status"),
399                serde_json::json!(expected)
400            );
401        }
402        assert_eq!(CacheRefreshAttemptStatus::from_label("unknown"), None);
403        for (status, expected) in [
404            (CacheHeaderStatus::Readable, "readable"),
405            (CacheHeaderStatus::Invalid, "invalid"),
406        ] {
407            assert_eq!(status.as_str(), expected);
408            assert_eq!(
409                serde_json::to_value(status).expect("serialize cache header status"),
410                serde_json::json!(expected)
411            );
412        }
413        for (status, expected) in [
414            (CacheAgeStatus::Fresh, "fresh"),
415            (CacheAgeStatus::Stale, "stale"),
416            (CacheAgeStatus::Unmanaged, "unmanaged"),
417            (CacheAgeStatus::Unknown, "unknown"),
418        ] {
419            assert_eq!(status.as_str(), expected);
420            assert_eq!(
421                serde_json::to_value(status).expect("serialize cache age status"),
422                serde_json::json!(expected)
423            );
424        }
425        for (policy, expected) in [
426            (CacheRecoveryPolicy::Automatic, "automatic"),
427            (CacheRecoveryPolicy::Explicit, "explicit"),
428            (CacheRecoveryPolicy::MissingOnly, "missing_only"),
429            (CacheRecoveryPolicy::Unknown, "unknown"),
430        ] {
431            assert_eq!(policy.as_str(), expected);
432            assert_eq!(
433                serde_json::to_value(policy).expect("serialize cache recovery policy"),
434                serde_json::json!(expected)
435            );
436        }
437        for (status, expected) in [
438            (CacheRefreshLockStatus::Active, "active"),
439            (CacheRefreshLockStatus::Stale, "stale"),
440            (CacheRefreshLockStatus::Invalid, "invalid"),
441        ] {
442            assert_eq!(status.as_str(), expected);
443            assert_eq!(
444                serde_json::to_value(status).expect("serialize refresh-lock status"),
445                serde_json::json!(expected)
446            );
447        }
448    }
449}