use axum::{ extract::{Path, Query, State}, http::StatusCode, response::Json, routing::{delete, get, post, put}, Router, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; use tracing::info; use crate::address_book::{Contact, ContactId, Group, GroupId, AddressBookStore, CreateContactRequest, UpdateContactRequest, CreateGroupRequest, UpdateGroupRequest}; #[derive(Clone)] pub struct AddressBookState { pub store: Arc>, } #[derive(Serialize)] pub struct ContactResponse { pub id: String, pub name: String, pub fax_number: String, pub company: Option, pub email: Option, pub phone: Option, pub category: Option, pub notes: Option, pub is_favorite: bool, pub created_at: String, pub updated_at: String, } impl From for ContactResponse { fn from(contact: Contact) -> Self { Self { id: contact.id.to_string(), name: contact.name, fax_number: contact.fax_number, company: contact.company, email: contact.email, phone: contact.phone, category: contact.category, notes: contact.notes, is_favorite: contact.is_favorite, created_at: contact.created_at.to_rfc3339(), updated_at: contact.updated_at.to_rfc3339(), } } } #[derive(Serialize)] pub struct ContactListResponse { pub contacts: Vec, pub total: usize, } #[derive(Serialize)] pub struct GroupResponse { pub id: String, pub name: String, pub description: Option, pub color: Option, pub created_at: String, pub updated_at: String, } impl From for GroupResponse { fn from(group: Group) -> Self { Self { id: group.id.to_string(), name: group.name, description: group.description, color: group.color, created_at: group.created_at.to_rfc3339(), updated_at: group.updated_at.to_rfc3339(), } } } #[derive(Serialize)] pub struct GroupListResponse { pub groups: Vec, pub total: usize, } #[derive(Deserialize)] pub struct SearchQuery { pub query: Option, pub category: Option, } async fn create_contact( State(state): State, Json(req): Json, ) -> Result, (StatusCode, Json)> { let mut contact = Contact::new(req.name, req.fax_number); contact.company = req.company; contact.email = req.email; contact.phone = req.phone; contact.category = req.category; contact.notes = req.notes; let store = state.store.lock().await; store.create_contact(&contact).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "create_contact_failed".to_string(), message: e.to_string(), }), ) })?; info!("Created contact: {} - {}", contact.name, contact.fax_number); Ok(Json(ContactResponse::from(contact))) } async fn get_contact( State(state): State, Path(id): Path, ) -> Result, (StatusCode, Json)> { let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_contact_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; let contact = store.get_contact(&contact_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "get_contact_failed".to_string(), message: e.to_string(), }), ) })?; match contact { Some(c) => Ok(Json(ContactResponse::from(c))), None => Err(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "contact_not_found".to_string(), message: "Contact not found".to_string(), }), )), } } async fn list_contacts( State(state): State, Query(query): Query, ) -> Result, (StatusCode, Json)> { let store = state.store.lock().await; let contacts = if let Some(search_query) = query.query { store.search_contacts(&search_query).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "search_contacts_failed".to_string(), message: e.to_string(), }), ) })? } else if let Some(category) = query.category { store.list_contacts(Some(&category)).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "list_contacts_failed".to_string(), message: e.to_string(), }), ) })? } else { store.list_contacts(None).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "list_contacts_failed".to_string(), message: e.to_string(), }), ) })? }; let total = contacts.len(); Ok(Json(ContactListResponse { contacts: contacts.into_iter().map(ContactResponse::from).collect(), total, })) } async fn update_contact( State(state): State, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_contact_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; let mut contact = store.get_contact(&contact_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "get_contact_failed".to_string(), message: e.to_string(), }), ) })?.ok_or(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "contact_not_found".to_string(), message: "Contact not found".to_string(), }), ))?; if let Some(name) = req.name { contact.name = name; } if let Some(fax_number) = req.fax_number { contact.fax_number = fax_number; } if let Some(company) = req.company { contact.company = Some(company); } if let Some(email) = req.email { contact.email = Some(email); } if let Some(phone) = req.phone { contact.phone = Some(phone); } if let Some(category) = req.category { contact.category = Some(category); } if let Some(notes) = req.notes { contact.notes = Some(notes); } if let Some(is_favorite) = req.is_favorite { contact.is_favorite = is_favorite; } contact.update(); store.update_contact(&contact_id, &contact).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "update_contact_failed".to_string(), message: e.to_string(), }), ) })?; info!("Updated contact: {} - {}", contact.name, contact.fax_number); Ok(Json(ContactResponse::from(contact))) } async fn delete_contact( State(state): State, Path(id): Path, ) -> Result)> { let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_contact_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; store.delete_contact(&contact_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "delete_contact_failed".to_string(), message: e.to_string(), }), ) })?; info!("Deleted contact: {}", id); Ok(StatusCode::NO_CONTENT) } async fn create_group( State(state): State, Json(req): Json, ) -> Result, (StatusCode, Json)> { let mut group = Group::new(req.name); group.description = req.description; group.color = req.color; let store = state.store.lock().await; store.create_group(&group).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "create_group_failed".to_string(), message: e.to_string(), }), ) })?; info!("Created group: {}", group.name); Ok(Json(GroupResponse::from(group))) } async fn get_group( State(state): State, Path(id): Path, ) -> Result, (StatusCode, Json)> { let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; let group = store.get_group(&group_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "get_group_failed".to_string(), message: e.to_string(), }), ) })?; match group { Some(g) => Ok(Json(GroupResponse::from(g))), None => Err(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "group_not_found".to_string(), message: "Group not found".to_string(), }), )), } } async fn list_groups( State(state): State, ) -> Result, (StatusCode, Json)> { let store = state.store.lock().await; let groups = store.list_groups().map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "list_groups_failed".to_string(), message: e.to_string(), }), ) })?; let total = groups.len(); Ok(Json(GroupListResponse { groups: groups.into_iter().map(GroupResponse::from).collect(), total, })) } async fn update_group( State(state): State, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; let mut group = store.get_group(&group_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "get_group_failed".to_string(), message: e.to_string(), }), ) })?.ok_or(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "group_not_found".to_string(), message: "Group not found".to_string(), }), ))?; if let Some(name) = req.name { group.name = name; } if let Some(description) = req.description { group.description = Some(description); } if let Some(color) = req.color { group.color = Some(color); } group.update(); store.update_group(&group_id, &group).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "update_group_failed".to_string(), message: e.to_string(), }), ) })?; info!("Updated group: {}", group.name); Ok(Json(GroupResponse::from(group))) } async fn delete_group( State(state): State, Path(id): Path, ) -> Result)> { let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; store.delete_group(&group_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "delete_group_failed".to_string(), message: e.to_string(), }), ) })?; info!("Deleted group: {}", id); Ok(StatusCode::NO_CONTENT) } async fn add_contact_to_group( State(state): State, Path((contact_id, group_id)): Path<(String, String)>, ) -> Result)> { let contact_uuid = ContactId::from_uuid(uuid::Uuid::parse_str(&contact_id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_contact_id".to_string(), message: e.to_string(), }), ) })?); let group_uuid = GroupId::from_uuid(uuid::Uuid::parse_str(&group_id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; store.add_contact_to_group(&contact_uuid, &group_uuid).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "add_contact_to_group_failed".to_string(), message: e.to_string(), }), ) })?; info!("Added contact {} to group {}", contact_id, group_id); Ok(StatusCode::NO_CONTENT) } async fn remove_contact_from_group( State(state): State, Path((contact_id, group_id)): Path<(String, String)>, ) -> Result)> { let contact_uuid = ContactId::from_uuid(uuid::Uuid::parse_str(&contact_id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_contact_id".to_string(), message: e.to_string(), }), ) })?); let group_uuid = GroupId::from_uuid(uuid::Uuid::parse_str(&group_id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; store.remove_contact_from_group(&contact_uuid, &group_uuid).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "remove_contact_from_group_failed".to_string(), message: e.to_string(), }), ) })?; info!("Removed contact {} from group {}", contact_id, group_id); Ok(StatusCode::NO_CONTENT) } async fn get_contacts_in_group( State(state): State, Path(id): Path, ) -> Result, (StatusCode, Json)> { let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "invalid_group_id".to_string(), message: e.to_string(), }), ) })?); let store = state.store.lock().await; let contacts = store.get_contacts_in_group(&group_id).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "get_contacts_in_group_failed".to_string(), message: e.to_string(), }), ) })?; let total = contacts.len(); Ok(Json(ContactListResponse { contacts: contacts.into_iter().map(ContactResponse::from).collect(), total, })) } #[derive(Serialize)] pub struct ErrorResponse { pub error: String, pub message: String, } pub fn address_book_routes(store: Arc>) -> Router { let state = AddressBookState { store }; Router::new() .route("/contacts", post(create_contact)) .route("/contacts", get(list_contacts)) .route("/contacts/:id", get(get_contact)) .route("/contacts/:id", put(update_contact)) .route("/contacts/:id", delete(delete_contact)) .route("/groups", post(create_group)) .route("/groups", get(list_groups)) .route("/groups/:id", get(get_group)) .route("/groups/:id", put(update_group)) .route("/groups/:id", delete(delete_group)) .route("/groups/:id/contacts", get(get_contacts_in_group)) .route("/contacts/:contact_id/groups/:group_id", post(add_contact_to_group)) .route("/contacts/:contact_id/groups/:group_id", delete(remove_contact_from_group)) .with_state(state) }