use std::{num::NonZeroU64, sync::Arc};
use crate::StateStoreError;
const MAX_NAMESPACE_BYTES: usize = 256;
const MAX_KEY_BYTES: usize = 1024;
const DEFAULT_LIST_LIMIT: usize = 100;
const MAX_LIST_LIMIT: usize = 1000;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StateNamespace(String);
impl StateNamespace {
pub fn new(namespace: impl Into<String>) -> Result<Self, StateStoreError> {
Self::validate_fragment("namespace", namespace.into(), MAX_NAMESPACE_BYTES).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub(super) fn validate_fragment(
label: &'static str,
value: String,
maximum: usize,
) -> Result<String, StateStoreError> {
if value.is_empty() {
return Err(StateStoreError::invalid_request(format!(
"state {label} must not be empty"
)));
}
if value.len() > maximum {
return Err(StateStoreError::invalid_request(format!(
"state {label} must contain at most {maximum} bytes"
)));
}
if value.trim() != value {
return Err(StateStoreError::invalid_request(format!(
"state {label} must not contain leading or trailing whitespace"
)));
}
if value.chars().any(char::is_control) {
return Err(StateStoreError::invalid_request(format!(
"state {label} must not contain control characters"
)));
}
Ok(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StateKeyPrefix(String);
impl StateKeyPrefix {
pub fn new(prefix: impl Into<String>) -> Result<Self, StateStoreError> {
StateNamespace::validate_fragment("prefix", prefix.into(), MAX_KEY_BYTES).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct StateKey {
namespace: StateNamespace,
key: String,
}
impl StateKey {
pub fn new(
namespace: impl Into<String>,
key: impl Into<String>,
) -> Result<Self, StateStoreError> {
Ok(Self {
namespace: StateNamespace::new(namespace)?,
key: StateNamespace::validate_fragment("key", key.into(), MAX_KEY_BYTES)?,
})
}
#[must_use]
pub fn namespace(&self) -> &StateNamespace {
&self.namespace
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StateRevision(NonZeroU64);
impl StateRevision {
pub fn new(value: u64) -> Result<Self, StateStoreError> {
NonZeroU64::new(value)
.map(Self)
.ok_or_else(|| StateStoreError::invalid_request("state revision must be positive"))
}
#[must_use]
pub fn first() -> Self {
Self(NonZeroU64::MIN)
}
#[must_use]
pub fn get(self) -> u64 {
self.0.get()
}
#[cfg(any(feature = "redb", feature = "sqlite"))]
pub(crate) fn next(self) -> Result<Self, StateStoreError> {
self.get()
.checked_add(1)
.ok_or_else(|| StateStoreError::invalid_request("state revision overflow"))
.and_then(Self::new)
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub(crate) fn from_i64(value: i64) -> Result<Self, StateStoreError> {
u64::try_from(value)
.map_err(|error| {
StateStoreError::invalid_request(format!(
"state revision conversion failed: {error}"
))
})
.and_then(Self::new)
}
#[cfg(feature = "redb")]
pub(crate) fn from_backend_u64(value: u64) -> Result<Self, StateStoreError> {
Self::new(value)
}
#[cfg(feature = "sqlite")]
pub(crate) fn to_backend_i64(self) -> Result<i64, StateStoreError> {
i64::try_from(self.get()).map_err(|error| {
StateStoreError::invalid_request(format!(
"state revision exceeds backend signed 64-bit range: {error}"
))
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateRecord {
pub key: StateKey,
pub value: Arc<serde_json::Value>,
}
impl StateRecord {
#[must_use]
pub fn new(key: StateKey, value: serde_json::Value) -> Self {
Self {
key,
value: Arc::new(value),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateRecordSnapshot {
pub record: StateRecord,
pub revision: StateRevision,
}
impl StateRecordSnapshot {
#[must_use]
pub fn new(record: StateRecord, revision: StateRevision) -> Self {
Self { record, revision }
}
}
#[derive(Clone, Debug)]
pub struct StateListRequest {
namespace: StateNamespace,
prefix: Option<StateKeyPrefix>,
limit: usize,
start_after: Option<String>,
}
impl StateListRequest {
pub const DEFAULT_LIMIT: usize = DEFAULT_LIST_LIMIT;
pub const MAX_LIMIT: usize = MAX_LIST_LIMIT;
pub fn new(namespace: impl Into<String>) -> Result<Self, StateStoreError> {
Ok(Self {
namespace: StateNamespace::new(namespace)?,
prefix: None,
limit: DEFAULT_LIST_LIMIT,
start_after: None,
})
}
#[must_use]
pub fn with_key_prefix(mut self, prefix: StateKeyPrefix) -> Self {
self.prefix = Some(prefix);
self
}
pub fn with_limit(mut self, limit: usize) -> Result<Self, StateStoreError> {
if !(1..=MAX_LIST_LIMIT).contains(&limit) {
return Err(StateStoreError::invalid_request(format!(
"state list limit must be between 1 and {MAX_LIST_LIMIT}"
)));
}
self.limit = limit;
Ok(self)
}
pub fn start_after(mut self, cursor: impl Into<String>) -> Result<Self, StateStoreError> {
self.start_after = Some(StateNamespace::validate_fragment(
"start_after",
cursor.into(),
MAX_KEY_BYTES,
)?);
Ok(self)
}
#[must_use]
pub fn namespace(&self) -> &StateNamespace {
&self.namespace
}
#[must_use]
pub fn key_prefix(&self) -> Option<&StateKeyPrefix> {
self.prefix.as_ref()
}
#[must_use]
pub fn prefix(&self) -> Option<&str> {
self.prefix.as_ref().map(StateKeyPrefix::as_str)
}
#[must_use]
pub fn limit(&self) -> usize {
self.limit
}
#[must_use]
pub fn start_after_key(&self) -> Option<&str> {
self.start_after.as_deref()
}
}
#[derive(Clone, Debug, Default)]
pub struct StateListResponse {
pub records: Vec<StateRecordSnapshot>,
pub next_start_after: Option<String>,
}