use async_trait::async_trait;
use bytes::Bytes;
use cid::Cid;
use helia_interface::Helia;
use sha2::{Digest, Sha256};
use std::sync::Arc;
#[derive(Debug, thiserror::Error)]
pub enum StringsError {
#[error("Invalid codec error: {0}")]
InvalidCodec(String),
#[error("Blockstore error: {0}")]
Blockstore(String),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}
#[derive(Default)]
pub struct AddOptions {
}
#[derive(Default)]
pub struct GetOptions {
}
#[async_trait]
pub trait StringsInterface: Send + Sync {
async fn add(&self, string: &str, options: Option<AddOptions>) -> Result<Cid, StringsError>;
async fn get(&self, cid: Cid, options: Option<GetOptions>) -> Result<String, StringsError>;
}
pub struct DefaultStrings {
helia: Arc<dyn Helia>,
}
impl DefaultStrings {
pub fn new(helia: Arc<dyn Helia>) -> Self {
Self { helia }
}
}
#[async_trait]
impl StringsInterface for DefaultStrings {
async fn add(&self, string: &str, options: Option<AddOptions>) -> Result<Cid, StringsError> {
let _options = options.unwrap_or_default();
let data = string.as_bytes();
let mut sha256 = Sha256::new();
sha256.update(data);
let hash_bytes = sha256.finalize();
let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &hash_bytes)
.map_err(|e| StringsError::Blockstore(format!("Multihash error: {}", e)))?;
let cid = Cid::new_v1(0x55, mh);
let bytes = Bytes::from(data.to_vec());
self.helia
.blockstore()
.put(&cid, bytes, None)
.await
.map_err(|e| StringsError::Blockstore(format!("Failed to store block: {}", e)))?;
Ok(cid)
}
async fn get(&self, cid: Cid, _options: Option<GetOptions>) -> Result<String, StringsError> {
match cid.codec() {
0x55 | 0x0129 | 0x0200 => {
}
_ => {
return Err(StringsError::InvalidCodec(
"The passed CID had an incorrect codec, it may correspond to a block data that cannot be interpreted as a string".to_string()
));
}
}
let data = self
.helia
.blockstore()
.get(&cid, None)
.await
.map_err(|e| StringsError::Blockstore(format!("Failed to get block: {}", e)))?;
String::from_utf8(data.to_vec()).map_err(StringsError::Utf8)
}
}
pub fn strings(helia: Arc<dyn Helia>) -> impl StringsInterface {
DefaultStrings::new(helia)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_helia::create_helia_default;
use std::sync::Arc;
async fn create_test_helia() -> Arc<dyn Helia> {
Arc::new(
create_helia_default()
.await
.expect("Failed to create Helia instance"),
)
}
#[tokio::test]
async fn test_add_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let cid = str_interface.add("hello world", None).await.unwrap();
assert_eq!(cid.codec(), 0x55); }
#[tokio::test]
async fn test_get_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let original = "hello world";
let cid = str_interface.add(original, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, original);
}
#[tokio::test]
async fn test_add_get_empty_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let original = "";
let cid = str_interface.add(original, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, original);
}
#[tokio::test]
async fn test_add_get_unicode_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let original = "Hello, δΈη! π";
let cid = str_interface.add(original, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, original);
}
#[tokio::test]
async fn test_add_get_multiline_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let original = "Line 1\nLine 2\nLine 3";
let cid = str_interface.add(original, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, original);
}
#[tokio::test]
async fn test_deterministic_cids() {
let helia1 = create_test_helia().await;
let helia2 = create_test_helia().await;
let str1 = strings(helia1);
let str2 = strings(helia2);
let content = "deterministic content";
let cid1 = str1.add(content, None).await.unwrap();
let cid2 = str2.add(content, None).await.unwrap();
assert_eq!(cid1, cid2, "Same content should produce same CID");
}
#[tokio::test]
async fn test_invalid_codec_error() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &[0u8; 32]).unwrap();
let invalid_cid = Cid::new_v1(0x71, mh);
let result = str_interface.get(invalid_cid, None).await;
assert!(result.is_err());
if let Err(StringsError::InvalidCodec(msg)) = result {
assert!(msg.contains("incorrect codec"));
} else {
panic!("Expected InvalidCodec error");
}
}
#[tokio::test]
async fn test_get_nonexistent_cid() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &[1u8; 32]).unwrap();
let nonexistent_cid = Cid::new_v1(0x55, mh);
let result = str_interface.get(nonexistent_cid, None).await;
assert!(result.is_err());
if let Err(StringsError::Blockstore(_)) = result {
} else {
panic!("Expected Blockstore error");
}
}
#[tokio::test]
async fn test_empty_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let empty = "";
let cid = str_interface.add(empty, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, empty);
assert_eq!(retrieved.len(), 0);
}
#[tokio::test]
async fn test_very_long_string() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let long_string = "a".repeat(10_000);
let cid = str_interface.add(&long_string, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, long_string);
assert_eq!(retrieved.len(), 10_000);
}
#[tokio::test]
async fn test_special_characters() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let special = "!@#$%^&*()_+-=[]{}|;:',.<>?/\\`~\"";
let cid = str_interface.add(special, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, special);
}
#[tokio::test]
async fn test_whitespace_only() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let whitespace = " \t\n\r ";
let cid = str_interface.add(whitespace, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, whitespace);
}
#[tokio::test]
async fn test_json_string_roundtrip() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let json_str = r#"{"name":"Alice","age":30,"active":true}"#;
let cid = str_interface.add(json_str, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, json_str);
}
#[tokio::test]
async fn test_multiple_emojis() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let emojis = "ππππππ
π€£πππ";
let cid = str_interface.add(emojis, None).await.unwrap();
let retrieved = str_interface.get(cid, None).await.unwrap();
assert_eq!(retrieved, emojis);
assert_eq!(retrieved.chars().count(), 10);
}
#[tokio::test]
async fn test_cid_string_format() {
let helia = create_test_helia().await;
let str_interface = strings(helia);
let text = "test content";
let cid = str_interface.add(text, None).await.unwrap();
assert_eq!(cid.version(), cid::Version::V1);
assert_eq!(cid.codec(), 0x55); assert_eq!(cid.hash().code(), 0x12);
let cid_string = cid.to_string();
assert!(cid_string.starts_with("bafkrei"));
}
#[tokio::test]
async fn test_concurrent_adds() {
use tokio::task::JoinSet;
let helia = create_test_helia().await;
let mut set = JoinSet::new();
for i in 0..10 {
let helia_clone = Arc::clone(&helia);
set.spawn(async move {
let str_interface = strings(helia_clone);
str_interface
.add(&format!("concurrent test {}", i), None)
.await
});
}
let mut count = 0;
while let Some(result) = set.join_next().await {
assert!(result.is_ok());
assert!(result.unwrap().is_ok());
count += 1;
}
assert_eq!(count, 10);
}
}