Skip to main content

distributed_cache/
action.rs

1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! The bounded, cache-shaped operation set of `v1.cache.redis` — Rust port of
18//! the Java `CacheAction` enum (design spec §2, Q5). Each maps to a
19//! cluster-correct Redis call through the `RedisBackend` seam:
20//!
21//! - [`CacheAction::Put`] — `SETEX` (value + TTL);
22//! - [`CacheAction::Get`] — `GET` (value, or null on a miss);
23//! - [`CacheAction::Mget`] — `MGET` (per-slot scatter-gather on a cluster);
24//! - [`CacheAction::Mput`] — a pipelined per-entry `SETEX` (TTL-preserving,
25//!   non-atomic across the map — never raw `MSET`, which sets no TTL);
26//! - [`CacheAction::Delete`] — `DEL` (count removed);
27//! - [`CacheAction::PutIfNotPresent`] — atomic `SET key value NX EX ttl`;
28//! - [`CacheAction::ListPush`] — atomic `RPUSH` + `EXPIRE` (new length);
29//! - [`CacheAction::ListPop`] — destructive `LPOP` (oldest value, or null);
30//! - [`CacheAction::ListLen`] — `LLEN`.
31//!
32//! `PING` is intentionally not here — it backs the `redis.health` check, not
33//! a general action.
34
35use platform_core::AppError;
36
37/// One cache operation, selected by the `action` header (case-insensitive).
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum CacheAction {
40    Put,
41    Get,
42    Mget,
43    Mput,
44    Delete,
45    PutIfNotPresent,
46    ListPush,
47    ListPop,
48    ListLen,
49}
50
51impl CacheAction {
52    /// Every action, in the Java enum's declaration order (the order the
53    /// error messages list them in).
54    pub const ALL: [CacheAction; 9] = [
55        CacheAction::Put,
56        CacheAction::Get,
57        CacheAction::Mget,
58        CacheAction::Mput,
59        CacheAction::Delete,
60        CacheAction::PutIfNotPresent,
61        CacheAction::ListPush,
62        CacheAction::ListPop,
63        CacheAction::ListLen,
64    ];
65
66    /// The wire name — the `action` header value (Java `Enum.name()`).
67    pub fn name(self) -> &'static str {
68        match self {
69            CacheAction::Put => "PUT",
70            CacheAction::Get => "GET",
71            CacheAction::Mget => "MGET",
72            CacheAction::Mput => "MPUT",
73            CacheAction::Delete => "DELETE",
74            CacheAction::PutIfNotPresent => "PUT_IF_NOT_PRESENT",
75            CacheAction::ListPush => "LIST_PUSH",
76            CacheAction::ListPop => "LIST_POP",
77            CacheAction::ListLen => "LIST_LEN",
78        }
79    }
80
81    /// Resolve an `action` header (case-insensitive) to an action, with a
82    /// clear error naming the supported set when it does not match (Java
83    /// `CacheAction.from`).
84    pub fn from_header(action: Option<&str>) -> Result<CacheAction, AppError> {
85        let text = action.unwrap_or("");
86        if text.trim().is_empty() {
87            return Err(AppError::new(
88                400,
89                format!("Missing 'action' - one of {}", Self::supported()),
90            ));
91        }
92        let wanted = text.trim().to_ascii_uppercase();
93        Self::ALL
94            .into_iter()
95            .find(|candidate| candidate.name() == wanted)
96            .ok_or_else(|| {
97                AppError::new(
98                    400,
99                    format!("Unsupported action '{text}' - one of {}", Self::supported()),
100                )
101            })
102    }
103
104    fn supported() -> String {
105        Self::ALL
106            .iter()
107            .map(|action| action.name())
108            .collect::<Vec<_>>()
109            .join(", ")
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    /// Java `CacheActionTest.resolvesCaseInsensitively`.
118    #[test]
119    fn resolves_case_insensitively() {
120        assert_eq!(
121            CacheAction::Get,
122            CacheAction::from_header(Some("get")).unwrap()
123        );
124        assert_eq!(
125            CacheAction::Put,
126            CacheAction::from_header(Some(" Put ")).unwrap()
127        );
128        assert_eq!(
129            CacheAction::PutIfNotPresent,
130            CacheAction::from_header(Some("put_if_not_present")).unwrap()
131        );
132        assert_eq!(
133            CacheAction::ListPush,
134            CacheAction::from_header(Some("LIST_PUSH")).unwrap()
135        );
136    }
137
138    /// Java `CacheActionTest.missingActionNamesTheSupportedSet`.
139    #[test]
140    fn missing_action_names_the_supported_set() {
141        for absent in [None, Some(""), Some("  ")] {
142            let error = CacheAction::from_header(absent).unwrap_err();
143            assert_eq!(400, error.status());
144            assert!(error
145                .message()
146                .starts_with("Missing 'action' - one of PUT, GET, MGET"));
147            assert!(error.message().ends_with("LIST_LEN"));
148        }
149    }
150
151    /// Java `CacheActionTest.unsupportedActionIsNamedInTheError`.
152    #[test]
153    fn unsupported_action_is_named_in_the_error() {
154        let error = CacheAction::from_header(Some("INCR")).unwrap_err();
155        assert_eq!(400, error.status());
156        assert!(error
157            .message()
158            .starts_with("Unsupported action 'INCR' - one of "));
159        assert!(error.message().contains("PUT_IF_NOT_PRESENT"));
160    }
161}