use crate::entity_service::{Entity, EntityService, EntityServiceError};
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("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>>,
validator: InputValidator,
}
impl LinkingService {
pub fn new(entity_service: Arc<RwLock<EntityService>>) -> Self {
Self {
entity_service,
validator: InputValidator::default(),
}
}
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 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 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 create_local_entity(
&self,
name: String,
entity_type: crate::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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EntityType;
use crate::crdt_manager::CrdtManager;
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 service = LinkingService::new(entity_service);
(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)));
LinkingService::new(entity_service)
}
#[tokio::test]
async fn test_validate_four_words_format() {
let service = create_simple_test_service().await;
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_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());
}
}