From 28c011c7b3ebd8f5aae111551222ed3748937b33 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 3 Jun 2025 21:49:25 +0200 Subject: Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basic commands have been added, but most of the features for this project are still work in progress. Signed-off-by: Miquel Sabaté Solà --- lib/mihi/Cargo.toml | 11 ++++ lib/mihi/src/lib.rs | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/mihi/src/migrate.rs | 42 ++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 lib/mihi/Cargo.toml create mode 100644 lib/mihi/src/lib.rs create mode 100644 lib/mihi/src/migrate.rs (limited to 'lib') diff --git a/lib/mihi/Cargo.toml b/lib/mihi/Cargo.toml new file mode 100644 index 0000000..33f3439 --- /dev/null +++ b/lib/mihi/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mihi" +version = "0.1.0" +description = "Library for the 'mihi' application" + +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +rusqlite = { version = "0.35.0", features = ["bundled"] } diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs new file mode 100644 index 0000000..906953d --- /dev/null +++ b/lib/mihi/src/lib.rs @@ -0,0 +1,146 @@ +use std::fs; +use std::fs::File; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; + +use rusqlite::{params, Connection}; + +mod migrate; + +/// Returns the configuration path for the application, and it even creates it +/// if it doesn't exist already. +pub fn get_config_path() -> Result { + let dir = match &std::env::var("XDG_CONFIG_HOME") { + Ok(path) => PathBuf::from(path), + Err(_) => match &std::env::var("HOME") { + Ok(path) => Path::new(path).join(".config"), + Err(_) => { + return Err(String::from( + "cannot find a suitable path for the configuration", + )) + } + }, + } + .join("mihi"); + + match fs::create_dir_all(&dir) { + Ok(_) => {} + Err(e) => return Err(e.to_string()), + }; + + Ok(dir) +} + +/// Add the given language into the configuration of this application. +pub fn add_language(language: String) -> Result<(), String> { + if language.as_str() != "latin" { + return Err(String::from("only 'latin' is allowed for a language")); + } + + let path = get_config_path()?; + let cfg = path.join("languages.txt"); + + if cfg.exists() { + return Ok(()); + } + + let mut file = match File::create(cfg) { + Ok(f) => f, + Err(e) => return Err(format!("could not create file: {}", e)), + }; + match file.write_all(language.as_bytes()) { + Ok(_) => Ok(()), + Err(e) => Err(format!("could not save language '{}': {}", language, e)), + } +} + +/// Ensure that in the config path there is a fully initialized database. +pub fn init_database() -> Result<(), String> { + let path = get_config_path()?.join("database.sqlite3"); + let conn = match Connection::open(path) { + Ok(handle) => handle, + Err(e) => { + return Err(format!( + "could not initialize the database: {}", + e.to_string() + )) + } + }; + + match migrate::init(conn) { + Ok(_) => Ok(()), + Err(e) => Err(format!("bad database schema file: {}", e.to_string())), + } +} + +// #[derive(Debug)] +// struct Word { +// id: i32, +// enunciated: String, +// particle: String, +// language_id: u64, +// declension_id: u64, +// conjugation_id: u64, +// kind: String, +// category: String, +// regular: bool, +// locative: bool, +// gender: u32, +// suffix: String, +// translation: String, +// // TODO: datetime +// // TODO: jsonb +// } + +pub fn select_enunciated(filter: Option) -> Result, String> { + let conn = get_connection()?; + + let mut stmt; + let mut it = match filter { + Some(filter) => { + stmt = conn + .prepare( + "SELECT enunciated FROM words WHERE enunciated LIKE ('%' || ?1 || '%') ORDER BY enunciated", + ) + .unwrap(); + stmt.query([filter.as_str()]).unwrap() + } + None => { + stmt = conn + .prepare("SELECT enunciated FROM words ORDER BY enunciated") + .unwrap(); + stmt.query([]).unwrap() + } + }; + + let mut res = vec![]; + while let Some(row) = it.next().unwrap() { + res.push(row.get::(0).unwrap()); + } + Ok(res) +} + +pub fn delete_word(enunciated: &String) -> Result<(), String> { + let conn = get_connection()?; + + match conn.execute( + "DELETE FROM words WHERE enunciated = ?1", + params![enunciated.as_str()], + ) { + Ok(_) => Ok(()), + Err(e) => return Err(format!("could not remove '{}': {}", enunciated, e)), + } +} + +fn get_connection() -> Result { + let path = get_config_path()?.join("database.sqlite3"); + match Connection::open(path) { + Ok(handle) => Ok(handle), + Err(_) => { + return Err( + "could not fetch the database. Ensure that you have called 'init' first" + .to_string(), + ) + } + } +} diff --git a/lib/mihi/src/migrate.rs b/lib/mihi/src/migrate.rs new file mode 100644 index 0000000..48c078e --- /dev/null +++ b/lib/mihi/src/migrate.rs @@ -0,0 +1,42 @@ +use rusqlite::{Connection, Result}; + +/// Use the given `connection` in order to initialize the database. +pub fn init(connection: Connection) -> Result { + connection.execute( + r#" +CREATE TABLE IF NOT EXISTS "words" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "particle" varchar, + "enunciated" varchar, + "declension_id" integer, + "conjugation_id" integer, + "kind" varchar, + "category" integer, + "regular" boolean DEFAULT 1, + "locative" boolean DEFAULT 0, + "gender" integer, + "created_at" datetime(6) NOT NULL, + "updated_at" datetime(6) NOT NULL, + "last_asked_at" datetime(6) NOT NULL, + "suffix" varchar, + "language_id" integer, + "translation" json, + "pending" boolean DEFAULT 0, + "flags" jsonb DEFAULT '{}', + + FOREIGN KEY ("conjugation_id") REFERENCES "conjugations" ("id"), + FOREIGN KEY ("declension_id") REFERENCES "declensions" ("id") +); +"#, + (), + )?; + + connection.execute( + r#" +CREATE UNIQUE INDEX IF NOT EXISTS "index_words_on_enunciated" ON "words" ("enunciated"); +"#, + (), + )?; + + Ok(0) +} -- cgit v1.2.3