fish-lib 0.2.3

A work-in-progress fishing game library containing the game/storage logic for a discord fishing game I'm working on.
Documentation
use crate::data::location_data::LocationData;
use crate::dto::location_unlock_requirements::LocationUnlockRequirements;
use crate::game::errors::resource::GameResourceError;
use crate::game::errors::GameResult;
use crate::game::repositories::fishing_history_entry_repository::FishingHistoryEntryRepositoryInterface;
use crate::game::repositories::user_repository::UserRepositoryInterface;
use crate::models::user::{NewUser, User};
use crate::models::user_location::UserLocation;
use std::sync::Arc;

pub trait UserServiceInterface: Send + Sync {
    fn create_and_save_user(&self, external_id: i64) -> GameResult<User>;
    fn get_unmet_location_unlock_requirements(
        &self,
        user: &User,
        location: Arc<LocationData>,
    ) -> GameResult<LocationUnlockRequirements>;
    fn unlock_location(
        &self,
        user: &User,
        location_data: Arc<LocationData>,
    ) -> GameResult<UserLocation>;
    fn get_unlocked_locations(&self, user: &User) -> GameResult<Vec<UserLocation>>;
    fn get_unlocked_location_ids(&self, user: &User) -> GameResult<Vec<i32>>;
}

pub struct UserService {
    fishing_history_entry_repository: Arc<dyn FishingHistoryEntryRepositoryInterface>,
    user_repository: Arc<dyn UserRepositoryInterface>,
}

impl UserService {
    pub fn new(
        fishing_history_entry_repository: Arc<dyn FishingHistoryEntryRepositoryInterface>,
        user_repository: Arc<dyn UserRepositoryInterface>,
    ) -> UserService {
        UserService {
            fishing_history_entry_repository,
            user_repository,
        }
    }
}

impl UserServiceInterface for UserService {
    fn create_and_save_user(&self, external_id: i64) -> GameResult<User> {
        let user = NewUser { external_id };
        Ok(self.user_repository.create(user)?)
    }

    fn get_unmet_location_unlock_requirements(
        &self,
        user: &User,
        location: Arc<LocationData>,
    ) -> GameResult<LocationUnlockRequirements> {
        let unlocked_location_ids = self.get_unlocked_location_ids(user)?;
        let missing_location_ids: Vec<i32> = location
            .required_locations_unlocked
            .iter()
            .copied()
            .filter(|required_location_id| !unlocked_location_ids.contains(required_location_id))
            .collect();

        let caught_species_ids = self
            .fishing_history_entry_repository
            .find_caught_species_ids_by_user(user.id)?;
        let missing_species_ids: Vec<i32> = location
            .required_species_caught
            .iter()
            .copied()
            .filter(|required_species_id| !caught_species_ids.contains(required_species_id))
            .collect();

        Ok(LocationUnlockRequirements {
            locations_unlocked: missing_location_ids,
            species_caught: missing_species_ids,
        })
    }

    fn unlock_location(
        &self,
        user: &User,
        location_data: Arc<LocationData>,
    ) -> GameResult<UserLocation> {
        let unmet_unlock_requirements =
            self.get_unmet_location_unlock_requirements(user, location_data.clone())?;
        if !unmet_unlock_requirements.is_empty() {
            return Err(
                GameResourceError::unmet_location_unlock_requirements(location_data.id).into(),
            );
        }

        self.user_repository
            .unlock_location(user.id, location_data.id)
            .map_err(|e| match e.get_database_error() {
                Some(db_error) if db_error.is_unique_constraint_violation() => {
                    GameResourceError::location_already_unlocked(user.external_id, location_data.id)
                        .into()
                }
                _ => e.into(),
            })
    }

    fn get_unlocked_locations(&self, user: &User) -> GameResult<Vec<UserLocation>> {
        Ok(self.user_repository.find_unlocked_locations(user.id)?)
    }

    fn get_unlocked_location_ids(&self, user: &User) -> GameResult<Vec<i32>> {
        Ok(self.user_repository.find_unlocked_location_ids(user.id)?)
    }
}