otarustlings 1.0.0

otafablab rustlings
Documentation
use std::{
    env::current_dir,
    path::{Path, PathBuf},
    sync::mpsc::{channel, Receiver, RecvError, Sender},
};

use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
use notify::DebouncedEvent;
use walkdir::WalkDir;

use crate::Message;

/// Filters .rs files
pub fn filter_rs(path: impl AsRef<Path>) -> bool {
    path.as_ref().extension().map_or(false, |ext| ext == "rs")
}

/// Filters .rs files which are exactly at one folder depth. These are plain exercises
pub fn filter_plain(path: impl AsRef<Path>) -> bool {
    // Assumes the path is not prefixed with ./
    if path.as_ref().ancestors().count() != 3 {
        return false;
    }
    filter_rs(path)
}

/// Filters crates which are used for cargo exercises
pub fn filter_cargo(path: impl AsRef<Path>) -> bool {
    Path::new("exercises")
        .join(path.as_ref())
        .join("Cargo.toml")
        .exists()
}

/// Worker (adapter) loop for mapping [`DebouncedEvent`]s from
/// [`notify::RecommendedWatcher`] to [`Message`]s and writing them to `tx`.
///
/// # Messages
///
/// Sends
///
/// - [`Message::Notify`] when any file in `exercises` changes
/// - [`Message::Terminate`] when the [`notify::RecommendedWatcher`] shuts down
///
/// # Panics
///
/// - If [`current_dir`] fails
///
/// # Errors
///
/// The [`anyhow::Error`] may contain
///
/// - a [`notify::Error`] if that is received from the
///   [`notify::RecommendedWatcher`]
pub fn notify_adapter(rx: &Receiver<DebouncedEvent>, tx: &Sender<Message>) -> anyhow::Result<()> {
    loop {
        match rx.recv() {
            Err(RecvError) => break,
            Ok(DebouncedEvent::Write(b)) => {
                let b = b
                    .strip_prefix(current_dir().unwrap().join("exercises"))
                    .expect("events to be in subfolder")
                    .to_path_buf();
                let msg = Message::Notify(b);
                tx.send(msg.clone())
                    .map_err(|_| anyhow::anyhow!("failed to send {:?}", msg))?;
            }
            Ok(DebouncedEvent::Error(e, _path)) => return Err(e.into()),
            _ => {}
        }
    }

    Ok(())
}

pub fn transmit_input(message_sender: &Sender<Message>) -> anyhow::Result<()> {
    let (tx, rx) = channel();
    loop {
        match read()? {
            Event::Key(KeyEvent {
                code: KeyCode::Char('c'),
                modifiers: KeyModifiers::CONTROL,
            }) => {
                message_sender
                    .send(Message::Terminate)
                    .map_err(|_| anyhow::anyhow!("transmit_input unable to send Terminate"))?;
                break;
            }
            key => {
                if message_sender
                    .send(Message::KeyEvent(key, tx.clone()))
                    .is_err()
                {
                    break;
                }
            }
        }
        // The tester can decide to break the loop
        if rx.recv()? {
            break;
        }
    }

    Ok(())
}

/// Lists exercises in the folder at `path`. Finds `.rs` files at
/// exactly two folders of depth. Returns a list of paths **without**
/// the `path` as a prefix.
///
/// # Examples
///
/// ```
/// # use otarustlings::utils::list_exercises;
/// let mut exercises = list_exercises("exercises");
///
/// // Should find `exercises/week1/01-quiz.rs` first
/// assert_eq!(exercises.next(), Some("week1/01-quiz.rs".into()));
/// ```
pub fn list_exercises(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
    WalkDir::new(&path)
        .min_depth(2)
        .sort_by_file_name()
        .into_iter()
        .filter_map(Result::ok)
        .map(move |de| {
            de.into_path()
                .strip_prefix(&path)
                .expect("dir entry to start with `path`")
                .to_owned()
        })
        .filter(|pb| filter_plain(pb) || filter_cargo(pb))
}