From fa7033cafd400156436873546449d887918be4b2 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:43:01 -0300 Subject: [PATCH 1/9] fix: use package name on error message --- src/error.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index cacf443..c8645b0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,10 @@ use std::process::exit; pub fn invalid(cause: &str) { - eprintln!("invalid {}. get help by running `conf set --help`", cause); + eprintln!( + "invalid {}. get help by running `{} --help`", + cause, + env!("CARGO_PKG_NAME") + ); exit(1); } From 035ed15b49789f025e25fb2834e2d9a822d4c85c Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:50:00 -0300 Subject: [PATCH 2/9] feat: use seahorse ActionResult --- src/actions.rs | 58 ++++++++++++++++++++++++++++++------------------- src/commands.rs | 12 +++++----- src/error.rs | 17 ++++++++------- src/main.rs | 16 +++++++++----- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/actions.rs b/src/actions.rs index 93a72a0..2a7f8aa 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,77 +1,87 @@ use std::fs::File; use std::io::Write; use std::path::Path; -use std::process::exit; use json::JsonValue; -use seahorse::Context; +use seahorse::{ActionError, ActionResult, Context}; +use crate::config::get_config_path; +use crate::error::invalid; use crate::json_object::{get_json_object_or_create, set_json_object}; -use crate::{config::get_config_path, error::invalid}; -pub fn init_action(c: &Context) { +pub fn init_action(c: &Context) -> ActionResult { let config_path = get_config_path(); let path = Path::new(&config_path); if path.exists() { println!("config file already exists"); } else { - clear_action(c); + clear_action(c)?; } + + Ok(()) } -pub fn list_action(c: &Context) { +pub fn list_action(c: &Context) -> ActionResult { let conf = get_json_object_or_create(c.bool_flag("force-create")); for (key, value) in conf.entries() { println!("{}\t{}", key, value); } + + Ok(()) } -pub fn clear_action(_c: &Context) { +pub fn clear_action(_c: &Context) -> ActionResult { let mut file = File::create(get_config_path()).unwrap(); - write!(file, "{}", "{}").unwrap(); + + write!(file, "{}", "{}").expect("couldn't overwrite config file"); println!("cleared config file at '{:?}'", get_config_path()); + + Ok(()) } -pub fn get_action(c: &Context) { +pub fn get_action(c: &Context) -> ActionResult { if c.args.len() != 1 { - return invalid("command"); + return Err(invalid("command")); } let conf = get_json_object_or_create(c.bool_flag("force-create")); let key = c.args.get(0); let Some(key) = key else { - return invalid("key"); + return Err(invalid("key")); }; if conf.has_key(&key) { println!("{}", conf[key]); - return; + return Ok(()); } if c.bool_flag("ignore-null") { println!(); } else { - eprintln!("could not find key '{}'", key); - exit(1); + return Err(ActionError { + message: format!("could not find key '{}'", key), + }); } + + Ok(()) } -pub fn set_action(c: &Context) { +pub fn set_action(c: &Context) -> ActionResult { if c.args.len() != 2 { - return invalid("command"); + return Err(invalid("command")); } let mut conf = get_json_object_or_create(c.bool_flag("force-create")); let Some(key) = c.args.get(0) else { - return invalid("key"); + return Err(invalid("key")); }; let Some(value_str) = c.args.get(1) else { - return invalid("value"); + return Err(invalid("value")); }; let json_value = JsonValue::from(value_str.as_str()); @@ -84,20 +94,22 @@ pub fn set_action(c: &Context) { conf.insert(key, value).unwrap(); match set_json_object(conf) { - Ok(_) => println!("updated config file"), + Ok(_) => println!("{}\t{}", key, value), Err(err) => eprintln!("{}", err), } + + Ok(()) } -pub fn remove_action(c: &Context) { +pub fn remove_action(c: &Context) -> ActionResult { let mut conf = get_json_object_or_create(c.bool_flag("force-create")); let Some(key) = c.args.get(0) else { - return invalid("key"); + return Err(invalid("key")); }; if !conf.has_key(&key) { println!("key '{}' was not found", key); - return; + return Ok(()); } conf.remove(&key); @@ -106,4 +118,6 @@ pub fn remove_action(c: &Context) { Ok(_) => println!("updated config file"), Err(err) => eprintln!("{}", err), } + + Ok(()) } diff --git a/src/commands.rs b/src/commands.rs index 7d73dee..30696d2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,7 +10,7 @@ pub fn init() -> Command { .description("inits config file") .alias("i") .usage(format!("{} init", env!("CARGO_PKG_NAME"))) - .action(init_action) + .action_with_result(init_action) } pub fn list() -> Command { @@ -18,7 +18,7 @@ pub fn list() -> Command { .description("list all keys and values") .alias("l") .usage(format!("{} list", env!("CARGO_PKG_NAME"))) - .action(list_action) + .action_with_result(list_action) .flag(force_create()) } @@ -27,7 +27,7 @@ pub fn clear() -> Command { .description("clear your config file") .alias("c") .usage(format!("{} clear", env!("CARGO_PKG_NAME"))) - .action(clear_action) + .action_with_result(clear_action) } pub fn remove_value() -> Command { @@ -35,7 +35,7 @@ pub fn remove_value() -> Command { .description("remove a value") .alias("r") .usage(format!("{} remove foo", env!("CARGO_PKG_NAME"))) - .action(remove_action) + .action_with_result(remove_action) } pub fn get_value() -> Command { @@ -43,7 +43,7 @@ pub fn get_value() -> Command { .description("get a value") .alias("g") .usage(format!("{} get foo", env!("CARGO_PKG_NAME"))) - .action(get_action) + .action_with_result(get_action) .flag(ignore_null()) .flag(force_create()) } @@ -53,6 +53,6 @@ pub fn set_value() -> Command { .description("set a value") .alias("s") .usage(format!("{} set foo bar", env!("CARGO_PKG_NAME"))) - .action(set_action) + .action_with_result(set_action) .flag(force_create()) } diff --git a/src/error.rs b/src/error.rs index c8645b0..8805dcd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,11 @@ -use std::process::exit; +use seahorse::ActionError; -pub fn invalid(cause: &str) { - eprintln!( - "invalid {}. get help by running `{} --help`", - cause, - env!("CARGO_PKG_NAME") - ); - exit(1); +pub fn invalid(cause: &str) -> ActionError { + ActionError { + message: format!( + "invalid {}. get help by running `{} --help`", + cause, + env!("CARGO_PKG_NAME") + ), + } } diff --git a/src/main.rs b/src/main.rs index b6f78b4..cb0d97b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use std::{env, io}; +use std::{env, process::exit}; -use seahorse::App; +use seahorse::{ActionResult, App}; use crate::commands::{clear, get_value, init, list, remove_value, set_value}; @@ -11,7 +11,7 @@ mod error; mod flags; mod json_object; -fn main() -> io::Result<()> { +fn main() -> ActionResult { let args: Vec = env::args().collect(); let app = App::new(env!("CARGO_PKG_NAME")) .description(env!("CARGO_PKG_DESCRIPTION")) @@ -25,7 +25,11 @@ fn main() -> io::Result<()> { .command(remove_value()) .command(clear()); - app.run(args); - - Ok(()) + match app.run_with_result(args) { + Ok(_) => Ok(()), + Err(action_error) => { + eprintln!("{}", action_error.message); + exit(1) + } + } } From 044fe98ded4ade4b736c0414fb2b15abb6851357 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:39:24 -0300 Subject: [PATCH 3/9] fix: change set_action message --- src/actions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions.rs b/src/actions.rs index 2a7f8aa..6c72760 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -94,7 +94,7 @@ pub fn set_action(c: &Context) -> ActionResult { conf.insert(key, value).unwrap(); match set_json_object(conf) { - Ok(_) => println!("{}\t{}", key, value), + Ok(_) => println!("'{}' -> '{}'", key, value), Err(err) => eprintln!("{}", err), } From 648f4a7c4a6811d9d35809e86f9bb61af3f0c30b Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:01:03 -0300 Subject: [PATCH 4/9] feat: storage trait - storage trait - json storage implementation --- src/actions.rs | 85 ++++++++++++++++------------------- src/json_object.rs | 45 ------------------- src/main.rs | 8 ++-- src/storage/json.rs | 103 +++++++++++++++++++++++++++++++++++++++++++ src/storage/mod.rs | 14 ++++++ src/storage/value.rs | 60 +++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 95 deletions(-) delete mode 100644 src/json_object.rs create mode 100644 src/storage/json.rs create mode 100644 src/storage/mod.rs create mode 100644 src/storage/value.rs diff --git a/src/actions.rs b/src/actions.rs index 6c72760..1247ad4 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,31 +1,29 @@ -use std::fs::File; -use std::io::Write; use std::path::Path; -use json::JsonValue; use seahorse::{ActionError, ActionResult, Context}; use crate::config::get_config_path; use crate::error::invalid; -use crate::json_object::{get_json_object_or_create, set_json_object}; +use crate::storage::json::CfsJSONStore; +use crate::storage::{CfsStorage, CfsValue}; -pub fn init_action(c: &Context) -> ActionResult { +pub fn init_action(_c: &Context) -> ActionResult { let config_path = get_config_path(); let path = Path::new(&config_path); if path.exists() { println!("config file already exists"); - } else { - clear_action(c)?; } + CfsJSONStore::with_force_create(true); + Ok(()) } pub fn list_action(c: &Context) -> ActionResult { - let conf = get_json_object_or_create(c.bool_flag("force-create")); + let store = CfsJSONStore::with_force_create(c.bool_flag("force-create")); - for (key, value) in conf.entries() { + for (key, value) in store.all().iter() { println!("{}\t{}", key, value); } @@ -33,9 +31,10 @@ pub fn list_action(c: &Context) -> ActionResult { } pub fn clear_action(_c: &Context) -> ActionResult { - let mut file = File::create(get_config_path()).unwrap(); + let mut store = CfsJSONStore::new(); + + store.clear(); - write!(file, "{}", "{}").expect("couldn't overwrite config file"); println!("cleared config file at '{:?}'", get_config_path()); Ok(()) @@ -46,24 +45,29 @@ pub fn get_action(c: &Context) -> ActionResult { return Err(invalid("command")); } - let conf = get_json_object_or_create(c.bool_flag("force-create")); - let key = c.args.get(0); + let key = c.args.get(0).to_owned(); let Some(key) = key else { return Err(invalid("key")); }; - if conf.has_key(&key) { - println!("{}", conf[key]); - return Ok(()); - } - - if c.bool_flag("ignore-null") { - println!(); - } else { - return Err(ActionError { - message: format!("could not find key '{}'", key), - }); + let store = CfsJSONStore::new(); + + let value = store.get(key); + + match value { + Some(v) => { + println!("{}", v) + } + None => { + if c.bool_flag("ignore_null") { + println!(); + } else { + return Err(ActionError { + message: format!("could not find key '{}'", key), + }); + } + } } Ok(()) @@ -74,8 +78,6 @@ pub fn set_action(c: &Context) -> ActionResult { return Err(invalid("command")); } - let mut conf = get_json_object_or_create(c.bool_flag("force-create")); - let Some(key) = c.args.get(0) else { return Err(invalid("key")); }; @@ -84,39 +86,28 @@ pub fn set_action(c: &Context) -> ActionResult { return Err(invalid("value")); }; - let json_value = JsonValue::from(value_str.as_str()); - let value = json_value.as_str().unwrap(); + let mut store = CfsJSONStore::new(); - if conf.has_key(key) { - conf.remove(key); - } - - conf.insert(key, value).unwrap(); + let value = CfsValue::Value(value_str.to_owned()); + store.set(key, value.clone()); - match set_json_object(conf) { - Ok(_) => println!("'{}' -> '{}'", key, value), - Err(err) => eprintln!("{}", err), - } + println!("{}\t{}", key, value); Ok(()) } pub fn remove_action(c: &Context) -> ActionResult { - let mut conf = get_json_object_or_create(c.bool_flag("force-create")); let Some(key) = c.args.get(0) else { return Err(invalid("key")); }; - if !conf.has_key(&key) { - println!("key '{}' was not found", key); - return Ok(()); - } - - conf.remove(&key); + let mut store = CfsJSONStore::new(); - match set_json_object(conf) { - Ok(_) => println!("updated config file"), - Err(err) => eprintln!("{}", err), + match store.remove(key) { + Some(value) => println!("{}\t{}", key, value), + None => { + println!("key '{}' was not found", key); + } } Ok(()) diff --git a/src/json_object.rs b/src/json_object.rs deleted file mode 100644 index 44a948c..0000000 --- a/src/json_object.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fs::{read_to_string, File}; -use std::io; -use std::io::Write; -use std::process::exit; - -use json::JsonValue; - -use crate::config::get_config_path; - -pub fn get_json_object_or_create(force_create: bool) -> JsonValue { - let path_exists = &get_config_path().exists(); - - if force_create && !path_exists { - let mut file = File::create(get_config_path()).unwrap(); - write!(file, "{}", "{}").unwrap(); - } - - get_json_object() -} - -pub fn get_json_object() -> JsonValue { - let path = get_config_path(); - - if !path.exists() { - eprintln!("config file does not exist at '{:?}'", &path); - exit(1); - } - - let json = json::parse(&*read_to_string(&path).unwrap()).unwrap(); - - if !json.is_object() { - eprintln!("config file is not a JSON file ('{:?}')", &path); - exit(1); - } - - return json; -} - -pub fn set_json_object(json: JsonValue) -> io::Result<()> { - let mut file = File::create(get_config_path())?; - let json_string = json::stringify_pretty(json, 2); - write!(file, "{}", json_string)?; - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index cb0d97b..1c46afe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ mod commands; mod config; mod error; mod flags; -mod json_object; +mod storage; fn main() -> ActionResult { let args: Vec = env::args().collect(); @@ -26,10 +26,12 @@ fn main() -> ActionResult { .command(clear()); match app.run_with_result(args) { - Ok(_) => Ok(()), + Ok(_) => (), Err(action_error) => { eprintln!("{}", action_error.message); exit(1) } - } + }; + + Ok(()) } diff --git a/src/storage/json.rs b/src/storage/json.rs new file mode 100644 index 0000000..ee15ddd --- /dev/null +++ b/src/storage/json.rs @@ -0,0 +1,103 @@ +use std::fs::{read_to_string, File}; +use std::io; +use std::io::Write; +use std::process::exit; + +use json::JsonValue; + +use crate::config::get_config_path; +use crate::storage::{CfsStorage, CfsValue}; + +pub fn init_store(force_create: bool) -> JsonValue { + let path = get_config_path(); + + if !path.exists() && force_create { + let mut file = File::create(get_config_path()).unwrap(); + write!(file, "{}", "{}").unwrap(); + } else if !path.exists() { + eprintln!("config file does not exist at '{:?}'", &path); + exit(1); + } + + let json = json::parse(&read_to_string(&path).unwrap()).unwrap(); + + if !json.is_object() { + eprintln!("config file is not a JSON file ('{:?}')", &path); + exit(1); + } + + json +} + +#[derive(Clone, Debug)] +pub struct CfsJSONStore { + store: JsonValue, +} + +impl CfsJSONStore { + pub fn new() -> Self { + return Self { + store: init_store(false), + }; + } + + pub fn with_force_create(force_create: bool) -> Self { + return Self { + store: init_store(force_create), + }; + } + + fn save_store(&mut self) -> Result<(), io::Error> { + let mut file = File::create(get_config_path())?; + + let json_string = json::stringify_pretty(self.store.clone(), 2); + + write!(file, "{}", json_string)?; + + Ok(()) + } +} + +impl CfsStorage for CfsJSONStore { + fn all(&self) -> Vec<(String, CfsValue)> { + self + .store + .entries() + .map(|(key, value)| (key.to_owned(), value.into())) + .collect() + } + + fn get(&self, key: &str) -> Option { + if !self.store.has_key(key) { + return None; + } + + Some(self.store[key].clone().into()) + } + + fn set(&mut self, key: &str, value: CfsValue) -> CfsValue { + self.store.insert(key, value.clone()).unwrap(); + + self.save_store().unwrap(); + + value + } + + fn remove(&mut self, key: &str) -> Option { + if !self.store.has_key(key) { + return None; + } + + let value = self.store.remove(key); + + self.save_store().unwrap(); + + return Some(value.into()); + } + + fn clear(&mut self) { + self.store.clear(); + + self.save_store().unwrap(); + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..8976d1a --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,14 @@ +pub mod json; +mod value; + +pub use value::CfsValue; + +pub trait CfsStorage { + fn all(&self) -> Vec<(String, CfsValue)>; + + fn get(&self, key: &str) -> Option; + fn set(&mut self, key: &str, value: CfsValue) -> CfsValue; + fn remove(&mut self, key: &str) -> Option; + + fn clear(&mut self); +} diff --git a/src/storage/value.rs b/src/storage/value.rs new file mode 100644 index 0000000..3c559c6 --- /dev/null +++ b/src/storage/value.rs @@ -0,0 +1,60 @@ +use std::fmt::Display; + +use json::JsonValue; + +#[derive(Debug, Clone)] +pub enum CfsValue { + Value(String), + List(Vec), +} + +impl Display for CfsValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CfsValue::Value(v) => write!(f, "{}", v), + CfsValue::List(items) => { + let mut array_string = String::new(); + for i in items.iter() { + array_string.push_str(&format!("{},", i)); + } + + write!(f, "[{}]", array_string) + } + } + } +} + +impl From for JsonValue { + fn from(value: CfsValue) -> Self { + match value { + CfsValue::Value(string) => JsonValue::String(string), + CfsValue::List(items) => { + JsonValue::Array(items.into_iter().map(|i| JsonValue::String(i)).collect()) + } + } + } +} + +impl From for CfsValue { + fn from(value: JsonValue) -> Self { + match value { + JsonValue::Array(json_values) => { + CfsValue::List(json_values.iter().map(|f| f.to_string()).collect()) + } + JsonValue::String(string) => CfsValue::Value(string), + _ => CfsValue::Value(value.to_string()), + } + } +} + +impl From<&JsonValue> for CfsValue { + fn from(value: &JsonValue) -> Self { + match value { + JsonValue::Array(json_values) => { + CfsValue::List(json_values.iter().map(|f| f.to_string()).collect()) + } + JsonValue::String(string) => CfsValue::Value(string.clone()), + _ => CfsValue::Value(value.to_string()), + } + } +} From d9140443e14b47e07e84ba72cd3de69ce04faf3b Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:08:00 -0300 Subject: [PATCH 5/9] refactor: remove Cfs prefix from store types --- src/actions.rs | 18 +++++++++--------- src/storage/json.rs | 16 ++++++++-------- src/storage/mod.rs | 13 +++++++------ src/storage/value.rs | 32 ++++++++++++++++---------------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/actions.rs b/src/actions.rs index 1247ad4..cf1132d 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -4,8 +4,8 @@ use seahorse::{ActionError, ActionResult, Context}; use crate::config::get_config_path; use crate::error::invalid; -use crate::storage::json::CfsJSONStore; -use crate::storage::{CfsStorage, CfsValue}; +use crate::storage::json::JSONStore; +use crate::storage::{Store, StoreValue}; pub fn init_action(_c: &Context) -> ActionResult { let config_path = get_config_path(); @@ -15,13 +15,13 @@ pub fn init_action(_c: &Context) -> ActionResult { println!("config file already exists"); } - CfsJSONStore::with_force_create(true); + JSONStore::with_force_create(true); Ok(()) } pub fn list_action(c: &Context) -> ActionResult { - let store = CfsJSONStore::with_force_create(c.bool_flag("force-create")); + let store = JSONStore::with_force_create(c.bool_flag("force-create")); for (key, value) in store.all().iter() { println!("{}\t{}", key, value); @@ -31,7 +31,7 @@ pub fn list_action(c: &Context) -> ActionResult { } pub fn clear_action(_c: &Context) -> ActionResult { - let mut store = CfsJSONStore::new(); + let mut store = JSONStore::new(); store.clear(); @@ -51,7 +51,7 @@ pub fn get_action(c: &Context) -> ActionResult { return Err(invalid("key")); }; - let store = CfsJSONStore::new(); + let store = JSONStore::new(); let value = store.get(key); @@ -86,9 +86,9 @@ pub fn set_action(c: &Context) -> ActionResult { return Err(invalid("value")); }; - let mut store = CfsJSONStore::new(); + let mut store = JSONStore::new(); - let value = CfsValue::Value(value_str.to_owned()); + let value = StoreValue::Value(value_str.to_owned()); store.set(key, value.clone()); println!("{}\t{}", key, value); @@ -101,7 +101,7 @@ pub fn remove_action(c: &Context) -> ActionResult { return Err(invalid("key")); }; - let mut store = CfsJSONStore::new(); + let mut store = JSONStore::new(); match store.remove(key) { Some(value) => println!("{}\t{}", key, value), diff --git a/src/storage/json.rs b/src/storage/json.rs index ee15ddd..297eaef 100644 --- a/src/storage/json.rs +++ b/src/storage/json.rs @@ -6,7 +6,7 @@ use std::process::exit; use json::JsonValue; use crate::config::get_config_path; -use crate::storage::{CfsStorage, CfsValue}; +use crate::storage::{Store, StoreValue}; pub fn init_store(force_create: bool) -> JsonValue { let path = get_config_path(); @@ -30,11 +30,11 @@ pub fn init_store(force_create: bool) -> JsonValue { } #[derive(Clone, Debug)] -pub struct CfsJSONStore { +pub struct JSONStore { store: JsonValue, } -impl CfsJSONStore { +impl JSONStore { pub fn new() -> Self { return Self { store: init_store(false), @@ -58,8 +58,8 @@ impl CfsJSONStore { } } -impl CfsStorage for CfsJSONStore { - fn all(&self) -> Vec<(String, CfsValue)> { +impl Store for JSONStore { + fn all(&self) -> Vec<(String, StoreValue)> { self .store .entries() @@ -67,7 +67,7 @@ impl CfsStorage for CfsJSONStore { .collect() } - fn get(&self, key: &str) -> Option { + fn get(&self, key: &str) -> Option { if !self.store.has_key(key) { return None; } @@ -75,7 +75,7 @@ impl CfsStorage for CfsJSONStore { Some(self.store[key].clone().into()) } - fn set(&mut self, key: &str, value: CfsValue) -> CfsValue { + fn set(&mut self, key: &str, value: StoreValue) -> StoreValue { self.store.insert(key, value.clone()).unwrap(); self.save_store().unwrap(); @@ -83,7 +83,7 @@ impl CfsStorage for CfsJSONStore { value } - fn remove(&mut self, key: &str) -> Option { + fn remove(&mut self, key: &str) -> Option { if !self.store.has_key(key) { return None; } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 8976d1a..2028996 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,14 +1,15 @@ pub mod json; +mod sqlite; mod value; -pub use value::CfsValue; +pub use value::StoreValue; -pub trait CfsStorage { - fn all(&self) -> Vec<(String, CfsValue)>; +pub trait Store { + fn all(&self) -> Vec<(String, StoreValue)>; - fn get(&self, key: &str) -> Option; - fn set(&mut self, key: &str, value: CfsValue) -> CfsValue; - fn remove(&mut self, key: &str) -> Option; + fn get(&self, key: &str) -> Option; + fn set(&mut self, key: &str, value: StoreValue) -> StoreValue; + fn remove(&mut self, key: &str) -> Option; fn clear(&mut self); } diff --git a/src/storage/value.rs b/src/storage/value.rs index 3c559c6..d69a683 100644 --- a/src/storage/value.rs +++ b/src/storage/value.rs @@ -3,16 +3,16 @@ use std::fmt::Display; use json::JsonValue; #[derive(Debug, Clone)] -pub enum CfsValue { +pub enum StoreValue { Value(String), List(Vec), } -impl Display for CfsValue { +impl Display for StoreValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - CfsValue::Value(v) => write!(f, "{}", v), - CfsValue::List(items) => { + StoreValue::Value(v) => write!(f, "{}", v), + StoreValue::List(items) => { let mut array_string = String::new(); for i in items.iter() { array_string.push_str(&format!("{},", i)); @@ -24,37 +24,37 @@ impl Display for CfsValue { } } -impl From for JsonValue { - fn from(value: CfsValue) -> Self { +impl From for JsonValue { + fn from(value: StoreValue) -> Self { match value { - CfsValue::Value(string) => JsonValue::String(string), - CfsValue::List(items) => { + StoreValue::Value(string) => JsonValue::String(string), + StoreValue::List(items) => { JsonValue::Array(items.into_iter().map(|i| JsonValue::String(i)).collect()) } } } } -impl From for CfsValue { +impl From for StoreValue { fn from(value: JsonValue) -> Self { match value { JsonValue::Array(json_values) => { - CfsValue::List(json_values.iter().map(|f| f.to_string()).collect()) + StoreValue::List(json_values.iter().map(|f| f.to_string()).collect()) } - JsonValue::String(string) => CfsValue::Value(string), - _ => CfsValue::Value(value.to_string()), + JsonValue::String(string) => StoreValue::Value(string), + _ => StoreValue::Value(value.to_string()), } } } -impl From<&JsonValue> for CfsValue { +impl From<&JsonValue> for StoreValue { fn from(value: &JsonValue) -> Self { match value { JsonValue::Array(json_values) => { - CfsValue::List(json_values.iter().map(|f| f.to_string()).collect()) + StoreValue::List(json_values.iter().map(|f| f.to_string()).collect()) } - JsonValue::String(string) => CfsValue::Value(string.clone()), - _ => CfsValue::Value(value.to_string()), + JsonValue::String(string) => StoreValue::Value(string.clone()), + _ => StoreValue::Value(value.to_string()), } } } From 8e6fd80a209b6281756023cb84bdd8c8a537de82 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:56:27 -0300 Subject: [PATCH 6/9] feat: sqlite store initial implementation --- Cargo.toml | 1 + src/actions.rs | 22 ++++---- src/config.rs | 11 ++-- src/storage/mod.rs | 9 +++- src/storage/sqlite/mod.rs | 94 +++++++++++++++++++++++++++++++++++ src/storage/sqlite/schema.sql | 7 +++ 6 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 src/storage/sqlite/mod.rs create mode 100644 src/storage/sqlite/schema.sql diff --git a/Cargo.toml b/Cargo.toml index ff551da..4610035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ categories = ["command-line-utilities"] [dependencies] json = "0.12.4" +rusqlite = { version = "0.39.0", features = ["bundled"] } seahorse = "2.1.0" diff --git a/src/actions.rs b/src/actions.rs index cf1132d..d159b78 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -2,26 +2,26 @@ use std::path::Path; use seahorse::{ActionError, ActionResult, Context}; -use crate::config::get_config_path; +use crate::config::{get_config_path, get_db_path}; use crate::error::invalid; -use crate::storage::json::JSONStore; -use crate::storage::{Store, StoreValue}; +use crate::storage::sqlite::SQLiteStore; +use crate::storage::{self, Store, StoreValue}; pub fn init_action(_c: &Context) -> ActionResult { - let config_path = get_config_path(); + let config_path = get_db_path(); let path = Path::new(&config_path); if path.exists() { println!("config file already exists"); } - JSONStore::with_force_create(true); + SQLiteStore::from_path(path); Ok(()) } pub fn list_action(c: &Context) -> ActionResult { - let store = JSONStore::with_force_create(c.bool_flag("force-create")); + let store = storage::load_storage(); for (key, value) in store.all().iter() { println!("{}\t{}", key, value); @@ -31,7 +31,7 @@ pub fn list_action(c: &Context) -> ActionResult { } pub fn clear_action(_c: &Context) -> ActionResult { - let mut store = JSONStore::new(); + let mut store = storage::load_storage(); store.clear(); @@ -51,7 +51,7 @@ pub fn get_action(c: &Context) -> ActionResult { return Err(invalid("key")); }; - let store = JSONStore::new(); + let store = storage::load_storage(); let value = store.get(key); @@ -86,12 +86,12 @@ pub fn set_action(c: &Context) -> ActionResult { return Err(invalid("value")); }; - let mut store = JSONStore::new(); + let mut store = storage::load_storage(); let value = StoreValue::Value(value_str.to_owned()); store.set(key, value.clone()); - println!("{}\t{}", key, value); + println!("'{}' -> '{}'", key, value); Ok(()) } @@ -101,7 +101,7 @@ pub fn remove_action(c: &Context) -> ActionResult { return Err(invalid("key")); }; - let mut store = JSONStore::new(); + let mut store = storage::load_storage(); match store.remove(key) { Some(value) => println!("{}\t{}", key, value), diff --git a/src/config.rs b/src/config.rs index 7aa09c1..52e8660 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,13 @@ use std::{env::home_dir, path::PathBuf}; +pub fn get_config_folder_path() -> PathBuf { + return home_dir().expect("couldn't find home directory"); +} + pub fn get_config_path() -> PathBuf { - let home_folder = home_dir().expect("couldn't find home directory"); - let path = home_folder.join(".cfs.json"); + return get_config_folder_path().join(".cfs.json"); +} - return path; +pub fn get_db_path() -> PathBuf { + return get_config_folder_path().join(".cfs.db"); } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 2028996..32e7c72 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,9 +1,11 @@ pub mod json; -mod sqlite; +pub mod sqlite; mod value; pub use value::StoreValue; +use crate::config::get_db_path; + pub trait Store { fn all(&self) -> Vec<(String, StoreValue)>; @@ -13,3 +15,8 @@ pub trait Store { fn clear(&mut self); } + +//TODO: Change STORE Based on config. +pub fn load_storage() -> impl Store { + return sqlite::SQLiteStore::from_path(get_db_path()); +} diff --git a/src/storage/sqlite/mod.rs b/src/storage/sqlite/mod.rs new file mode 100644 index 0000000..26b4d22 --- /dev/null +++ b/src/storage/sqlite/mod.rs @@ -0,0 +1,94 @@ +use std::path::Path; + +use rusqlite::OptionalExtension as _; + +use crate::storage::{Store, StoreValue}; + +#[derive(Debug)] +pub struct SQLiteStore { + connection: rusqlite::Connection, +} + +impl SQLiteStore { + pub fn from_path>(path: P) -> Self { + let conn = rusqlite::Connection::open(path).expect("To Open SQLite DB"); + + conn + .execute_batch(include_str!("schema.sql")) + .expect("To Create DB"); + + return Self { connection: conn }; + } +} + +impl Store for SQLiteStore { + fn all(&self) -> Vec<(String, super::StoreValue)> { + let mut stmt = self.connection.prepare("SELECT key,value from KV").unwrap(); + + let values = stmt + .query_map([], |row| { + let key: String = row.get(0)?; + let value = StoreValue::Value(row.get::<_, String>(1)?); + + return Ok((key, value)); + }) + .unwrap() + .collect::, _>>() + .unwrap(); + + return values; + } + + fn get(&self, key: &str) -> Option { + let stmt = self + .connection + .query_row( + "SELECT key,value from KV where key = ?1 LIMIT 1", + [key], + |row| Ok(StoreValue::Value(row.get(1).unwrap())), + ) + .optional() + .unwrap(); + + return stmt; + } + + fn set(&mut self, key: &str, value: super::StoreValue) -> super::StoreValue { + let StoreValue::Value(value) = value else { + panic!("Invalid value passed into SQLiteStore GET [{}]", value) + }; + + self + .connection + .execute( + "INSERT INTO KV VALUES(NULL,?1,?2) ON CONFLICT(key) DO UPDATE SET value = ?2 WHERE key = ?1", + [key, &value], + ) + .unwrap(); + + return StoreValue::Value(value); + } + + fn remove(&mut self, key: &str) -> Option { + let value = self.get(key); + + let Some(value) = value else { + return None; + }; + + let stmt = self + .connection + .execute("DELETE FROM KV where key = ?1", [key]) + .unwrap(); + + if stmt == 0 { + panic!("Deleted 0 Rows when trying to delete Value from Store") + } + + Some(value) + } + + fn clear(&mut self) { + self.connection.execute("DELETE FROM KV", []).unwrap(); + } +} diff --git a/src/storage/sqlite/schema.sql b/src/storage/sqlite/schema.sql new file mode 100644 index 0000000..37d54a7 --- /dev/null +++ b/src/storage/sqlite/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS KV ( + id INTEGER PRIMARY KEY, + key TEXT NOT NULL, + value TEXT NOT NULL + ); + +CREATE UNIQUE INDEX IF NOT EXISTS kv_keys ON KV (key); From 05e6099eed0abc102dd4f75d07f10ece0db49754 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:40:27 -0300 Subject: [PATCH 7/9] wip: add anyhow results to store trait --- Cargo.toml | 1 + src/actions.rs | 18 +++++------ src/error.rs | 6 ++++ src/storage/json.rs | 37 +++++++++++++--------- src/storage/mod.rs | 12 +++++--- src/storage/sqlite/mod.rs | 65 +++++++++++++++++---------------------- 6 files changed, 74 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4610035..630bd81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["command-line-utilities"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.102" json = "0.12.4" rusqlite = { version = "0.39.0", features = ["bundled"] } seahorse = "2.1.0" diff --git a/src/actions.rs b/src/actions.rs index d159b78..84e33be 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -2,8 +2,8 @@ use std::path::Path; use seahorse::{ActionError, ActionResult, Context}; -use crate::config::{get_config_path, get_db_path}; -use crate::error::invalid; +use crate::config::get_db_path; +use crate::error::{invalid, to_action_error}; use crate::storage::sqlite::SQLiteStore; use crate::storage::{self, Store, StoreValue}; @@ -20,10 +20,10 @@ pub fn init_action(_c: &Context) -> ActionResult { Ok(()) } -pub fn list_action(c: &Context) -> ActionResult { +pub fn list_action(_c: &Context) -> ActionResult { let store = storage::load_storage(); - for (key, value) in store.all().iter() { + for (key, value) in store.all().map_err(to_action_error)?.iter() { println!("{}\t{}", key, value); } @@ -33,9 +33,9 @@ pub fn list_action(c: &Context) -> ActionResult { pub fn clear_action(_c: &Context) -> ActionResult { let mut store = storage::load_storage(); - store.clear(); + let count = store.clear().map_err(to_action_error)?; - println!("cleared config file at '{:?}'", get_config_path()); + println!("removed {} keys from store", count); Ok(()) } @@ -53,7 +53,7 @@ pub fn get_action(c: &Context) -> ActionResult { let store = storage::load_storage(); - let value = store.get(key); + let value = store.get(key).map_err(to_action_error)?; match value { Some(v) => { @@ -89,7 +89,7 @@ pub fn set_action(c: &Context) -> ActionResult { let mut store = storage::load_storage(); let value = StoreValue::Value(value_str.to_owned()); - store.set(key, value.clone()); + store.set(key, value.clone()).map_err(to_action_error)?; println!("'{}' -> '{}'", key, value); @@ -103,7 +103,7 @@ pub fn remove_action(c: &Context) -> ActionResult { let mut store = storage::load_storage(); - match store.remove(key) { + match store.remove(key).map_err(to_action_error)? { Some(value) => println!("{}\t{}", key, value), None => { println!("key '{}' was not found", key); diff --git a/src/error.rs b/src/error.rs index 8805dcd..f23f60a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,3 +9,9 @@ pub fn invalid(cause: &str) -> ActionError { ), } } + +pub fn to_action_error(err: anyhow::Error) -> ActionError { + return ActionError { + message: err.to_string(), + }; +} diff --git a/src/storage/json.rs b/src/storage/json.rs index 297eaef..b3a8f6a 100644 --- a/src/storage/json.rs +++ b/src/storage/json.rs @@ -1,3 +1,5 @@ +use anyhow::Result; + use std::fs::{read_to_string, File}; use std::io; use std::io::Write; @@ -59,45 +61,50 @@ impl JSONStore { } impl Store for JSONStore { - fn all(&self) -> Vec<(String, StoreValue)> { - self - .store - .entries() - .map(|(key, value)| (key.to_owned(), value.into())) - .collect() + fn all(&self) -> Result> { + Ok( + self + .store + .entries() + .map(|(key, value)| (key.to_owned(), value.into())) + .collect(), + ) } - fn get(&self, key: &str) -> Option { + fn get(&self, key: &str) -> Result> { if !self.store.has_key(key) { - return None; + return Ok(None); } - Some(self.store[key].clone().into()) + Ok(Some(self.store[key].clone().into())) } - fn set(&mut self, key: &str, value: StoreValue) -> StoreValue { + fn set(&mut self, key: &str, value: StoreValue) -> Result { self.store.insert(key, value.clone()).unwrap(); self.save_store().unwrap(); - value + Ok(value) } - fn remove(&mut self, key: &str) -> Option { + fn remove(&mut self, key: &str) -> Result> { if !self.store.has_key(key) { - return None; + return Ok(None); } let value = self.store.remove(key); self.save_store().unwrap(); - return Some(value.into()); + return Ok(Some(value.into())); } - fn clear(&mut self) { + fn clear(&mut self) -> Result { + let len = self.store.len(); self.store.clear(); self.save_store().unwrap(); + + return Ok(len); } } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 32e7c72..ab323dc 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,3 +1,5 @@ +use anyhow::Result; + pub mod json; pub mod sqlite; mod value; @@ -7,13 +9,13 @@ pub use value::StoreValue; use crate::config::get_db_path; pub trait Store { - fn all(&self) -> Vec<(String, StoreValue)>; + fn all(&self) -> Result>; - fn get(&self, key: &str) -> Option; - fn set(&mut self, key: &str, value: StoreValue) -> StoreValue; - fn remove(&mut self, key: &str) -> Option; + fn get(&self, key: &str) -> Result>; + fn set(&mut self, key: &str, value: StoreValue) -> Result; + fn remove(&mut self, key: &str) -> Result>; - fn clear(&mut self); + fn clear(&mut self) -> Result; } //TODO: Change STORE Based on config. diff --git a/src/storage/sqlite/mod.rs b/src/storage/sqlite/mod.rs index 26b4d22..55430cb 100644 --- a/src/storage/sqlite/mod.rs +++ b/src/storage/sqlite/mod.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use std::path::Path; use rusqlite::OptionalExtension as _; @@ -17,78 +18,70 @@ impl SQLiteStore { .execute_batch(include_str!("schema.sql")) .expect("To Create DB"); - return Self { connection: conn }; + Self { connection: conn } } } impl Store for SQLiteStore { - fn all(&self) -> Vec<(String, super::StoreValue)> { - let mut stmt = self.connection.prepare("SELECT key,value from KV").unwrap(); + fn all(&self) -> Result> { + let mut stmt = self.connection.prepare("SELECT key,value from KV")?; let values = stmt - .query_map([], |row| { - let key: String = row.get(0)?; - let value = StoreValue::Value(row.get::<_, String>(1)?); + .query_map([], |row| Ok((row.get(0)?, StoreValue::Value(row.get(1)?))))? + .collect::, _>>()?; - return Ok((key, value)); - }) - .unwrap() - .collect::, _>>() - .unwrap(); - - return values; + Ok(values) } - fn get(&self, key: &str) -> Option { + fn get(&self, key: &str) -> Result> { let stmt = self .connection .query_row( "SELECT key,value from KV where key = ?1 LIMIT 1", [key], - |row| Ok(StoreValue::Value(row.get(1).unwrap())), + |row| Ok(StoreValue::Value(row.get(1)?)), ) - .optional() - .unwrap(); + .optional()?; - return stmt; + Ok(stmt) } - fn set(&mut self, key: &str, value: super::StoreValue) -> super::StoreValue { + fn set(&mut self, key: &str, value: StoreValue) -> Result { let StoreValue::Value(value) = value else { - panic!("Invalid value passed into SQLiteStore GET [{}]", value) + return Err(anyhow!( + "Invalid value passed into SQLiteStore GET [{}]", + value + )); }; - self - .connection - .execute( - "INSERT INTO KV VALUES(NULL,?1,?2) ON CONFLICT(key) DO UPDATE SET value = ?2 WHERE key = ?1", - [key, &value], - ) - .unwrap(); + self.connection.execute( + "INSERT INTO KV VALUES(NULL,?1,?2) ON CONFLICT(key) DO UPDATE SET value = ?2 WHERE key = ?1", + [key, &value], + )?; - return StoreValue::Value(value); + Ok(StoreValue::Value(value)) } - fn remove(&mut self, key: &str) -> Option { - let value = self.get(key); + fn remove(&mut self, key: &str) -> Result> { + let value = self.get(key)?; let Some(value) = value else { - return None; + return Ok(None); }; let stmt = self .connection - .execute("DELETE FROM KV where key = ?1", [key]) - .unwrap(); + .execute("DELETE FROM KV where key = ?1", [key])?; if stmt == 0 { panic!("Deleted 0 Rows when trying to delete Value from Store") } - Some(value) + Ok(Some(value)) } - fn clear(&mut self) { - self.connection.execute("DELETE FROM KV", []).unwrap(); + fn clear(&mut self) -> Result { + let deleted = self.connection.execute("DELETE FROM KV", [])?; + Ok(deleted) } } From 58892f91a1b2d2535fcd99bf9df3fd832d04eab0 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:50:03 -0300 Subject: [PATCH 8/9] fix: remove where in upsert query --- src/storage/sqlite/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/sqlite/mod.rs b/src/storage/sqlite/mod.rs index 55430cb..59afab2 100644 --- a/src/storage/sqlite/mod.rs +++ b/src/storage/sqlite/mod.rs @@ -55,7 +55,7 @@ impl Store for SQLiteStore { }; self.connection.execute( - "INSERT INTO KV VALUES(NULL,?1,?2) ON CONFLICT(key) DO UPDATE SET value = ?2 WHERE key = ?1", + "INSERT INTO KV VALUES(NULL,?1,?2) ON CONFLICT(key) DO UPDATE SET value = ?2", [key, &value], )?; From 938e4bd9cee811becc08a3431d80735c0bf635c9 Mon Sep 17 00:00:00 2001 From: lzldev <38144471+lzldev@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:50:56 -0300 Subject: [PATCH 9/9] refactor: rename query variables --- src/storage/sqlite/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/storage/sqlite/mod.rs b/src/storage/sqlite/mod.rs index 59afab2..773a1de 100644 --- a/src/storage/sqlite/mod.rs +++ b/src/storage/sqlite/mod.rs @@ -24,9 +24,9 @@ impl SQLiteStore { impl Store for SQLiteStore { fn all(&self) -> Result> { - let mut stmt = self.connection.prepare("SELECT key,value from KV")?; + let mut query = self.connection.prepare("SELECT key,value from KV")?; - let values = stmt + let values = query .query_map([], |row| Ok((row.get(0)?, StoreValue::Value(row.get(1)?))))? .collect::, _>>()?; @@ -34,7 +34,7 @@ impl Store for SQLiteStore { } fn get(&self, key: &str) -> Result> { - let stmt = self + let query = self .connection .query_row( "SELECT key,value from KV where key = ?1 LIMIT 1", @@ -43,7 +43,7 @@ impl Store for SQLiteStore { ) .optional()?; - Ok(stmt) + Ok(query) } fn set(&mut self, key: &str, value: StoreValue) -> Result { @@ -69,11 +69,11 @@ impl Store for SQLiteStore { return Ok(None); }; - let stmt = self + let query = self .connection .execute("DELETE FROM KV where key = ?1", [key])?; - if stmt == 0 { + if query == 0 { panic!("Deleted 0 Rows when trying to delete Value from Store") }