aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/cli/src/words.rs90
-rw-r--r--lib/mihi/src/lib.rs98
-rw-r--r--testdata/test.sqlite3bin2916352 -> 2916352 bytes
3 files changed, 181 insertions, 7 deletions
diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs
index 9210dd8..b97a57f 100644
--- a/crates/cli/src/words.rs
+++ b/crates/cli/src/words.rs
@@ -1,10 +1,10 @@
use crate::inflection::print_full_inflection_for;
use crate::locale::current_locale;
-use inquire::{Confirm, Editor, Select, Text};
+use inquire::{Confirm, Editor, MultiSelect, Select, Text};
use mihi::{
- adverb, comparative, joint_related_words, select_related_words, superlative, Category, Gender,
- Language, RelationKind, Word,
+ adverb, comparative, joint_related_words, select_related_words, select_tags_for, superlative,
+ Category, Gender, Language, RelationKind, Word,
};
use std::vec::IntoIter;
@@ -388,9 +388,22 @@ fn do_create(enunciated: String) -> Result<(), String> {
let mut guess = get_initial_guess(enunciated.as_str());
guess.enunciated = enunciated.trim().to_string();
+ let tags = select_tags_for(None)?;
let word = ask_for_word_based_on(enunciated.clone(), guess)?;
+ let Ok(selected_tags) = MultiSelect::new("Tags:", tags)
+ .with_starting_cursor(0)
+ .prompt()
+ else {
+ return Err("abort!".to_string());
+ };
+
match mihi::create_word(word) {
- Ok(_) => {
+ Ok(word_id) => {
+ for tag in selected_tags {
+ if let Err(e) = mihi::attach_tag_to_word(tag.id as i64, word_id) {
+ println!("warning: words: {e}");
+ }
+ }
println!("Word '{enunciated}' has been successfully created!");
Ok(())
}
@@ -522,6 +535,9 @@ fn edit(mut args: IntoIter<String>) -> i32 {
}
};
+ // Preserve this value as it will be used at the end of this function.
+ let word_id = word.id as i64;
+
// The enunciate might change, let's ask for it again. This way we get the
// same experience as with the 'create' command.
let Ok(enunciated) = Text::new("Enunciated:")
@@ -534,6 +550,22 @@ fn edit(mut args: IntoIter<String>) -> i32 {
return 0;
}
+ // Select the tags for the current word.
+ let tags = match mihi::select_tags_for(Some(word.id)) {
+ Ok(tags) => tags,
+ Err(e) => {
+ println!("error: words: {e}");
+ return 1;
+ }
+ };
+ let all_tags = match mihi::select_tags_for(None) {
+ Ok(tags) => tags,
+ Err(e) => {
+ println!("error: words: {e}");
+ return 1;
+ }
+ };
+
// And ask again column by column to check for changes.
let updated = match ask_for_word_based_on(enunciated.clone(), word) {
Ok(word) => word,
@@ -543,8 +575,58 @@ fn edit(mut args: IntoIter<String>) -> i32 {
}
};
+ // Ask for tags. The indeces on the UI do not match the ones on the
+ // DB. Hence, we need to match the IDs from the DB to the ones displayed on
+ // the DB. It's a bit cumbersome but there shouldn't be many tags for this
+ // to become painfully slow.
+ let mut default_indices = vec![];
+ for t in &tags {
+ for (idx, ta) in all_tags.iter().enumerate() {
+ if t.id == ta.id {
+ default_indices.push(idx);
+ }
+ }
+ }
+ let Ok(selected_tags) = MultiSelect::new("Tags:", all_tags)
+ .with_starting_cursor(0)
+ .with_default(&default_indices)
+ .prompt()
+ else {
+ return 1;
+ };
+
+ // Compute which tags to add and which to remove. This is, again, not the
+ // most fun thing to do, but I think it's better/cleaner on the long run
+ // than simply removing all tag associations and then bringing them
+ // back. And as I said before, there shouldn't be too many tags for this to
+ // become too slow.
+ let mut tags_to_add = vec![];
+ let mut tags_to_remove = vec![];
+ for st in &selected_tags {
+ if !tags.iter().any(|et| st.id == et.id) {
+ tags_to_add.push(st.id);
+ }
+ }
+ for et in &tags {
+ if !selected_tags.iter().any(|st| st.id == et.id) {
+ tags_to_remove.push(et.id);
+ }
+ }
+
match mihi::update_word(updated) {
Ok(_) => {
+ // Add missing tags.
+ for tag in tags_to_add {
+ if let Err(e) = mihi::attach_tag_to_word(tag as i64, word_id) {
+ println!("warning: words: {e}");
+ }
+ }
+
+ // Drop tags which are no longer needed.
+ if let Err(e) = mihi::dettach_tags_from_word(&tags_to_remove, word_id) {
+ println!("warning: words: {e}");
+ }
+
println!("Word '{enunciated}' has been updated!");
0
}
diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs
index 6ce450e..9d65b1c 100644
--- a/lib/mihi/src/lib.rs
+++ b/lib/mihi/src/lib.rs
@@ -360,6 +360,19 @@ pub fn adverb(word: &Word, related: &[Word]) -> String {
}
#[derive(Clone, Debug)]
+pub struct Tag {
+ pub id: i32,
+ pub name: String,
+}
+
+// Needed for inquire's (Multi)Select.
+impl std::fmt::Display for Tag {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.name)
+ }
+}
+
+#[derive(Clone, Debug)]
pub struct Word {
pub id: i32,
pub enunciated: String,
@@ -516,8 +529,8 @@ pub fn is_valid_word_flag(flag: &str) -> bool {
BOOLEAN_FLAGS.contains(&flag)
}
-/// Creates the given word into the database.
-pub fn create_word(word: Word) -> Result<(), String> {
+/// Creates the given word into the database and returns its ID on success.
+pub fn create_word(word: Word) -> Result<i64, String> {
match word.category {
Category::Noun => match word.declension_id {
Some(id @ 1..7) => {
@@ -605,11 +618,13 @@ pub fn create_word(word: Word) -> Result<(), String> {
0
],
) {
- Ok(_) => Ok(()),
+ Ok(_) => Ok(conn.last_insert_rowid()),
Err(e) => Err(format!("could not create '{}': {}", word.enunciated, e)),
}
}
+/// Update the word that matches the ID on `word` and set it to the new values
+/// contained in the `word` object.
pub fn update_word(word: Word) -> Result<(), String> {
if word.id == 0 {
return Err("invalid word to update; seems it has not been created before".to_string());
@@ -1022,6 +1037,44 @@ pub fn select_tag_names(filter: &Option<String>) -> Result<Vec<String>, String>
Ok(res)
}
+/// Select all tags for the given `word`. If None is provided, then all tags
+/// from the database are returned.
+pub fn select_tags_for(word: Option<i32>) -> Result<Vec<Tag>, String> {
+ let conn = get_connection()?;
+
+ let mut stmt;
+ let mut it = match word {
+ Some(id) => {
+ stmt = conn
+ .prepare(
+ "SELECT t.id, t.name \
+ FROM tags t \
+ JOIN tag_associations ta ON t.id = ta.tag_id \
+ JOIN words w ON w.id = ta.word_id \
+ WHERE w.id = ?1 \
+ ORDER BY t.name",
+ )
+ .unwrap();
+ stmt.query([id]).unwrap()
+ }
+ None => {
+ stmt = conn
+ .prepare("SELECT id, name FROM tags ORDER BY name")
+ .unwrap();
+ stmt.query([]).unwrap()
+ }
+ };
+
+ let mut res = vec![];
+ while let Some(row) = it.next().unwrap() {
+ res.push(Tag {
+ id: row.get::<usize, i32>(0).unwrap(),
+ name: row.get::<usize, String>(1).unwrap(),
+ });
+ }
+ Ok(res)
+}
+
/// Insert into the database the tag identified by the given name.
pub fn create_tag(name: &str) -> Result<(), String> {
let conn = get_connection()?;
@@ -1036,6 +1089,45 @@ pub fn create_tag(name: &str) -> Result<(), String> {
}
}
+/// Inserts the pair of IDs into the tag_associations table.
+pub fn attach_tag_to_word(tag_id: i64, word_id: i64) -> Result<(), String> {
+ let conn = get_connection()?;
+
+ match conn.execute(
+ "INSERT INTO tag_associations (tag_id, word_id, updated_at, created_at) \
+ VALUES (?1, ?2, datetime('now'), datetime('now'))",
+ params![tag_id, word_id],
+ ) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("could not attach tag: {e}")),
+ }
+}
+
+/// Inserts the pair of IDs into the tag_associations table.
+pub fn dettach_tags_from_word(tags: &[i32], word_id: i64) -> Result<(), String> {
+ if tags.is_empty() {
+ return Ok(());
+ }
+
+ let conn = get_connection()?;
+
+ match conn.execute(
+ format!(
+ "DELETE FROM tag_associations \
+ WHERE tag_id in ({}) AND word_id = ?1",
+ tags.iter()
+ .map(|t| format!("{}", t))
+ .collect::<Vec<_>>()
+ .join(", ")
+ )
+ .as_str(),
+ params![word_id],
+ ) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("could not attach tag: {e}")),
+ }
+}
+
/// Delete the tag from the database.
pub fn delete_tag(name: &String) -> Result<(), String> {
let conn = get_connection()?;
diff --git a/testdata/test.sqlite3 b/testdata/test.sqlite3
index 43d7ca0..f6e5939 100644
--- a/testdata/test.sqlite3
+++ b/testdata/test.sqlite3
Binary files differ