use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::types::script_value::ScriptValue;
pub const MAX_SERIALISED_BYTES: usize = 1024 * 1024;
pub const MAX_KEY_BYTES: usize = 512;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ScriptState(BTreeMap<String, ScriptValue>);
impl ScriptState {
#[must_use]
pub fn get(&self, key: &str) -> Option<&ScriptValue> {
self.0.get(key)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn apply(&mut self, write: &ScriptStateWrite) -> Result<WriteOutcome, String> {
Self::check_key(&write.key)?;
if let Expected::Exactly(expected) = &write.expected {
let current = self.0.get(&write.key);
if current != expected.as_ref() {
return Ok(WriteOutcome::Conflict {
current: current.cloned(),
});
}
}
self.check_capacity(write)?;
let changed = match &write.value {
Some(value) => match self.0.get(&write.key) {
Some(existing) if existing == value => false,
_ => {
self.0.insert(write.key.clone(), value.clone());
true
}
},
None => self.0.remove(&write.key).is_some(),
};
Ok(WriteOutcome::Applied { changed })
}
pub fn check_key(key: &str) -> Result<(), String> {
if key.is_empty() {
return Err("key must not be empty".to_string());
}
if key.len() > MAX_KEY_BYTES {
return Err(format!(
"key is {} bytes, over the {MAX_KEY_BYTES} byte limit",
key.len()
));
}
Ok(())
}
pub fn check_capacity(&self, write: &ScriptStateWrite) -> Result<(), String> {
let Some(value) = &write.value else {
return Ok(());
};
let mut trial = self.clone();
trial.0.insert(write.key.clone(), value.clone());
let size = serde_json::to_vec(&trial)
.map_err(|err| format!("value could not be stored: {err}"))?
.len();
if size > MAX_SERIALISED_BYTES {
return Err(format!(
"store would be {size} bytes, over the {MAX_SERIALISED_BYTES} byte limit"
));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScriptStateWrite {
pub key: String,
pub value: Option<ScriptValue>,
pub expected: Expected,
}
impl ScriptStateWrite {
#[must_use]
pub fn set(key: String, value: ScriptValue) -> Self {
Self {
key,
value: Some(value),
expected: Expected::Anything,
}
}
#[must_use]
pub fn remove(key: String) -> Self {
Self {
key,
value: None,
expected: Expected::Anything,
}
}
#[must_use]
pub fn compare_and_set(
key: String,
expected: Option<ScriptValue>,
value: Option<ScriptValue>,
) -> Self {
Self {
key,
value,
expected: Expected::Exactly(expected),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Expected {
Anything,
Exactly(Option<ScriptValue>),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WriteOutcome {
Applied {
changed: bool,
},
Conflict { current: Option<ScriptValue> },
}
impl WriteOutcome {
pub fn to_json(&self) -> serde_json::Result<serde_json::Value> {
Ok(crate::types::json::flatten_tag(
serde_json::to_value(self)?,
"outcome",
))
}
}