diff --git a/Cargo.lock b/Cargo.lock index 7d44953f0..f865a723e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,54 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -115,28 +157,72 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", ] [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cli-table" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbb116d9e2c4be7011360d0c0bee565712c11e969c9609b25b619366dc379d" +dependencies = [ + "cli-table-derive", + "csv", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cli-table-derive" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "2af3bfb9da627b0a6c467624fb7963921433774ed435493b5c08a3053e829ad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" @@ -233,6 +319,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -308,6 +415,8 @@ name = "gosub-engine" version = "0.1.0" dependencies = [ "anyhow", + "clap", + "cli-table", "colored", "criterion", "derive_more", @@ -321,6 +430,7 @@ dependencies = [ "serde_json", "sqlite", "test-case", + "textwrap", "thiserror", "typed-arena", "ureq", @@ -335,6 +445,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -803,6 +919,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "spin" version = "0.5.2" @@ -839,6 +961,12 @@ dependencies = [ "sqlite3-src", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -861,6 +989,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-case" version = "3.2.1" @@ -896,6 +1033,17 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -959,6 +1107,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.22" @@ -968,6 +1122,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "untrusted" version = "0.7.1" @@ -1001,6 +1161,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 3f45ca04a..74b1745e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,9 @@ nom = "7.1.3" nom_locate = "4.2.0" sqlite = "0.32.0" wildmatch = "2.1.1" +clap = { version = "4.4.7", features = ["derive"] } +cli-table = "0.4.7" +textwrap = "0.16.0" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/src/bin/config-store.rs b/src/bin/config-store.rs new file mode 100644 index 000000000..97b9b4b70 --- /dev/null +++ b/src/bin/config-store.rs @@ -0,0 +1,115 @@ +use clap::{Parser, Subcommand}; +use derive_more::Display; +use gosub_engine::config::{ConfigStore, StorageAdapter}; +use gosub_engine::config::settings::Setting; +use gosub_engine::config::storage::json_storage::JsonStorageAdapter; +use gosub_engine::config::storage::sqlite_storage::SqliteStorageAdapter; + +#[derive(Debug, Parser)] +#[clap(name = "Config-Store", version = "0.1.0", author = "Gosub")] +struct Cli { + #[clap(flatten)] + global_opts: GlobalOpts, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + #[clap(arg_required_else_help = true, about = "View a setting")] + View { + #[clap(required = true, short = 'k', long = "key")] + key: String, + }, + #[clap(about = "List all settings")] + List, + #[clap(arg_required_else_help = true, about = "Set a setting")] + Set { + #[clap(required = true, short = 'k', long = "key")] + key: String, + #[clap(required = true, short = 'v', long = "value")] + value: String, + }, + #[clap(arg_required_else_help = true, about = "Search for a setting")] + Search { + #[clap(required = true, short = 'k', long = "key")] + key: String, + }, +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum, Display)] +enum Engine { + Sqlite, + Json, +} + +impl std::str::FromStr for Engine { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "sqlite" => Ok(Engine::Sqlite), + "json" => Ok(Engine::Json), + _ => Err(()), + } + } +} + + +#[derive(Debug, Parser)] +struct GlobalOpts { + #[clap(short = 'e', long = "engine", global = true, default_value = "sqlite")] + engine: Engine, + #[clap(short = 'p', long = "path", global = true, default_value = "settings.db")] + path: String, +} + +fn main() { + let args = Cli::parse(); + + let storage_box: Box; + match args.global_opts.engine { + Engine::Sqlite => { + storage_box = Box::new(SqliteStorageAdapter::new(args.global_opts.path.as_str())); + } + Engine::Json => { + storage_box = Box::new(JsonStorageAdapter::new(args.global_opts.path.as_str())); + } + } + + let mut store = ConfigStore::new(storage_box, true); + + + match args.command { + Commands::View { key } => { + if !store.has(key.as_str()) { + println!("Key not found"); + return; + } + + let info = store.get_info(key.as_str()).unwrap(); + let value = store.get(key.as_str(), None); + + println!("Key : {}", key); + println!("Current Value : {}", value.to_string()); + println!("Default Value : {}", info.default.to_string()); + println!("Description : {}", info.description); + } + Commands::List => { + for key in store.find("*".into()) { + let value = store.get(key.as_str(), None); + println!("{:40}: {}", key, value.to_string()); + } + } + Commands::Set { key, value } => { + store.set(key.as_str(), Setting::from_string(value.as_str()).expect("incorrect value")); + } + Commands::Search { key } => { + for key in store.find(key.as_str().into()) { + let value = store.get(key.as_str(), None); + println!("{:40}: {}", key, value.to_string()); + } + } + } +} diff --git a/src/config.rs b/src/config.rs index 2b75b7a94..900681763 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,7 +18,7 @@ struct JsonEntry { description: String, } -trait StorageAdapter { +pub trait StorageAdapter { /// Retrieves a setting from the storage fn get_setting(&self, key: &str) -> Option; /// Stores a given setting to the storage @@ -41,7 +41,7 @@ pub struct ConfigStore { impl ConfigStore { /// Creates a new store with the given storage adapter and preloads the store if needed - fn new(storage: Box, preload: bool) -> Self { + pub fn new(storage: Box, preload: bool) -> Self { let mut store = ConfigStore { settings: HashMap::new(), settings_info: HashMap::new(), @@ -63,8 +63,12 @@ impl ConfigStore { store } + pub fn has(&self, key: &str) -> bool { + self.settings.contains_key(key) + } + /// Returns a list of keys that mathces the given search string (can use *) - fn find_setting_keys(&self, search: &str) -> Vec { + pub fn find(&self, search: &str) -> Vec { let search = WildMatch::new(search); let mut keys = Vec::new(); @@ -78,9 +82,16 @@ impl ConfigStore { keys } + pub fn get_info(&self, key: &str) -> Option { + match self.settings_info.get(key) { + Some(info) => Some(info.clone()), + None => None + } + } + /// Returns the setting with the given key /// If the setting does not exist, it will try and load it from the storage adapter - fn get(&mut self, key: &str, default: Option) -> Setting { + pub fn get(&mut self, key: &str, default: Option) -> Setting { if let Some(setting) = self.settings.get(key) { return setting.clone(); } @@ -99,7 +110,7 @@ impl ConfigStore { default.unwrap() } - fn set(&mut self, key: &str, value: Setting) { + pub fn set(&mut self, key: &str, value: Setting) { self.settings.insert(key.to_string(), value.clone()); self.storage.set_setting(key, value); } @@ -137,12 +148,12 @@ impl ConfigStore { #[cfg(test)] mod test { - use crate::config::storage::memory_storage::MemoryStorageAdapter; + use crate::config::storage::sqlite_storage::SqliteStorageAdapter; use super::*; #[test] fn test_config_store() { - let mut store = ConfigStore::new(Box::new(MemoryStorageAdapter::new()), true); + let mut store = ConfigStore::new(Box::new(SqliteStorageAdapter::new("settings.db")), true); let setting = store.get("dns.local_resolver.enabled", None); assert_eq!(setting, Setting::Bool(false)); diff --git a/src/config/settings.rs b/src/config/settings.rs index 21f1518d1..634cab571 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -18,7 +18,7 @@ impl Setting { // m:foo,bar,baz /// Converts a string to a setting or None when the string is invalid - pub(crate) fn from_string(p0: &str) -> Option { + pub fn from_string(p0: &str) -> Option { let mut parts = p0.splitn(2, ':'); let p1 = parts.next().unwrap(); let p2 = parts.next().unwrap(); @@ -55,7 +55,7 @@ impl Setting { } /// Converts a setting to a string representation - pub(crate) fn to_string(&self) -> String { + pub fn to_string(&self) -> String { match self { Setting::SInt(value) => format!("i:{}", value), Setting::UInt(value) => format!("u:{}", value), diff --git a/src/config/storage/json_storage.rs b/src/config/storage/json_storage.rs index a0bbe8dd9..fe394742f 100644 --- a/src/config/storage/json_storage.rs +++ b/src/config/storage/json_storage.rs @@ -7,9 +7,9 @@ pub struct JsonStorageAdapter { } impl JsonStorageAdapter { - fn new(path: String) -> Self { + pub fn new(path: &str) -> Self { JsonStorageAdapter { - path + path: path.to_string() } } } diff --git a/src/config/storage/sqlite_storage.rs b/src/config/storage/sqlite_storage.rs index 17029b1c0..2d4c042fb 100644 --- a/src/config/storage/sqlite_storage.rs +++ b/src/config/storage/sqlite_storage.rs @@ -2,29 +2,28 @@ use std::collections::HashMap; use crate::config::settings::Setting; use crate::config::StorageAdapter; -struct SqlStorageAdapter { +pub struct SqliteStorageAdapter { connection: sqlite::Connection } -impl SqlStorageAdapter { - fn new(path: String) -> Self { +impl SqliteStorageAdapter { + pub fn new(path: &str) -> Self { let conn = sqlite::open(path).expect("cannot open db file"); let query = "CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY, key TEXT NOT NULL, - value TEXT NOT NULL, + value TEXT NOT NULL )"; conn.execute(query).unwrap(); - SqlStorageAdapter { + SqliteStorageAdapter { connection: conn } - } } -impl StorageAdapter for SqlStorageAdapter +impl StorageAdapter for SqliteStorageAdapter { fn get_setting(&self, key: &str) -> Option { let query = "SELECT * FROM settings WHERE key = :key"; @@ -44,6 +43,16 @@ impl StorageAdapter for SqlStorageAdapter } fn get_all_settings(&self) -> HashMap { - todo!() + let query = "SELECT * FROM settings"; + let mut statement = self.connection.prepare(query).unwrap(); + + let mut settings = HashMap::new(); + while let sqlite::State::Row = statement.next().unwrap() { + let key = statement.read::(1).unwrap(); + let value = statement.read::(2).unwrap(); + settings.insert(key, Setting::from_string(value.as_str()).unwrap()); + } + + settings } }