use async_trait::async_trait;
use super::{DbTransactionFinalizer, Error};
pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
return Err(Error::KVStoreInvalidKey(format!(
"{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
)));
}
if !s
.chars()
.all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c))
{
return Err(Error::KVStoreInvalidKey("key contains invalid characters. Only ASCII letters, numbers, underscore, and hyphen are allowed".to_string()));
}
Ok(())
}
pub fn validate_kvstore_params(
primary_namespace: &str,
secondary_namespace: &str,
key: Option<&str>,
) -> Result<(), Error> {
validate_kvstore_string(primary_namespace)?;
validate_kvstore_string(secondary_namespace)?;
if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
return Err(Error::KVStoreInvalidKey(
"If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
));
}
if let Some(key) = key {
validate_kvstore_string(key)?;
let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
if key == primary_namespace || key == secondary_namespace || key == namespace_key {
return Err(Error::KVStoreInvalidKey(format!(
"Key '{key}' conflicts with namespace names"
)));
}
}
Ok(())
}
#[async_trait]
pub trait KVStoreTransaction<Error>: DbTransactionFinalizer<Err = Error> {
async fn kv_read(
&mut self,
primary_namespace: &str,
secondary_namespace: &str,
key: &str,
) -> Result<Option<Vec<u8>>, Error>;
async fn kv_write(
&mut self,
primary_namespace: &str,
secondary_namespace: &str,
key: &str,
value: &[u8],
) -> Result<(), Error>;
async fn kv_remove(
&mut self,
primary_namespace: &str,
secondary_namespace: &str,
key: &str,
) -> Result<(), Error>;
async fn kv_list(
&mut self,
primary_namespace: &str,
secondary_namespace: &str,
) -> Result<Vec<String>, Error>;
}
#[async_trait]
pub trait KVStoreDatabase {
type Err: Into<Error> + From<Error>;
async fn kv_read(
&self,
primary_namespace: &str,
secondary_namespace: &str,
key: &str,
) -> Result<Option<Vec<u8>>, Self::Err>;
async fn kv_list(
&self,
primary_namespace: &str,
secondary_namespace: &str,
) -> Result<Vec<String>, Self::Err>;
}
#[async_trait]
pub trait KVStore: KVStoreDatabase {
async fn begin_transaction(
&self,
) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, Error>;
}
#[cfg(test)]
mod tests {
use super::{
validate_kvstore_params, validate_kvstore_string, KVSTORE_NAMESPACE_KEY_ALPHABET,
KVSTORE_NAMESPACE_KEY_MAX_LEN,
};
#[test]
fn test_validate_kvstore_string_valid_inputs() {
assert!(validate_kvstore_string("").is_ok());
assert!(validate_kvstore_string("abc").is_ok());
assert!(validate_kvstore_string("ABC").is_ok());
assert!(validate_kvstore_string("123").is_ok());
assert!(validate_kvstore_string("test_key").is_ok());
assert!(validate_kvstore_string("test-key").is_ok());
assert!(validate_kvstore_string("test_KEY-123").is_ok());
let max_length_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN);
assert!(validate_kvstore_string(&max_length_str).is_ok());
}
#[test]
fn test_validate_kvstore_string_invalid_length() {
let too_long_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN + 1);
let result = validate_kvstore_string(&too_long_str);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("exceeds maximum length"));
}
#[test]
fn test_validate_kvstore_string_invalid_characters() {
let invalid_chars = vec![
"test@key", "test key", "test.key", "test/key", "test\\key", "test+key", "test=key", "test!key", "test#key", "test$key", "test%key", "test&key", "test*key", "test(key", "test)key", "test[key", "test]key", "test{key", "test}key", "test|key", "test;key", "test:key", "test'key", "test\"key", "test<key", "test>key", "test,key", "test?key", "test~key", "test`key", ];
for invalid_str in invalid_chars {
let result = validate_kvstore_string(invalid_str);
assert!(result.is_err(), "Expected '{}' to be invalid", invalid_str);
assert!(result
.unwrap_err()
.to_string()
.contains("invalid characters"));
}
}
#[test]
fn test_validate_kvstore_params_valid() {
assert!(validate_kvstore_params("primary", "secondary", Some("key")).is_ok());
assert!(validate_kvstore_params("primary", "", Some("key")).is_ok());
assert!(validate_kvstore_params("", "", Some("key")).is_ok());
assert!(validate_kvstore_params("p1", "s1", Some("different_key")).is_ok());
}
#[test]
fn test_validate_kvstore_params_empty_namespace_rules() {
let result = validate_kvstore_params("", "secondary", Some("key"));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("If primary_namespace is empty"));
}
#[test]
fn test_validate_kvstore_params_collision_prevention() {
let test_cases = vec![
("primary", "secondary", "primary"), ("primary", "secondary", "secondary"), ];
for (primary, secondary, key) in test_cases {
let result = validate_kvstore_params(primary, secondary, Some(key));
assert!(
result.is_err(),
"Expected collision for key '{}' with namespaces '{}'/'{}'",
key,
primary,
secondary
);
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("conflicts with namespace"));
}
let result = validate_kvstore_params("primary", "secondary", Some("primary_secondary"));
assert!(result.is_ok(), "This should be valid - no actual collision");
}
#[test]
fn test_validate_kvstore_params_invalid_strings() {
let result = validate_kvstore_params("primary@", "secondary", Some("key"));
assert!(result.is_err());
let result = validate_kvstore_params("primary", "secondary!", Some("key"));
assert!(result.is_err());
let result = validate_kvstore_params("primary", "secondary", Some("key with space"));
assert!(result.is_err());
}
#[test]
fn test_alphabet_constants() {
assert_eq!(
KVSTORE_NAMESPACE_KEY_ALPHABET,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
);
assert_eq!(KVSTORE_NAMESPACE_KEY_MAX_LEN, 120);
}
#[test]
fn test_alphabet_coverage() {
for ch in KVSTORE_NAMESPACE_KEY_ALPHABET.chars() {
let test_str = ch.to_string();
assert!(
validate_kvstore_string(&test_str).is_ok(),
"Character '{}' should be valid",
ch
);
}
}
#[test]
fn test_namespace_segmentation_examples() {
let valid_examples = vec![
("wallets", "user123", "balance"),
("quotes", "mint", "quote_12345"),
("keysets", "", "active_keyset"),
("", "", "global_config"),
("auth", "session_456", "token"),
("mint_info", "", "version"),
];
for (primary, secondary, key) in valid_examples {
assert!(
validate_kvstore_params(primary, secondary, Some(key)).is_ok(),
"Valid example should pass: '{}'/'{}'/'{}'",
primary,
secondary,
key
);
}
}
#[test]
fn test_per_namespace_uniqueness() {
assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok());
assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); }
}