1use alloc::string::String;
7use alloc::vec::Vec;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12pub const CAP_STATE_KV_V1: &str = "greentic.cap.state.kv.v1";
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
19pub enum StateOp {
20 Get,
22 Put,
24 Delete,
26 List,
28 Cas,
30}
31
32impl StateOp {
33 pub fn as_str(&self) -> &'static str {
35 match self {
36 Self::Get => "state.get",
37 Self::Put => "state.put",
38 Self::Delete => "state.delete",
39 Self::List => "state.list",
40 Self::Cas => "state.cas",
41 }
42 }
43}
44
45impl core::fmt::Display for StateOp {
46 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47 f.write_str(self.as_str())
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54pub struct StateOpPayload {
55 pub namespace: String,
57 pub key: String,
59 #[cfg_attr(
61 feature = "serde",
62 serde(default, skip_serializing_if = "Option::is_none")
63 )]
64 pub value: Option<Vec<u8>>,
65 #[cfg_attr(
67 feature = "serde",
68 serde(default, skip_serializing_if = "Option::is_none")
69 )]
70 pub ttl_seconds: Option<u32>,
71 #[cfg_attr(
73 feature = "serde",
74 serde(default, skip_serializing_if = "Option::is_none")
75 )]
76 pub cas_version: Option<u64>,
77 #[cfg_attr(
79 feature = "serde",
80 serde(default, skip_serializing_if = "Option::is_none")
81 )]
82 pub prefix: Option<String>,
83}
84
85impl StateOpPayload {
86 pub fn get(namespace: impl Into<String>, key: impl Into<String>) -> Self {
88 Self {
89 namespace: namespace.into(),
90 key: key.into(),
91 value: None,
92 ttl_seconds: None,
93 cas_version: None,
94 prefix: None,
95 }
96 }
97
98 pub fn put(namespace: impl Into<String>, key: impl Into<String>, value: Vec<u8>) -> Self {
100 Self {
101 namespace: namespace.into(),
102 key: key.into(),
103 value: Some(value),
104 ttl_seconds: None,
105 cas_version: None,
106 prefix: None,
107 }
108 }
109
110 pub fn delete(namespace: impl Into<String>, key: impl Into<String>) -> Self {
112 Self {
113 namespace: namespace.into(),
114 key: key.into(),
115 value: None,
116 ttl_seconds: None,
117 cas_version: None,
118 prefix: None,
119 }
120 }
121
122 pub fn list(namespace: impl Into<String>, prefix: impl Into<String>) -> Self {
124 Self {
125 namespace: namespace.into(),
126 key: String::new(),
127 value: None,
128 ttl_seconds: None,
129 cas_version: None,
130 prefix: Some(prefix.into()),
131 }
132 }
133
134 pub fn with_ttl(mut self, seconds: u32) -> Self {
136 self.ttl_seconds = Some(seconds);
137 self
138 }
139
140 pub fn with_cas_version(mut self, version: u64) -> Self {
142 self.cas_version = Some(version);
143 self
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
149#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
150pub struct StateOpResult {
151 pub found: bool,
153 #[cfg_attr(
155 feature = "serde",
156 serde(default, skip_serializing_if = "Option::is_none")
157 )]
158 pub value: Option<Vec<u8>>,
159 #[cfg_attr(
161 feature = "serde",
162 serde(default, skip_serializing_if = "Option::is_none")
163 )]
164 pub version: Option<u64>,
165 #[cfg_attr(
167 feature = "serde",
168 serde(default, skip_serializing_if = "Option::is_none")
169 )]
170 pub keys: Option<Vec<String>>,
171 #[cfg_attr(
173 feature = "serde",
174 serde(default, skip_serializing_if = "Option::is_none")
175 )]
176 pub error: Option<String>,
177}
178
179impl StateOpResult {
180 pub fn found(value: Vec<u8>) -> Self {
182 Self {
183 found: true,
184 value: Some(value),
185 version: None,
186 keys: None,
187 error: None,
188 }
189 }
190
191 pub fn not_found() -> Self {
193 Self {
194 found: false,
195 value: None,
196 version: None,
197 keys: None,
198 error: None,
199 }
200 }
201
202 pub fn ok() -> Self {
204 Self {
205 found: true,
206 value: None,
207 version: None,
208 keys: None,
209 error: None,
210 }
211 }
212
213 pub fn list(keys: Vec<String>) -> Self {
215 Self {
216 found: true,
217 value: None,
218 version: None,
219 keys: Some(keys),
220 error: None,
221 }
222 }
223
224 pub fn err(message: impl Into<String>) -> Self {
226 Self {
227 found: false,
228 value: None,
229 version: None,
230 keys: None,
231 error: Some(message.into()),
232 }
233 }
234
235 pub fn with_version(mut self, version: u64) -> Self {
237 self.version = Some(version);
238 self
239 }
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245#[cfg_attr(feature = "serde", serde(tag = "backend", rename_all = "snake_case"))]
246pub enum StateBackendKind {
247 Memory {
249 #[cfg_attr(feature = "serde", serde(default))]
251 max_entries: u32,
252 #[cfg_attr(feature = "serde", serde(default))]
254 default_ttl_seconds: u32,
255 },
256 Redis {
258 redis_url: String,
260 #[cfg_attr(feature = "serde", serde(default = "default_key_prefix"))]
262 key_prefix: String,
263 #[cfg_attr(feature = "serde", serde(default))]
265 default_ttl_seconds: u32,
266 #[cfg_attr(feature = "serde", serde(default = "default_pool_size"))]
268 pool_size: u32,
269 #[cfg_attr(feature = "serde", serde(default))]
271 tls_enabled: bool,
272 },
273}
274
275#[cfg(feature = "serde")]
276fn default_key_prefix() -> String {
277 String::from("greentic")
278}
279
280#[cfg(feature = "serde")]
281const fn default_pool_size() -> u32 {
282 5
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn state_op_display() {
291 assert_eq!(StateOp::Get.as_str(), "state.get");
292 assert_eq!(StateOp::Put.as_str(), "state.put");
293 assert_eq!(StateOp::Delete.as_str(), "state.delete");
294 assert_eq!(StateOp::List.as_str(), "state.list");
295 assert_eq!(StateOp::Cas.as_str(), "state.cas");
296 }
297
298 #[test]
299 fn payload_builders() {
300 let p = StateOpPayload::get("dev::t1::team", "session:abc");
301 assert_eq!(p.namespace, "dev::t1::team");
302 assert_eq!(p.key, "session:abc");
303 assert!(p.value.is_none());
304
305 let p = StateOpPayload::put("dev::t1::team", "k", vec![1, 2, 3]).with_ttl(60);
306 assert_eq!(p.value, Some(vec![1, 2, 3]));
307 assert_eq!(p.ttl_seconds, Some(60));
308
309 let p = StateOpPayload::delete("ns", "k");
310 assert!(p.value.is_none());
311
312 let p = StateOpPayload::list("ns", "session:");
313 assert_eq!(p.prefix, Some("session:".to_string()));
314 }
315
316 #[test]
317 fn result_builders() {
318 let r = StateOpResult::found(vec![42]);
319 assert!(r.found);
320 assert_eq!(r.value, Some(vec![42]));
321
322 let r = StateOpResult::not_found();
323 assert!(!r.found);
324
325 let r = StateOpResult::ok().with_version(7);
326 assert!(r.found);
327 assert_eq!(r.version, Some(7));
328
329 let r = StateOpResult::list(vec!["a".into(), "b".into()]);
330 assert_eq!(r.keys, Some(vec!["a".to_string(), "b".to_string()]));
331
332 let r = StateOpResult::err("boom");
333 assert!(!r.found);
334 assert_eq!(r.error, Some("boom".to_string()));
335 }
336
337 #[test]
338 fn state_op_payload_json_roundtrip() {
339 let original = StateOpPayload::put("ns", "key", b"hello".to_vec()).with_ttl(300);
340 let json = match serde_json::to_string(&original) {
341 Ok(value) => value,
342 Err(err) => panic!("serialize: {err}"),
343 };
344 let decoded: StateOpPayload = match serde_json::from_str(&json) {
345 Ok(value) => value,
346 Err(err) => panic!("deserialize: {err}"),
347 };
348 assert_eq!(decoded.namespace, "ns");
349 assert_eq!(decoded.key, "key");
350 assert_eq!(decoded.value, Some(b"hello".to_vec()));
351 assert_eq!(decoded.ttl_seconds, Some(300));
352 }
353
354 #[test]
355 fn state_op_result_json_roundtrip() {
356 let original = StateOpResult::found(b"world".to_vec()).with_version(42);
357 let json = match serde_json::to_string(&original) {
358 Ok(value) => value,
359 Err(err) => panic!("serialize: {err}"),
360 };
361 let decoded: StateOpResult = match serde_json::from_str(&json) {
362 Ok(value) => value,
363 Err(err) => panic!("deserialize: {err}"),
364 };
365 assert!(decoded.found);
366 assert_eq!(decoded.value, Some(b"world".to_vec()));
367 assert_eq!(decoded.version, Some(42));
368 }
369
370 #[test]
371 fn state_backend_kind_json_roundtrip() {
372 let memory = StateBackendKind::Memory {
373 max_entries: 10000,
374 default_ttl_seconds: 0,
375 };
376 let json = match serde_json::to_string(&memory) {
377 Ok(value) => value,
378 Err(err) => panic!("serialize: {err}"),
379 };
380 assert!(json.contains("\"backend\":\"memory\""));
381 let decoded: StateBackendKind = match serde_json::from_str(&json) {
382 Ok(value) => value,
383 Err(err) => panic!("deserialize: {err}"),
384 };
385 assert_eq!(decoded, memory);
386
387 let redis = StateBackendKind::Redis {
388 redis_url: "redis://localhost:6379/0".into(),
389 key_prefix: "greentic".into(),
390 default_ttl_seconds: 3600,
391 pool_size: 10,
392 tls_enabled: false,
393 };
394 let json = match serde_json::to_string(&redis) {
395 Ok(value) => value,
396 Err(err) => panic!("serialize: {err}"),
397 };
398 assert!(json.contains("\"backend\":\"redis\""));
399 let decoded: StateBackendKind = match serde_json::from_str(&json) {
400 Ok(value) => value,
401 Err(err) => panic!("deserialize: {err}"),
402 };
403 assert_eq!(decoded, redis);
404 }
405}