use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use serde::{
Deserialize, Deserializer, Serialize, Serializer, de,
ser::{SerializeMap, SerializeStruct},
};
use crate::StateStoreError;
const DEFAULT_OPERATION_TIMEOUT_MS: u64 = 5_000;
pub enum SqliteStateStoreLocation {
Memory,
File(PathBuf),
}
impl SqliteStateStoreLocation {
#[must_use]
pub fn is_memory(&self) -> bool {
match self {
Self::Memory => true,
Self::File(_) => false,
}
}
pub(crate) fn path_for_turso(&self) -> Result<&str, StateStoreError> {
match self {
Self::Memory => Ok(":memory:"),
Self::File(path) => Self::validate_file_path(path),
}
}
pub(crate) fn validate(&self) -> Result<(), StateStoreError> {
match self {
Self::Memory => Ok(()),
Self::File(path) => Self::validate_file_path(path).map(|_| ()),
}
}
fn validate_file_path(path: &Path) -> Result<&str, StateStoreError> {
let value = path.to_str().ok_or_else(|| {
StateStoreError::invalid_config("SQLite state store file path must be valid UTF-8")
})?;
if value.is_empty() {
return Err(StateStoreError::invalid_config(
"SQLite state store file path must not be empty",
));
}
if value.chars().any(|character| character == '\0') {
return Err(StateStoreError::invalid_config(
"SQLite state store file path must not contain NUL",
));
}
Ok(value)
}
fn from_json_value(value: &serde_json::Value) -> Result<Self, String> {
if value.as_str() == Some("Memory") {
return Ok(Self::Memory);
}
let object = value.as_object().ok_or_else(|| {
"SqliteStateStoreLocation must be `Memory` or `{ File: ... }`".to_owned()
})?;
if object.len() != 1 {
return Err(
"SqliteStateStoreLocation object must contain exactly one variant".to_owned(),
);
}
let file = object
.get("File")
.ok_or_else(|| "SqliteStateStoreLocation object must contain `File`".to_owned())?;
let path = file
.as_str()
.ok_or_else(|| "SqliteStateStoreLocation.File must be a string".to_owned())?;
Ok(Self::File(PathBuf::from(path)))
}
}
impl Clone for SqliteStateStoreLocation {
fn clone(&self) -> Self {
match self {
Self::Memory => Self::Memory,
Self::File(path) => Self::File(path.clone()),
}
}
}
impl fmt::Debug for SqliteStateStoreLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory => f.write_str("Memory"),
Self::File(path) => f.debug_tuple("File").field(path).finish(),
}
}
}
impl PartialEq for SqliteStateStoreLocation {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Memory, Self::Memory) => true,
(Self::File(left), Self::File(right)) => left == right,
_ => false,
}
}
}
impl Eq for SqliteStateStoreLocation {}
impl Serialize for SqliteStateStoreLocation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Memory => serializer.serialize_str("Memory"),
Self::File(path) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("File", path)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for SqliteStateStoreLocation {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
Self::from_json_value(&value).map_err(de::Error::custom)
}
}
pub struct SqliteStateStoreConfig {
location: SqliteStateStoreLocation,
operation_timeout_ms: u64,
}
impl SqliteStateStoreConfig {
#[must_use]
pub fn memory() -> Self {
Self {
location: SqliteStateStoreLocation::Memory,
operation_timeout_ms: DEFAULT_OPERATION_TIMEOUT_MS,
}
}
#[must_use]
pub fn file(path: impl Into<PathBuf>) -> Self {
Self {
location: SqliteStateStoreLocation::File(path.into()),
operation_timeout_ms: DEFAULT_OPERATION_TIMEOUT_MS,
}
}
#[must_use]
pub fn with_operation_timeout(mut self, timeout: Duration) -> Self {
let millis = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
self.operation_timeout_ms = millis;
self
}
#[must_use]
pub fn operation_timeout(&self) -> Duration {
Duration::from_millis(self.operation_timeout_ms)
}
#[must_use]
pub fn location(&self) -> &SqliteStateStoreLocation {
&self.location
}
pub fn validate(&self) -> Result<(), StateStoreError> {
if self.operation_timeout_ms == 0 {
return Err(StateStoreError::invalid_config(
"SQLite state store operation_timeout must be greater than zero",
));
}
self.location.validate()
}
pub(crate) fn path_for_turso(&self) -> Result<&str, StateStoreError> {
self.location.path_for_turso()
}
pub(crate) fn operation_timeout_ms(&self) -> u64 {
self.operation_timeout_ms
}
pub(crate) fn from_json_value(value: &serde_json::Value) -> Result<Self, String> {
let object = value
.as_object()
.ok_or_else(|| "SqliteStateStoreConfig must be an object".to_owned())?;
let location = object
.get("location")
.ok_or_else(|| "SqliteStateStoreConfig.location is required".to_owned())
.and_then(SqliteStateStoreLocation::from_json_value)?;
let timeout = object
.get("operation_timeout_ms")
.ok_or_else(|| "SqliteStateStoreConfig.operation_timeout_ms is required".to_owned())?
.as_u64()
.ok_or_else(|| {
"SqliteStateStoreConfig.operation_timeout_ms must be a u64".to_owned()
})?;
Ok(Self {
location,
operation_timeout_ms: timeout,
})
}
}
impl Clone for SqliteStateStoreConfig {
fn clone(&self) -> Self {
Self {
location: self.location.clone(),
operation_timeout_ms: self.operation_timeout_ms,
}
}
}
impl fmt::Debug for SqliteStateStoreConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqliteStateStoreConfig")
.field("location", &self.location)
.field("operation_timeout_ms", &self.operation_timeout_ms)
.finish()
}
}
impl Serialize for SqliteStateStoreConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("SqliteStateStoreConfig", 2)?;
state.serialize_field("location", &self.location)?;
state.serialize_field("operation_timeout_ms", &self.operation_timeout_ms)?;
state.end()
}
}
impl<'de> Deserialize<'de> for SqliteStateStoreConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
Self::from_json_value(&value).map_err(de::Error::custom)
}
}