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}