use crate::entity_service::{Entity, EntityService, EntityServiceError};
use crate::gossip::contact_storage::{
ContactRecord, ContactResult, ContactStorageError, ContactStore,
};
use crate::identity::validate_id_words;
use crate::security::input_validation::InputValidator;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
#[derive(Debug, Error)]
pub enum LinkingError {
#[error("Invalid four-word address: {0}")]
InvalidFourWords(String),
#[error("Entity not found: {0}")]
EntityNotFound(String),
#[error("Contact not found: {0}")]
ContactNotFound(String),
#[error("Entity service error: {0}")]
EntityServiceError(#[from] EntityServiceError),
#[error("Contact storage error: {0}")]
ContactStorageError(#[from] ContactStorageError),
#[error("Already linked: {0}")]
AlreadyLinked(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
pub type LinkingResult<T> = Result<T, LinkingError>;
#[derive(Debug, Clone)]
pub struct SyncResult {
pub success: bool,
pub id: String,
pub changes_pushed: usize,
pub changes_pulled: usize,
pub error: Option<String>,
}
impl SyncResult {
pub fn success(id: String, pushed: usize, pulled: usize) -> Self {
Self {
success: true,
id,
changes_pushed: pushed,
changes_pulled: pulled,
error: None,
}
}
pub fn failure(id: String, error: String) -> Self {
Self {
success: false,
id,
changes_pushed: 0,
changes_pulled: 0,
error: Some(error),
}
}
}
pub struct LinkingService {
entity_service: Arc<RwLock<EntityService>>,
contact_store: Arc<ContactStore>,
validator: InputValidator,
}
impl LinkingService {
pub fn new(
entity_service: Arc<RwLock<EntityService>>,
contact_store: Arc<ContactStore>,
) -> Self {
Self {
entity_service,
contact_store,
validator: InputValidator::new(),
}
}
pub fn validate_four_words(&self, four_words: &str) -> LinkingResult<String> {
let normalized = self
.validator
.validate_four_words(four_words)
.map_err(|e| LinkingError::ValidationError(e.to_string()))?;
if !validate_id_words(&normalized) {
return Err(LinkingError::InvalidFourWords(format!(
"'{}' contains words not in dictionary",
normalized
)));
}
Ok(normalized)
}
pub fn is_valid_four_words(&self, four_words: &str) -> bool {
self.validate_four_words(four_words).is_ok()
}
pub async fn link_entity(&self, entity_id: &str, four_words: &str) -> LinkingResult<Entity> {
let normalized = self.validate_four_words(four_words)?;
let entity_service = self.entity_service.write().await;
let entity = entity_service
.link_entity_to_network(entity_id, &normalized)
.await?;
Ok(entity)
}
pub async fn link_contact(
&self,
contact_id: &str,
four_words: &str,
) -> LinkingResult<ContactRecord> {
let normalized = self.validate_four_words(four_words)?;
let contact = self
.contact_store
.link_contact(contact_id, &normalized)
.await?;
Ok(contact)
}
pub async fn get_local_only_entities(&self) -> LinkingResult<Vec<Entity>> {
let entity_service = self.entity_service.read().await;
let all_entities = entity_service.list_entities().await?;
Ok(all_entities
.into_iter()
.filter(|e| e.is_local_only)
.collect())
}
pub async fn get_linked_entities(&self) -> LinkingResult<Vec<Entity>> {
let entity_service = self.entity_service.read().await;
let all_entities = entity_service.list_entities().await?;
Ok(all_entities.into_iter().filter(|e| e.is_linked()).collect())
}
pub async fn get_local_only_contacts(&self) -> Vec<ContactRecord> {
self.contact_store.local_only().await
}
pub async fn get_linked_contacts(&self) -> Vec<ContactRecord> {
self.contact_store.network_linked().await
}
pub async fn mark_entity_synced(&self, entity_id: &str) -> LinkingResult<Entity> {
let entity_service = self.entity_service.write().await;
let entity = entity_service.mark_entity_synced(entity_id).await?;
Ok(entity)
}
pub async fn mark_contact_synced(&self, contact_id: &str) -> LinkingResult<ContactRecord> {
let contact = self
.contact_store
.get_by_id(contact_id)
.await
.ok_or_else(|| LinkingError::ContactNotFound(contact_id.to_string()))?;
let mut updated = contact;
updated.mark_synced();
let contacts = self.contact_store.all().await;
self.contact_store.clear().await;
for c in contacts {
if c.id == contact_id {
let _ = self.contact_store.add(updated.clone()).await;
} else {
let _ = self.contact_store.add(c).await;
}
}
Ok(updated)
}
pub async fn create_local_entity(
&self,
name: String,
entity_type: crate::legacy_crdt::EntityType,
description: Option<String>,
created_by: String,
) -> LinkingResult<Entity> {
let entity_service = self.entity_service.write().await;
let entity = entity_service
.create_local_entity(name, entity_type, description, created_by)
.await?;
Ok(entity)
}
pub async fn create_local_contact(&self, display_name: String) -> ContactResult<ContactRecord> {
let contact = ContactRecord::new_local(display_name);
self.contact_store.add(contact.clone()).await?;
Ok(contact)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crdt_manager::CrdtManager;
use crate::legacy_crdt::EntityType;
use tempfile::TempDir;
async fn create_test_service() -> (LinkingService, TempDir) {
let temp_dir = TempDir::new().unwrap();
let data_dir = temp_dir.path().to_path_buf();
let crdt_manager = Arc::new(CrdtManager::new(data_dir.clone()).await.unwrap());
let entity_service = Arc::new(RwLock::new(EntityService::new(crdt_manager)));
let contact_store = Arc::new(ContactStore::new());
let service = LinkingService::new(entity_service, contact_store);
(service, temp_dir)
}
async fn create_simple_test_service() -> LinkingService {
let temp_dir = TempDir::new().unwrap();
let data_dir = temp_dir.path().to_path_buf();
let crdt_manager = Arc::new(CrdtManager::new(data_dir).await.unwrap());
let entity_service = Arc::new(RwLock::new(EntityService::new(crdt_manager)));
let contact_store = Arc::new(ContactStore::new());
LinkingService::new(entity_service, contact_store)
}
#[tokio::test]
async fn test_validate_four_words_format() {
let service = create_simple_test_service().await;
let _result = service.validate_four_words("hello-world-test-network");
assert!(
service
.validator
.validate_four_words("hello-world-test-network")
.is_ok()
);
assert!(service.validate_four_words("only-three-words").is_err());
assert!(service.validate_four_words("").is_err());
assert!(
service
.validate_four_words("too-many-words-here-now")
.is_err()
);
}
#[tokio::test]
async fn test_create_local_contact() {
let (service, _temp_dir) = create_test_service().await;
let contact = service
.create_local_contact("Alice".to_string())
.await
.unwrap();
assert!(contact.is_local_only);
assert!(contact.four_words.is_none());
assert_eq!(contact.display_name, Some("Alice".to_string()));
assert_eq!(contact.effective_name(), "Alice");
}
#[tokio::test]
async fn test_get_local_only_contacts() {
let (service, _temp_dir) = create_test_service().await;
service
.create_local_contact("Alice".to_string())
.await
.unwrap();
service
.create_local_contact("Bob".to_string())
.await
.unwrap();
let linked = ContactRecord::new("ocean-forest-moon-star".to_string());
service.contact_store.add(linked).await.unwrap();
let local_only = service.get_local_only_contacts().await;
assert_eq!(local_only.len(), 2);
let linked = service.get_linked_contacts().await;
assert_eq!(linked.len(), 1);
}
#[tokio::test]
async fn test_create_local_entity() {
let (service, _temp_dir) = create_test_service().await;
let entity = service
.create_local_entity(
"Test Org".to_string(),
EntityType::Organisation,
Some("A test organisation".to_string()),
"creator-id".to_string(),
)
.await
.unwrap();
assert!(entity.is_local_only);
assert!(entity.network_four_words.is_none());
assert_eq!(entity.name, "Test Org");
assert!(!entity.is_linked());
}
}