Skip to main content

oxihuman_core/
response_cache.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! HTTP-style response cache — stores and retrieves keyed response entries.
6
7/// A cached HTTP-style response entry.
8#[derive(Clone, Debug)]
9pub struct CachedResponse {
10    pub status: u16,
11    pub body: String,
12    pub content_type: String,
13    pub max_age_secs: u64,
14    pub cached_at: u64,
15}
16
17/// Cache configuration.
18#[derive(Clone, Debug)]
19pub struct ResponseCacheConfig {
20    pub max_entries: usize,
21    pub default_max_age_secs: u64,
22}
23
24impl Default for ResponseCacheConfig {
25    fn default() -> Self {
26        Self {
27            max_entries: 512,
28            default_max_age_secs: 300,
29        }
30    }
31}
32
33/// An LRU-style response cache (simplified: evicts oldest entry).
34pub struct ResponseCache {
35    pub config: ResponseCacheConfig,
36    entries: Vec<(String, CachedResponse)>,
37}
38
39/// Creates a new response cache.
40pub fn new_response_cache(config: ResponseCacheConfig) -> ResponseCache {
41    ResponseCache {
42        config,
43        entries: Vec::new(),
44    }
45}
46
47/// Stores a response in the cache.
48pub fn cache_store(cache: &mut ResponseCache, key: &str, response: CachedResponse) {
49    cache.entries.retain(|(k, _)| k != key);
50    if cache.entries.len() >= cache.config.max_entries {
51        cache.entries.remove(0); /* evict oldest */
52    }
53    cache.entries.push((key.into(), response));
54}
55
56/// Retrieves a cached response by key if it has not expired.
57pub fn cache_get<'a>(cache: &'a ResponseCache, key: &str, now: u64) -> Option<&'a CachedResponse> {
58    cache
59        .entries
60        .iter()
61        .find(|(k, r)| k == key && now.saturating_sub(r.cached_at) < r.max_age_secs)
62        .map(|(_, r)| r)
63}
64
65/// Invalidates a cached entry by key.
66pub fn cache_invalidate(cache: &mut ResponseCache, key: &str) -> bool {
67    let before = cache.entries.len();
68    cache.entries.retain(|(k, _)| k != key);
69    cache.entries.len() < before
70}
71
72/// Purges all expired entries at the given timestamp.
73pub fn purge_expired_responses(cache: &mut ResponseCache, now: u64) -> usize {
74    let before = cache.entries.len();
75    cache
76        .entries
77        .retain(|(_, r)| now.saturating_sub(r.cached_at) < r.max_age_secs);
78    before.saturating_sub(cache.entries.len())
79}
80
81/// Returns the number of entries currently in the cache.
82pub fn cache_size(cache: &ResponseCache) -> usize {
83    cache.entries.len()
84}
85
86impl ResponseCache {
87    /// Creates a new cache with default config.
88    pub fn new(config: ResponseCacheConfig) -> Self {
89        new_response_cache(config)
90    }
91}
92
93fn make_response(status: u16, body: &str, max_age: u64, cached_at: u64) -> CachedResponse {
94    CachedResponse {
95        status,
96        body: body.into(),
97        content_type: "text/plain".into(),
98        max_age_secs: max_age,
99        cached_at,
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    fn make_cache() -> ResponseCache {
108        new_response_cache(ResponseCacheConfig::default())
109    }
110
111    #[test]
112    fn test_store_and_retrieve() {
113        let mut c = make_cache();
114        cache_store(&mut c, "/api", make_response(200, "ok", 300, 0));
115        let r = cache_get(&c, "/api", 100);
116        assert!(r.is_some());
117    }
118
119    #[test]
120    fn test_expired_entry_returns_none() {
121        let mut c = make_cache();
122        cache_store(&mut c, "/api", make_response(200, "ok", 60, 0));
123        /* now = 100, max_age=60 => expired */
124        assert!(cache_get(&c, "/api", 100).is_none());
125    }
126
127    #[test]
128    fn test_invalidate_removes_entry() {
129        let mut c = make_cache();
130        cache_store(&mut c, "/api", make_response(200, "ok", 300, 0));
131        assert!(cache_invalidate(&mut c, "/api"));
132        assert_eq!(cache_size(&c), 0);
133    }
134
135    #[test]
136    fn test_invalidate_nonexistent_returns_false() {
137        let mut c = make_cache();
138        assert!(!cache_invalidate(&mut c, "/missing"));
139    }
140
141    #[test]
142    fn test_purge_expired_removes_old() {
143        let mut c = make_cache();
144        cache_store(&mut c, "/old", make_response(200, "old", 10, 0));
145        cache_store(&mut c, "/new", make_response(200, "new", 1000, 0));
146        let removed = purge_expired_responses(&mut c, 100);
147        assert_eq!(removed, 1);
148        assert_eq!(cache_size(&c), 1);
149    }
150
151    #[test]
152    fn test_overwrite_existing_key() {
153        let mut c = make_cache();
154        cache_store(&mut c, "/k", make_response(200, "v1", 300, 0));
155        cache_store(&mut c, "/k", make_response(200, "v2", 300, 0));
156        assert_eq!(cache_size(&c), 1);
157        assert_eq!(cache_get(&c, "/k", 0).expect("should succeed").body, "v2");
158    }
159
160    #[test]
161    fn test_eviction_when_at_capacity() {
162        let mut c = new_response_cache(ResponseCacheConfig {
163            max_entries: 2,
164            default_max_age_secs: 300,
165        });
166        cache_store(&mut c, "/a", make_response(200, "a", 300, 0));
167        cache_store(&mut c, "/b", make_response(200, "b", 300, 0));
168        cache_store(&mut c, "/c", make_response(200, "c", 300, 0)); /* evicts /a */
169        assert!(cache_get(&c, "/a", 0).is_none());
170        assert_eq!(cache_size(&c), 2);
171    }
172
173    #[test]
174    fn test_cache_size_tracks_correctly() {
175        let mut c = make_cache();
176        assert_eq!(cache_size(&c), 0);
177        cache_store(&mut c, "/x", make_response(200, "x", 60, 0));
178        assert_eq!(cache_size(&c), 1);
179    }
180
181    #[test]
182    fn test_response_status_preserved() {
183        let mut c = make_cache();
184        cache_store(&mut c, "/e", make_response(404, "not found", 300, 0));
185        assert_eq!(cache_get(&c, "/e", 0).expect("should succeed").status, 404);
186    }
187}