Skip to content

Commit

Permalink
Add interface for getting and setting user preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
martinmr committed May 27, 2024
1 parent 9d3664a commit c827e05
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 38 deletions.
8 changes: 0 additions & 8 deletions src/course_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ pub trait CourseLibrary {

/// Returns the IDs of all the units which match the given query.
fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError>;

/// Returns the user preferences found in the library. The default preferences should be
/// returned if the user preferences file is not found.
fn get_user_preferences(&self) -> UserPreferences;
}

/// A trait that retrieves the unit graph generated after reading a course library.
Expand Down Expand Up @@ -688,10 +684,6 @@ impl CourseLibrary for LocalCourseLibrary {
self.search_helper(query)
.map_err(|e| CourseLibraryError::Search(query.into(), e))
}

fn get_user_preferences(&self) -> UserPreferences {
self.user_preferences.clone()
}
}

impl GetUnitGraph for LocalCourseLibrary {
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ pub enum PracticeStatsError {
RemovePrefix(String, #[source] anyhow::Error),
}

/// An error returned when dealing with user preferences.
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum PreferencesManagerError {
#[error("cannot get user preferences: {0}")]
GetUserPreferences(#[source] anyhow::Error),

#[error("cannot set user preferences: {0}")]
SetUserPreferences(#[source] anyhow::Error),
}

/// An error returned when dealing with git repositories containing courses.
#[derive(Debug, Error)]
#[allow(missing_docs)]
Expand Down
28 changes: 24 additions & 4 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::path::Path;
use ustr::Ustr;
use ustr::UstrSet;

use crate::preferences_manager::PreferencesManager;
use crate::{
blacklist::Blacklist,
course_library::CourseLibrary,
Expand Down Expand Up @@ -70,7 +71,6 @@ pub trait CourseLibraryFFI {
fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>>;
fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr>;
fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError>;
fn get_user_preferences(&self) -> UserPreferences;
}

impl CourseLibraryFFI for TraneFFI {
Expand Down Expand Up @@ -101,9 +101,6 @@ impl CourseLibraryFFI for TraneFFI {
fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError> {
self.trane.search(query)
}
fn get_user_preferences(&self) -> UserPreferences {
self.trane.get_user_preferences().into()
}
}

/// The FFI version of the `ExerciseScheduler` trait.
Expand Down Expand Up @@ -219,6 +216,29 @@ impl PracticeStatsFFI for TraneFFI {
}
}

// The FFI version of the `PreferencesManager` trait.
#[allow(missing_docs)]
pub trait PreferencesManagerFFI {
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError>;
fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError>;
}

impl PreferencesManagerFFI for TraneFFI {
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError> {
self.trane.get_user_preferences().map(Into::into)
}

fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError> {
self.trane.set_user_preferences(preferences.into())
}
}

/// The FFI version of the `RepositoryManager` trait.
#[allow(missing_docs)]
pub trait RepositoryManagerFFI {
Expand Down
54 changes: 30 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,22 @@ pub mod filter_manager;
pub mod graph;
pub mod mantra_miner;
pub mod practice_stats;
pub mod preferences_manager;
pub mod repository_manager;
pub mod review_list;
pub mod scheduler;
pub mod scorer;
pub mod study_session_manager;
pub mod testutil;

use anyhow::{anyhow, bail, ensure, Context, Result};
use anyhow::{bail, ensure, Context, Result};
use error::*;
use parking_lot::RwLock;
use preferences_manager::{LocalPreferencesManager, PreferencesManager};
use review_list::{LocalReviewList, ReviewList};
use std::{
fs::{create_dir, File},
io::{BufReader, Write},
io::Write,
path::Path,
sync::Arc,
};
Expand Down Expand Up @@ -140,6 +142,9 @@ pub struct Trane {
/// The object managing the information on previous exercise trials.
practice_stats: Arc<RwLock<dyn PracticeStats + Send + Sync>>,

/// The object managing the user preferences.
preferences_manager: Arc<RwLock<dyn PreferencesManager + Send + Sync>>,

/// The object managing git repositories containing courses.
repo_manager: Arc<RwLock<dyn RepositoryManager + Send + Sync>>,

Expand Down Expand Up @@ -253,19 +258,6 @@ impl Trane {
Ok(())
}

/// Returns the user preferences stored in the local course library.
fn open_preferences(library_root: &Path) -> Result<UserPreferences> {
// The user preferences should exist when this function is called.
let path = library_root
.join(TRANE_CONFIG_DIR_PATH)
.join(USER_PREFERENCES_PATH);
let file = File::open(path.clone())
.with_context(|| anyhow!("cannot open user preferences file {}", path.display()))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)
.with_context(|| anyhow!("cannot parse user preferences file {}", path.display()))
}

/// Creates a new local instance of the Trane given the path to the root of a course library.
/// The user data will be stored in a directory named `.trane` inside the library root
/// directory. The working directory will be used to resolve relative paths.
Expand All @@ -275,10 +267,15 @@ impl Trane {
Self::init_config_directory(library_root)?;

// Build all the components needed to create a Trane instance.
let user_preferences = Self::open_preferences(library_root)?;
let preferences_manager = Arc::new(RwLock::new(LocalPreferencesManager {
path: library_root
.join(TRANE_CONFIG_DIR_PATH)
.join(USER_PREFERENCES_PATH),
}));
let user_preferences = preferences_manager.read().get_user_preferences()?;
let course_library = Arc::new(RwLock::new(LocalCourseLibrary::new(
&working_dir.join(library_root),
user_preferences,
user_preferences.clone(),
)?));
let unit_graph = course_library.write().get_unit_graph();
let practice_stats = Arc::new(RwLock::new(LocalPracticeStats::new_from_disk(
Expand All @@ -299,9 +296,6 @@ impl Trane {
let repo_manager = Arc::new(RwLock::new(LocalRepositoryManager::new(library_root)?));
let mut mantra_miner = TraneMantraMiner::default();
mantra_miner.mantra_miner.start()?;

// Build the scheduler options and data.
let user_preferences = course_library.read().get_user_preferences();
let options = Self::create_scheduler_options(&user_preferences.scheduler);
options.verify()?;
let scheduler_data = SchedulerData {
Expand All @@ -321,6 +315,7 @@ impl Trane {
filter_manager,
library_root: library_root.to_str().unwrap().to_string(),
practice_stats,
preferences_manager,
repo_manager,
review_list,
scheduler_data: scheduler_data.clone(),
Expand Down Expand Up @@ -421,10 +416,6 @@ impl CourseLibrary for Trane {
fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError> {
self.course_library.read().search(query)
}

fn get_user_preferences(&self) -> UserPreferences {
self.course_library.read().get_user_preferences()
}
}

impl ExerciseScheduler for Trane {
Expand Down Expand Up @@ -508,6 +499,21 @@ impl PracticeStats for Trane {
}
}

impl PreferencesManager for Trane {
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError> {
self.preferences_manager.read().get_user_preferences()
}

fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError> {
self.preferences_manager
.write()
.set_user_preferences(preferences)
}
}

impl RepositoryManager for Trane {
fn add_repo(
&mut self,
Expand Down
103 changes: 103 additions & 0 deletions src/preferences_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! A module containing methods to read and write user preferences.
use anyhow::{anyhow, Context, Result};
use std::{fs::File, io::BufReader, path::PathBuf};

use crate::{data::UserPreferences, PreferencesManagerError};

/// A trait for managing user preferences.
pub trait PreferencesManager {
/// Gets the current user preferences.
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError>;

/// Sets the user preferences to the given value.
fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError>;
}

/// A preferences manager backed by a local file containing a serialized `UserPreferences` object.
pub struct LocalPreferencesManager {
/// The path to the user preferences file.
pub path: PathBuf,
}

impl LocalPreferencesManager {
/// Helper function to get the current user preferences.
fn get_user_preferences_helper(&self) -> Result<UserPreferences> {
let file = File::open(self.path.clone()).with_context(|| {
anyhow!("cannot open user preferences file {}", self.path.display())
})?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)
.with_context(|| anyhow!("cannot parse user preferences file {}", self.path.display()))
}

/// Helper function to set the user preferences to the given value.
fn set_user_preferences_helper(&self, preferences: &UserPreferences) -> Result<()> {
let file = File::create(self.path.clone()).with_context(|| {
anyhow!(
"cannot create user preferences file {}",
self.path.display()
)
})?;
serde_json::to_writer(file, &preferences).with_context(|| {
anyhow!(
"cannot serialize user preferences to file at {}",
self.path.display()
)
})
}
}

impl PreferencesManager for LocalPreferencesManager {
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError> {
self.get_user_preferences_helper()
.map_err(PreferencesManagerError::GetUserPreferences)
}

fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError> {
self.set_user_preferences_helper(&preferences)
.map_err(PreferencesManagerError::SetUserPreferences)
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use tempfile::tempdir;

use crate::{
data::UserPreferences,
preferences_manager::{LocalPreferencesManager, PreferencesManager},
USER_PREFERENCES_PATH,
};

/// Verifies setting and getting user preferences using the local filesystem.
#[test]
fn local_preferences_manager() -> Result<()> {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join(USER_PREFERENCES_PATH);

let mut manager = LocalPreferencesManager { path };

// Set and get the default user preferences.
let preferences = UserPreferences::default();
assert!(manager.get_user_preferences().is_err());
manager.set_user_preferences(preferences.clone())?;
assert_eq!(manager.get_user_preferences()?, preferences);

// Set and get modified user preferences.
let new_preferences = UserPreferences {
ignored_paths: vec!["foo".to_string(), "bar".to_string()],
..Default::default()
};
manager.set_user_preferences(new_preferences.clone())?;
assert_eq!(manager.get_user_preferences()?, new_preferences);
Ok(())
}
}
4 changes: 2 additions & 2 deletions src/repository_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use crate::{
/// Trane to have access to the user's SSH keys.
const HTTPS_PREFIX: &str = "https://";

/// A trait with function to manage git repositories of courses, with functions to add new
/// repositories, remove existing ones, and update repositories to the latest version.
/// A trait to manage git repositories of courses, with functions to add new repositories, remove
/// existing ones, and update repositories to the latest version.
pub trait RepositoryManager {
/// Downloads the courses from the given git repository into the given directory. The ID will
/// also be used to identify the repository in the future and as the name of the directory. If
Expand Down

0 comments on commit c827e05

Please sign in to comment.