storage_kit/state_stores/config/
redb.rs1use std::{
2 ffi::OsStr,
3 fmt,
4 path::{Path, PathBuf},
5 time::Duration,
6};
7
8use serde::{
9 Deserialize, Deserializer, Serialize, Serializer, de,
10 ser::{SerializeMap, SerializeStruct},
11};
12
13use crate::StateStoreError;
14
15const DEFAULT_OPERATION_TIMEOUT_MS: u64 = 5_000;
16const DEFAULT_CACHE_SIZE_BYTES: usize = 64 * 1024 * 1024;
17
18pub enum RedbStateStoreLocation {
34 Memory,
36 File(PathBuf),
38}
39
40impl RedbStateStoreLocation {
41 #[must_use]
55 pub fn is_memory(&self) -> bool {
56 match self {
57 Self::Memory => true,
58 Self::File(_) => false,
59 }
60 }
61
62 pub(crate) fn validate(&self) -> Result<(), StateStoreError> {
63 match self {
64 Self::Memory => Ok(()),
65 Self::File(path) => RedbStateStorePath::new(path).validate(),
66 }
67 }
68
69 fn from_json_value(value: &serde_json::Value) -> Result<Self, String> {
70 if value.as_str() == Some("Memory") {
71 return Ok(Self::Memory);
72 }
73 let object = value.as_object().ok_or_else(|| {
74 "RedbStateStoreLocation must be `Memory` or `{ File: ... }`".to_owned()
75 })?;
76 if object.len() != 1 {
77 return Err(
78 "RedbStateStoreLocation object must contain exactly one variant".to_owned(),
79 );
80 }
81 let file = object
82 .get("File")
83 .ok_or_else(|| "RedbStateStoreLocation object must contain `File`".to_owned())?;
84 let path = file
85 .as_str()
86 .ok_or_else(|| "RedbStateStoreLocation.File must be a string".to_owned())?;
87 Ok(Self::File(PathBuf::from(path)))
88 }
89}
90
91impl Clone for RedbStateStoreLocation {
92 fn clone(&self) -> Self {
93 match self {
94 Self::Memory => Self::Memory,
95 Self::File(path) => Self::File(path.clone()),
96 }
97 }
98}
99
100impl fmt::Debug for RedbStateStoreLocation {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::Memory => f.write_str("Memory"),
104 Self::File(path) => f.debug_tuple("File").field(path).finish(),
105 }
106 }
107}
108
109impl PartialEq for RedbStateStoreLocation {
110 fn eq(&self, other: &Self) -> bool {
111 match (self, other) {
112 (Self::Memory, Self::Memory) => true,
113 (Self::File(left), Self::File(right)) => left == right,
114 _ => false,
115 }
116 }
117}
118
119impl Eq for RedbStateStoreLocation {}
120
121impl Serialize for RedbStateStoreLocation {
122 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123 where
124 S: Serializer,
125 {
126 match self {
127 Self::Memory => serializer.serialize_str("Memory"),
128 Self::File(path) => {
129 let value = path.to_str().ok_or_else(|| {
130 serde::ser::Error::custom(
131 "RedbStateStoreLocation.File must be valid UTF-8 to serialize as JSON",
132 )
133 })?;
134 let mut map = serializer.serialize_map(Some(1))?;
135 map.serialize_entry("File", value)?;
136 map.end()
137 }
138 }
139 }
140}
141
142impl<'de> Deserialize<'de> for RedbStateStoreLocation {
143 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144 where
145 D: Deserializer<'de>,
146 {
147 let value = serde_json::Value::deserialize(deserializer)?;
148 Self::from_json_value(&value).map_err(de::Error::custom)
149 }
150}
151
152pub struct RedbStateStoreConfig {
167 location: RedbStateStoreLocation,
168 operation_timeout_ms: u64,
169 cache_size_bytes: usize,
170}
171
172impl RedbStateStoreConfig {
173 #[must_use]
187 pub fn memory() -> Self {
188 Self {
189 location: RedbStateStoreLocation::Memory,
190 operation_timeout_ms: DEFAULT_OPERATION_TIMEOUT_MS,
191 cache_size_bytes: DEFAULT_CACHE_SIZE_BYTES,
192 }
193 }
194
195 #[must_use]
205 pub fn file(path: impl Into<PathBuf>) -> Self {
206 Self {
207 location: RedbStateStoreLocation::File(path.into()),
208 operation_timeout_ms: DEFAULT_OPERATION_TIMEOUT_MS,
209 cache_size_bytes: DEFAULT_CACHE_SIZE_BYTES,
210 }
211 }
212
213 pub fn with_operation_timeout(mut self, timeout: Duration) -> Result<Self, StateStoreError> {
229 let millis = u64::try_from(timeout.as_millis()).map_err(|err| {
230 StateStoreError::invalid_config({
231 let mut message = String::from("Redb state store operation_timeout overflow: ");
232 message.push_str(&err.to_string());
233 message
234 })
235 })?;
236 if millis == 0 {
237 return Err(StateStoreError::invalid_config(
238 "Redb state store operation_timeout must be greater than zero",
239 ));
240 }
241 self.operation_timeout_ms = millis;
242 Ok(self)
243 }
244
245 #[must_use]
255 pub fn with_cache_size_bytes(mut self, bytes: usize) -> Self {
256 self.cache_size_bytes = bytes;
257 self
258 }
259
260 #[must_use]
269 pub fn operation_timeout(&self) -> Duration {
270 Duration::from_millis(self.operation_timeout_ms)
271 }
272
273 #[must_use]
282 pub fn cache_size_bytes(&self) -> usize {
283 self.cache_size_bytes
284 }
285
286 #[must_use]
296 pub fn location(&self) -> &RedbStateStoreLocation {
297 &self.location
298 }
299
300 pub fn validate(&self) -> Result<(), StateStoreError> {
314 if self.operation_timeout_ms == 0 {
315 return Err(StateStoreError::invalid_config(
316 "Redb state store operation_timeout must be greater than zero",
317 ));
318 }
319 if self.cache_size_bytes == 0 {
320 return Err(StateStoreError::invalid_config(
321 "Redb state store cache_size_bytes must be greater than zero",
322 ));
323 }
324 self.location.validate()
325 }
326
327 #[cfg(feature = "redb")]
328 pub(crate) fn operation_timeout_ms(&self) -> u64 {
329 self.operation_timeout_ms
330 }
331
332 pub(crate) fn from_json_value(value: &serde_json::Value) -> Result<Self, String> {
333 let object = value
334 .as_object()
335 .ok_or_else(|| "RedbStateStoreConfig must be an object".to_owned())?;
336 let location = object
337 .get("location")
338 .ok_or_else(|| "RedbStateStoreConfig.location is required".to_owned())
339 .and_then(RedbStateStoreLocation::from_json_value)?;
340 let operation_timeout_ms = object
341 .get("operation_timeout_ms")
342 .ok_or_else(|| "RedbStateStoreConfig.operation_timeout_ms is required".to_owned())?
343 .as_u64()
344 .ok_or_else(|| "RedbStateStoreConfig.operation_timeout_ms must be a u64".to_owned())?;
345 let cache_size_u64 = object
346 .get("cache_size_bytes")
347 .ok_or_else(|| "RedbStateStoreConfig.cache_size_bytes is required".to_owned())?
348 .as_u64()
349 .ok_or_else(|| "RedbStateStoreConfig.cache_size_bytes must be a u64".to_owned())?;
350 let cache_size_bytes = usize::try_from(cache_size_u64).map_err(|err| {
351 let mut message = String::from("RedbStateStoreConfig.cache_size_bytes overflow: ");
352 message.push_str(&err.to_string());
353 message
354 })?;
355 Ok(Self {
356 location,
357 operation_timeout_ms,
358 cache_size_bytes,
359 })
360 }
361}
362
363impl Clone for RedbStateStoreConfig {
364 fn clone(&self) -> Self {
365 Self {
366 location: self.location.clone(),
367 operation_timeout_ms: self.operation_timeout_ms,
368 cache_size_bytes: self.cache_size_bytes,
369 }
370 }
371}
372
373impl fmt::Debug for RedbStateStoreConfig {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 f.debug_struct("RedbStateStoreConfig")
376 .field("location", &self.location)
377 .field("operation_timeout_ms", &self.operation_timeout_ms)
378 .field("cache_size_bytes", &self.cache_size_bytes)
379 .finish()
380 }
381}
382
383impl PartialEq for RedbStateStoreConfig {
384 fn eq(&self, other: &Self) -> bool {
385 self.location == other.location
386 && self.operation_timeout_ms == other.operation_timeout_ms
387 && self.cache_size_bytes == other.cache_size_bytes
388 }
389}
390
391impl Eq for RedbStateStoreConfig {}
392
393impl Serialize for RedbStateStoreConfig {
394 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
395 where
396 S: Serializer,
397 {
398 let mut state = serializer.serialize_struct("RedbStateStoreConfig", 3)?;
399 state.serialize_field("location", &self.location)?;
400 state.serialize_field("operation_timeout_ms", &self.operation_timeout_ms)?;
401 state.serialize_field("cache_size_bytes", &self.cache_size_bytes)?;
402 state.end()
403 }
404}
405
406impl<'de> Deserialize<'de> for RedbStateStoreConfig {
407 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
408 where
409 D: Deserializer<'de>,
410 {
411 let value = serde_json::Value::deserialize(deserializer)?;
412 Self::from_json_value(&value).map_err(de::Error::custom)
413 }
414}
415
416struct RedbStateStorePath<'a> {
417 path: &'a Path,
418}
419
420impl<'a> RedbStateStorePath<'a> {
421 fn new(path: &'a Path) -> Self {
422 Self { path }
423 }
424
425 fn validate(&self) -> Result<(), StateStoreError> {
426 if self.path.as_os_str().is_empty() {
427 return Err(StateStoreError::invalid_config(
428 "Redb state store file path must not be empty",
429 ));
430 }
431 if self.contains_nul(self.path.as_os_str()) {
432 return Err(StateStoreError::invalid_config(
433 "Redb state store file path must not contain NUL",
434 ));
435 }
436 Ok(())
437 }
438
439 fn contains_nul(&self, value: &OsStr) -> bool {
440 value.as_encoded_bytes().contains(&0)
441 }
442}