bitpill 0.3.3

A personal medication management TUI application built in Rust.
Documentation
use std::convert::TryFrom;
use std::sync::Arc;

use uuid::Uuid;

use crate::application::{
    dtos::{requests::UpdateMedicationRequest, responses::UpdateMedicationResponse},
    errors::ApplicationError,
    ports::{
        inbound::update_medication_port::UpdateMedicationPort,
        outbound::medication_repository_port::MedicationRepository,
    },
};
use crate::domain::entities::medication::Medication;
use crate::domain::value_objects::medication_id::MedicationId;

pub struct UpdateMedicationService {
    repository: Arc<dyn MedicationRepository>,
}

impl UpdateMedicationService {
    pub fn new(repository: Arc<dyn MedicationRepository>) -> Self {
        Self { repository }
    }
}

impl UpdateMedicationPort for UpdateMedicationService {
    fn execute(
        &self,
        request: UpdateMedicationRequest,
    ) -> Result<UpdateMedicationResponse, ApplicationError> {
        let id = Uuid::parse_str(&request.id)
            .map_err(|_| ApplicationError::InvalidInput("invalid id".into()))?;
        let id_str = request.id.clone();
        let med_id = MedicationId::from(id);

        let medication = Medication::try_from((request, med_id))?;

        self.repository.save(&medication)?;

        Ok(UpdateMedicationResponse { id: id_str })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::application::ports::fakes::FakeMedicationRepository;

    fn make_service(repo: Arc<FakeMedicationRepository>) -> UpdateMedicationService {
        UpdateMedicationService::new(repo)
    }

    fn valid_id() -> String {
        Uuid::now_v7().to_string()
    }

    fn valid_request(id: &str) -> UpdateMedicationRequest {
        UpdateMedicationRequest::new(id, "Ibuprofen", 200, vec![(8, 0)], "OnceDaily")
    }

    #[test]
    fn execute_with_valid_request_returns_same_id() {
        let repo = Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let id = valid_id();

        let result = service.execute(valid_request(&id));

        assert!(result.is_ok());
        assert_eq!(result.unwrap().id, id);
    }

    #[test]
    fn execute_with_valid_request_saves_to_repository() {
        let repo = Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo.clone());

        service.execute(valid_request(&valid_id())).unwrap();

        assert_eq!(repo.saved_count(), 1);
    }

    #[test]
    fn execute_with_invalid_uuid_returns_invalid_input_error() {
        let repo = Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let request =
            UpdateMedicationRequest::new("not-a-uuid", "Ibuprofen", 200, vec![(8, 0)], "OnceDaily");

        let result = service.execute(request);

        assert!(matches!(result, Err(ApplicationError::InvalidInput(_))));
    }

    #[test]
    fn execute_with_empty_name_returns_domain_error() {
        let repo = Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let request = UpdateMedicationRequest::new(valid_id(), "", 200, vec![(8, 0)], "OnceDaily");

        let result = service.execute(request);

        assert!(matches!(result, Err(ApplicationError::Domain(_))));
    }

    #[test]
    fn execute_with_zero_dosage_returns_domain_error() {
        let repo = Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let request =
            UpdateMedicationRequest::new(valid_id(), "Ibuprofen", 0, vec![(8, 0)], "OnceDaily");

        let result = service.execute(request);

        assert!(matches!(result, Err(ApplicationError::Domain(_))));
    }

    #[test]
    fn execute_when_repository_fails_returns_storage_error() {
        let repo = Arc::new(FakeMedicationRepository::failing());
        let service = make_service(repo);

        let result = service.execute(valid_request(&valid_id()));

        assert!(matches!(result, Err(ApplicationError::Storage(_))));
    }
}