aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-06-03 21:49:25 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-06-03 21:49:25 +0200
commit28c011c7b3ebd8f5aae111551222ed3748937b33 (patch)
treef6310107b9fbdabeda1cce46e2feb2cb85d1ce1a /lib
downloadmihi-28c011c7b3ebd8f5aae111551222ed3748937b33.tar.gz
mihi-28c011c7b3ebd8f5aae111551222ed3748937b33.zip
Initial commit
Basic commands have been added, but most of the features for this project are still work in progress. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/mihi/Cargo.toml11
-rw-r--r--lib/mihi/src/lib.rs146
-rw-r--r--lib/mihi/src/migrate.rs42
3 files changed, 199 insertions, 0 deletions
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<PathBuf, String> {
+ 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<String>) -> Result<Vec<String>, 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::<usize, String>(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<rusqlite::Connection, String> {
+ 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<usize> {
+ 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)
+}