~prma/ddd

9492461462b3d61655c00e459561df17abf75d13 — Perma Alesheikh 5 months ago f0e5522 v0.2.4
do some clean up

Some updates here, some lint fixes there, some extra dependencies
removed. House cleaning stuff.

Signed-off-by: Perma Alesheikh <me@prma.dev>
M Cargo.lock => Cargo.lock +19 -20
@@ 306,7 306,7 @@ dependencies = [

[[package]]
name = "der_die_das"
version = "0.2.3"
version = "0.2.4"
dependencies = [
 "clap",
 "clap_complete_command",


@@ 319,7 319,6 @@ dependencies = [
 "inquire",
 "serde",
 "serde_json",
 "thiserror",
 "time",
 "tracing",
 "tracing-error",


@@ 460,9 459,9 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"

[[package]]
name = "inquire"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [
 "bitflags 2.5.0",
 "crossterm 0.25.0",


@@ 505,9 504,9 @@ dependencies = [

[[package]]
name = "lock_api"
version = "0.4.11"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
 "autocfg",
 "scopeguard",


@@ 624,9 623,9 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"

[[package]]
name = "parking_lot"
version = "0.12.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
dependencies = [
 "lock_api",
 "parking_lot_core",


@@ 634,15 633,15 @@ dependencies = [

[[package]]
name = "parking_lot_core"
version = "0.9.9"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
 "cfg-if",
 "libc",
 "redox_syscall",
 "smallvec",
 "windows-targets 0.48.5",
 "windows-targets 0.52.5",
]

[[package]]


@@ 713,11 712,11 @@ dependencies = [

[[package]]
name = "redox_syscall"
version = "0.4.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
dependencies = [
 "bitflags 1.3.2",
 "bitflags 2.5.0",
]

[[package]]


@@ 801,18 800,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"

[[package]]
name = "serde"
version = "1.0.198"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.198"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
dependencies = [
 "proc-macro2",
 "quote",


@@ 1068,9 1067,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"

[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"

[[package]]
name = "utf8parse"

M Cargo.toml => Cargo.toml +4 -2
@@ 1,6 1,6 @@
[package]
name = "der_die_das"
version = "0.2.3"
version = "0.2.4"
edition = "2021"
authors = ["Perma Alesheikh <me@prma.dev>"]
license = "EUPL-1.2"


@@ 32,8 32,10 @@ fs_extra = "1.3.0"
inquire = "0.7.4"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
thiserror = "1.0.59"
time = { version = "0.3.36", features = ["serde", "local-offset", "serde-well-known"] }
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

[lints.clippy]
pedantic = "warn"

M src/attempt.rs => src/attempt.rs +35 -2
@@ 7,19 7,52 @@ pub struct Attempt {
    pub id: i128,
    pub at: time::OffsetDateTime,
    pub for_word: i128,
    pub what_happened: AttemptResult,
    pub what_happened: Conclusion,
}

#[derive(Debug, Clone)]
pub enum AttemptResult {
pub enum Conclusion {
    Success,
    WrongArticle(Article),
}

pub trait Repo {
    /// Saves an attempt
    ///
    /// # Errors
    ///
    /// This function will return an error if the IO fails or the item already
    /// Exists.
    fn save_attempt(self, attempt: Attempt) -> Result<()>;

    /// Returns all the attempts
    ///
    /// # Errors
    ///
    /// When IO failed.
    fn all_attempts(self) -> Result<Vec<Attempt>>;

    /// Find an attempt by its ID.
    ///
    /// # Errors
    ///
    /// This function will return an error if the item was not found or there
    /// was an IO failure .
    fn find_attempt_by_id(self, id: i128) -> Result<Attempt>;

    /// Finds the attempts for an specific noun.
    ///
    /// # Errors
    ///
    /// This function will return an error if the item was not found or there
    /// was an IO failure .
    fn find_attempt_by_noun_id(self, id: i128) -> Result<Vec<Attempt>>;

    /// Deletes an attempt by its ID.
    ///
    /// # Errors
    ///
    /// This function will return an error if the item was not found or there
    /// was an IO failure .
    fn delete_attempt_by_id(self, id: i128) -> Result<()>;
}

M src/commands.rs => src/commands.rs +20 -20
@@ 21,7 21,7 @@ use der_die_das::{
};
use deunicode::deunicode;

use crate::{config::GroupConfig, importer};
use crate::{config::GroupConfiguration, importer};

pub fn new_noun(
    id: i128,


@@ 29,7 29,7 @@ pub fn new_noun(
    meaning: String,
    article: Vec<Article>,
    group: Option<String>,
    storage: Storage,
    storage: &Storage,
) -> Result<()> {
    if word.is_empty() {
        return Err(eyre!("word must be at least 1 character"));


@@ 47,7 47,7 @@ pub fn new_noun(
    Ok(())
}

pub fn list_nouns(storage: Storage) -> Result<()> {
pub fn list_nouns(storage: &Storage) -> Result<()> {
    let h = History {
        nouns: storage.all_nouns()?,
        attempts: storage.all_attempts()?,


@@ 160,10 160,10 @@ fn ask_noun(storage: &Storage, noun: &Noun) -> Result<()> {
        .with_validators(&[
            Box::new(ValidateShouldBeTwoWords {}),
            Box::new(ValidateFirstWordShouldBeArticle {}),
            Box::new(ValidateLastWordShouldMatchTheSpelling(noun.word.to_owned())),
            Box::new(ValidateLastWordShouldMatchTheSpelling(noun.word.clone())),
        ])
        .prompt()?
        .to_owned();
        .clone();
    let resp: Vec<_> = resp.split_whitespace().collect();
    if resp.len() != 2 {
        return Err(eyre!("response is more than two words!"));


@@ 181,13 181,22 @@ fn ask_noun(storage: &Storage, noun: &Noun) -> Result<()> {
    }?;

    let now = time::OffsetDateTime::now_utc();
    if !noun.articles.contains(&article) {
    if noun.articles.contains(&article) {
        storage.save_attempt(Attempt {
            id: now.unix_timestamp_nanos(),
            at: now,
            for_word: noun.id,
            what_happened: der_die_das::attempt::Conclusion::Success,
        })?;
        println!("{}", "Correct!".green().bold());
        Ok(())
    } else {
        println!("{}", "Article did not match!".red());
        println!(
            "correct article(s) is(are):\t{}",
            noun.articles
                .iter()
                .map(|s| s.to_string())
                .map(std::string::ToString::to_string)
                .collect::<Vec<_>>()
                .join(", ")
                .bold()


@@ 197,18 206,9 @@ fn ask_noun(storage: &Storage, noun: &Noun) -> Result<()> {
            id: now.unix_timestamp_nanos(),
            at: now,
            for_word: noun.id,
            what_happened: der_die_das::attempt::AttemptResult::WrongArticle(article),
            what_happened: der_die_das::attempt::Conclusion::WrongArticle(article),
        })?;
        ask_noun(storage, noun)
    } else {
        storage.save_attempt(Attempt {
            id: now.unix_timestamp_nanos(),
            at: now,
            for_word: noun.id,
            what_happened: der_die_das::attempt::AttemptResult::Success,
        })?;
        println!("{}", "Correct!".green().bold());
        Ok(())
    }
}



@@ 222,7 222,7 @@ fn get_confidence(storage: &Storage, id: i128) -> Result<(Noun, u8)> {
        .ok_or_eyre("Some how the word is not there any more!")
}

pub fn ask_next(storage: &Storage, group_config: GroupConfig) -> Result<()> {
pub fn ask_next(storage: &Storage, group_config: &GroupConfiguration) -> Result<()> {
    let nouns = storage.all_nouns()?;
    let attempts = storage.all_attempts()?;
    let h = History { nouns, attempts };


@@ 246,7 246,7 @@ pub fn ask_next(storage: &Storage, group_config: GroupConfig) -> Result<()> {
    Ok(())
}

pub fn group_ask(storage: &Storage, group_config: GroupConfig) -> Result<()> {
pub fn group_ask(storage: &Storage, group_config: &GroupConfiguration) -> Result<()> {
    let nouns = storage.all_nouns()?;
    let attempts = storage.all_attempts()?;
    let h = History { nouns, attempts };


@@ 273,7 273,7 @@ pub fn group_ask(storage: &Storage, group_config: GroupConfig) -> Result<()> {
    })
}

pub fn import_nouns(storage: Storage, at: PathBuf) -> Result<()> {
pub fn import_nouns(storage: &Storage, at: PathBuf) -> Result<()> {
    if !at.try_exists()? {
        return Err(eyre!(format!(
            "Such file or directory does not exist: {}",

M src/config.rs => src/config.rs +24 -22
@@ 34,12 34,12 @@ impl Default for GroupConfigFile {
    serde::Deserialize,
    Default,
)]
pub struct ConfigFile {
pub struct ConfigurationFile {
    pub data_path: Option<PathBuf>,
    pub group: Option<GroupConfigFile>,
}

impl TryFrom<PathBuf> for ConfigFile {
impl TryFrom<PathBuf> for ConfigurationFile {
    type Error = color_eyre::Report;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {


@@ 50,7 50,7 @@ impl TryFrom<PathBuf> for ConfigFile {
    }
}

impl TryFrom<&PathBuf> for ConfigFile {
impl TryFrom<&PathBuf> for ConfigurationFile {
    type Error = color_eyre::Report;

    fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {


@@ 64,11 64,11 @@ impl TryFrom<&PathBuf> for ConfigFile {
#[derive(
    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct GroupConfig {
pub struct GroupConfiguration {
    pub threshold: u8,
    pub enable: bool,
}
impl Default for GroupConfig {
impl Default for GroupConfiguration {
    fn default() -> Self {
        Self {
            threshold: 100,


@@ 77,7 77,7 @@ impl Default for GroupConfig {
    }
}

impl From<GroupConfigFile> for GroupConfig {
impl From<GroupConfigFile> for GroupConfiguration {
    fn from(value: GroupConfigFile) -> Self {
        Self {
            threshold: value.threshold.unwrap_or(100),


@@ 91,26 91,28 @@ impl From<GroupConfigFile> for GroupConfig {
)]
pub struct Config {
    pub data_path: PathBuf,
    pub group: GroupConfig,
    pub group: GroupConfiguration,
}

impl TryFrom<ConfigFile> for Config {
impl TryFrom<ConfigurationFile> for Config {
    type Error = color_eyre::Report;

    fn try_from(value: ConfigFile) -> color_eyre::Result<Self> {
        let data_path = match value.data_path {
            Some(r) => r,
            None => {
                let p = dirs::data_dir()
                    .ok_or_eyre("Could not find any folder to put the data in.")?
                    .join("ddd");
                if !p.try_exists()? {
                    fs_extra::dir::create_all(&p, false)?
                };
                p
            }
        };
    fn try_from(value: ConfigurationFile) -> color_eyre::Result<Self> {
        let group = value.group.unwrap_or_default().into();
        Ok(Config { data_path, group })

        if let Some(data_path) = value.data_path {
            return Ok(Config { data_path, group });
        };
        let p = dirs::data_dir()
            .ok_or_eyre("Could not find any folder to put the data in.")?
            .join("ddd");
        if !p.try_exists()? {
            fs_extra::dir::create_all(&p, false)?;
        };

        Ok(Config {
            group,
            data_path: p,
        })
    }
}

M src/history.rs => src/history.rs +10 -3
@@ 8,6 8,7 @@ pub struct History {
}

impl History {
    #[must_use]
    pub fn attempt_per_noun(&self) -> Vec<(Noun, Vec<Attempt>)> {
        let n: Vec<_> = self
            .nouns


@@ 30,16 31,17 @@ impl History {
        n
    }

    #[must_use]
    pub fn confidence_map(&self) -> Vec<(Noun, u8)> {
        self.attempt_per_noun()
            .iter()
            .map(|s| {
                let confidence = s.1.iter().fold(0u8, |confidence, a| match a.what_happened {
                    crate::attempt::AttemptResult::Success => match confidence {
                    crate::attempt::Conclusion::Success => match confidence {
                        u8::MAX => confidence,
                        _ => confidence + 1,
                    },
                    crate::attempt::AttemptResult::WrongArticle(_) => match confidence {
                    crate::attempt::Conclusion::WrongArticle(_) => match confidence {
                        u8::MIN => confidence,
                        _ => confidence - 1,
                    },


@@ 49,19 51,21 @@ impl History {
            .collect()
    }

    #[must_use]
    pub fn grouped_confidence_map(&self) -> HashMap<String, Vec<(Noun, u8)>> {
        self.confidence_map().into_iter().fold(
            HashMap::new(),
            |mut acc: HashMap<String, Vec<(Noun, u8)>>, (n, c)| {
                match acc.get_mut(&n.group) {
                    Some(cu) => cu.push((n, c)),
                    None => _ = acc.insert(n.group.to_owned(), vec![(n, c)]),
                    None => _ = acc.insert(n.group.clone(), vec![(n, c)]),
                };
                acc
            },
        )
    }

    #[must_use]
    pub fn next(&self) -> Option<(Noun, u8)> {
        let mut sets = self.confidence_map();



@@ 69,6 73,8 @@ impl History {

        sets.first().cloned()
    }

    #[must_use]
    pub fn next_group(&self, threshold: u8) -> Option<Vec<(Noun, u8)>> {
        let sets = self.grouped_confidence_map();
        if sets.is_empty() {


@@ 89,6 95,7 @@ impl History {
        Some(left)
    }

    #[must_use]
    pub fn next_with_group(&self, threshold: u8) -> Option<(Noun, u8)> {
        let sets = self.grouped_confidence_map();
        if sets.is_empty() {

M src/importer.rs => src/importer.rs +2 -2
@@ 5,7 5,7 @@ use der_die_das::{
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Word {
pub struct WordSet {
    articles: Vec<ArticleFileV1>,
    word: String,
    meaning: String,


@@ 14,7 14,7 @@ pub struct Word {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Group {
    name: String,
    words: Vec<Word>,
    words: Vec<WordSet>,
}

impl From<Group> for Vec<Noun> {

M src/main.rs => src/main.rs +8 -8
@@ 25,7 25,7 @@ fn main() -> Result<()> {

    debug!("the config path is at: {:#?}", &config_path);

    let configs_file = config::ConfigFile::try_from(config_path)?;
    let configs_file = config::ConfigurationFile::try_from(config_path)?;

    debug!("the configuration file is: {:#?}", &configs_file);
    let configs = config::Config::try_from(configs_file)?;


@@ 52,18 52,18 @@ fn main() -> Result<()> {
                    meaning,
                    articles,
                    group,
                    storage,
                    &storage,
                ),
            },
            Commands::List(list_commands) => match list_commands {
                arugments::ListCommands::Nouns => commands::list_nouns(storage),
                arugments::ListCommands::Nouns => commands::list_nouns(&storage),
            },
            Commands::Import(import_commands) => match import_commands {
                arugments::ImportCommands::Nouns { at } => commands::import_nouns(storage, at),
                arugments::ImportCommands::Nouns { at } => commands::import_nouns(&storage, at),
            },
            Commands::Ask(ask_commands) => match ask_commands {
                arugments::AskCommands::Nouns => commands::ask_next(&storage, configs.group),
                arugments::AskCommands::Group => commands::group_ask(&storage, configs.group),
                arugments::AskCommands::Nouns => commands::ask_next(&storage, &configs.group),
                arugments::AskCommands::Group => commands::group_ask(&storage, &configs.group),
            },
        },
        None => todo!(),


@@ 88,9 88,9 @@ fn trace_install() -> Result<()> {
        .with(ErrorLayer::default());

    if current_filter == LevelFilter::OFF {
        registry.with(fmt_layer.without_time()).init()
        registry.with(fmt_layer.without_time()).init();
    } else {
        registry.with(fmt_layer).init()
        registry.with(fmt_layer).init();
    }

    if env::var("RUST_BACKTRACE").is_err() && current_filter == LevelFilter::TRACE {

M src/nouns.rs => src/nouns.rs +26 -0
@@ 33,8 33,34 @@ impl Display for Article {
}

pub trait Repo {
    /// Saves one noun.
    ///
    /// # Errors
    ///
    /// This function will return an error if the noun with the same ID already
    /// exist or there is an IO failure.
    fn save_noun(self, noun: Noun) -> Result<()>;

    /// Returns all the nouns
    ///
    /// # Errors
    ///
    /// This function will return an error if there is an IO Failure.
    fn all_nouns(self) -> Result<Vec<Noun>>;

    /// Find noun by specific ID
    ///
    /// # Errors
    ///
    /// This function will return an error if a noun by that ID does not exist
    /// or there is an IO failure.
    fn find_noun_by_id(self, id: i128) -> Result<Noun>;

    /// Delete the noun by its ID
    ///
    /// # Errors
    ///
    /// This function will return an error if a noun by that ID does not exist
    /// or there is an IO failure.
    fn delete_noun_by_id(self, id: i128) -> Result<()>;
}

M src/storage.rs => src/storage.rs +10 -10
@@ 7,7 7,7 @@ use color_eyre::{
use serde::{Deserialize, Serialize};

use crate::{
    attempt::{self, Attempt, AttemptResult},
    attempt::{self, Attempt, Conclusion},
    nouns::{self, Article, Noun},
};



@@ 81,7 81,7 @@ impl nouns::Repo for &Storage {

    fn all_nouns(self) -> Result<Vec<Noun>> {
        Ok(std::fs::read_dir(self.words_path())?
            .filter_map(|e| e.ok())
            .filter_map(std::result::Result::ok)
            .filter_map(|e| {
                let at = e.path();
                match at.extension().map(|ext| ext.to_str()) {


@@ 224,11 224,11 @@ impl From<AttemptFile> for Attempt {
    }
}

impl From<AttemptResultFileV1> for AttemptResult {
impl From<AttemptResultFileV1> for Conclusion {
    fn from(ar: AttemptResultFileV1) -> Self {
        match ar {
            AttemptResultFileV1::Success => AttemptResult::Success,
            AttemptResultFileV1::WrongArticle(art) => AttemptResult::WrongArticle(art.into()),
            AttemptResultFileV1::Success => Conclusion::Success,
            AttemptResultFileV1::WrongArticle(art) => Conclusion::WrongArticle(art.into()),
        }
    }
}


@@ 244,11 244,11 @@ impl From<Attempt> for AttemptFile {
    }
}

impl From<AttemptResult> for AttemptResultFileV1 {
    fn from(ar: AttemptResult) -> Self {
impl From<Conclusion> for AttemptResultFileV1 {
    fn from(ar: Conclusion) -> Self {
        match ar {
            AttemptResult::Success => AttemptResultFileV1::Success,
            AttemptResult::WrongArticle(art) => AttemptResultFileV1::WrongArticle(art.into()),
            Conclusion::Success => AttemptResultFileV1::Success,
            Conclusion::WrongArticle(art) => AttemptResultFileV1::WrongArticle(art.into()),
        }
    }
}


@@ 272,7 272,7 @@ impl attempt::Repo for &Storage {

    fn all_attempts(self) -> Result<Vec<Attempt>> {
        Ok(std::fs::read_dir(self.attempts_path())?
            .filter_map(|e| e.ok())
            .filter_map(std::result::Result::ok)
            .filter_map(|e| {
                let at = e.path();
                match at.extension().map(|ext| ext.to_str()) {