From 02dd805db4e8f294ba14c994baa1fd314541c2a7 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sun, 25 Aug 2024 22:13:50 +0200 Subject: [PATCH] feat: add command get for doing queries Signed-off-by: kjuulh --- Cargo.lock | 25 ++++ crates/cuddle/Cargo.toml | 1 + crates/cuddle/examples/schema/plan/schema.ncl | 2 +- crates/cuddle/src/cli.rs | 88 +++++++++++++ crates/cuddle/src/cli/get_command.rs | 124 ++++++++++++++++++ crates/cuddle/src/main.rs | 68 +--------- crates/cuddle/src/state/validated_project.rs | 5 + 7 files changed, 246 insertions(+), 67 deletions(-) create mode 100644 crates/cuddle/src/cli.rs create mode 100644 crates/cuddle/src/cli/get_command.rs diff --git a/Cargo.lock b/Cargo.lock index b269ee4..56f820d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,7 @@ dependencies = [ "dotenv", "fs_extra", "serde", + "serde_json", "tokio", "toml", "tracing", @@ -251,6 +252,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.5.0" @@ -399,6 +406,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -425,6 +438,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.7" diff --git a/crates/cuddle/Cargo.toml b/crates/cuddle/Cargo.toml index 26f0826..0fc8a20 100644 --- a/crates/cuddle/Cargo.toml +++ b/crates/cuddle/Cargo.toml @@ -16,3 +16,4 @@ serde = { version = "1.0.197", features = ["derive"] } uuid = { version = "1.7.0", features = ["v4"] } toml = "0.8.19" fs_extra = "1.3.0" +serde_json = "1.0.127" diff --git a/crates/cuddle/examples/schema/plan/schema.ncl b/crates/cuddle/examples/schema/plan/schema.ncl index d670119..597c893 100644 --- a/crates/cuddle/examples/schema/plan/schema.ncl +++ b/crates/cuddle/examples/schema/plan/schema.ncl @@ -1,5 +1,5 @@ { ProjectSchema = { - name | String + name | String,.. } } diff --git a/crates/cuddle/src/cli.rs b/crates/cuddle/src/cli.rs new file mode 100644 index 0000000..33a3dea --- /dev/null +++ b/crates/cuddle/src/cli.rs @@ -0,0 +1,88 @@ +use std::{borrow::BorrowMut, io::Write}; + +use anyhow::anyhow; +use get_command::GetCommand; + +use crate::{cuddle_state::Cuddle, state::ValidatedState}; + +mod get_command; + +pub struct Cli { + cli: clap::Command, + cuddle: Cuddle, +} + +impl Cli { + pub fn new(cuddle: Cuddle) -> Self { + let cli = clap::Command::new("cuddle").subcommand_required(true); + + Self { cli, cuddle } + } + + pub async fn setup(mut self) -> anyhow::Result { + let commands = self.get_commands().await?; + + self.cli = self.cli.subcommands(commands); + + // TODO: Add global + // TODO: Add components + + Ok(self) + } + + async fn get_commands(&self) -> anyhow::Result> { + Ok(vec![ + clap::Command::new("do").subcommand_required(true), + clap::Command::new("get") + .about(GetCommand::description()) + .arg( + clap::Arg::new("query") + .required(true) + .help("query is how values are extracted, '.project.name' etc"), + ), + ]) + } + + pub async fn execute(self) -> anyhow::Result<()> { + match self + .cli + .get_matches_from(std::env::args()) + .subcommand() + .ok_or(anyhow::anyhow!("failed to find subcommand"))? + { + ("do", _args) => { + tracing::debug!("executing do"); + } + ("get", args) => { + let query = args + .get_one::("query") + .ok_or(anyhow!("query is required"))?; + + let res = GetCommand::new(self.cuddle).execute(query).await?; + + std::io::stdout().write_all(res.as_bytes())?; + std::io::stdout().write_all("\n".as_bytes())?; + } + _ => {} + } + + Ok(()) + } + + async fn add_project_commands(&self) -> anyhow::Result> { + if let Some(_project) = self.cuddle.state.project.as_ref() { + // Add project level commands + return Ok(vec![]); + } + + Ok(Vec::new()) + } + + async fn add_plan_commands(self) -> anyhow::Result { + if let Some(_plan) = self.cuddle.state.plan.as_ref() { + // Add plan level commands + } + + Ok(self) + } +} diff --git a/crates/cuddle/src/cli/get_command.rs b/crates/cuddle/src/cli/get_command.rs new file mode 100644 index 0000000..91d11de --- /dev/null +++ b/crates/cuddle/src/cli/get_command.rs @@ -0,0 +1,124 @@ +use crate::{ + cuddle_state::Cuddle, + state::{ + validated_project::{Project, Value}, + ValidatedState, + }, +}; + +pub struct GetCommand { + query_engine: ProjectQueryEngine, +} + +impl GetCommand { + pub fn new(cuddle: Cuddle) -> Self { + Self { + query_engine: ProjectQueryEngine::new( + &cuddle + .state + .project + .expect("we should always have a project if get command is available"), + ), + } + } + + pub async fn execute(&self, query: &str) -> anyhow::Result { + let res = self + .query_engine + .query(query)? + .ok_or(anyhow::anyhow!("query was not found in project"))?; + + match res { + Value::String(s) => Ok(s), + Value::Bool(b) => Ok(b.to_string()), + Value::Array(value) => { + let val = serde_json::to_string_pretty(&value)?; + Ok(val) + } + Value::Map(value) => { + let val = serde_json::to_string_pretty(&value)?; + Ok(val) + } + } + } + + pub fn description() -> String { + "get returns a given variable from the project given a key, following a jq like schema (.project.name, etc.)" + .into() + } +} + +pub struct ProjectQueryEngine { + project: Project, +} + +impl ProjectQueryEngine { + pub fn new(project: &Project) -> Self { + Self { + project: project.clone(), + } + } + + pub fn query(&self, query: &str) -> anyhow::Result> { + let parts = query + .split('.') + .filter(|i| !i.is_empty()) + .collect::>(); + + Ok(self.traverse(&parts, &self.project.value)) + } + + fn traverse(&self, query: &[&str], value: &Value) -> Option { + match query.split_first() { + Some((key, rest)) => match value { + Value::Map(items) => { + let item = items.get(*key)?; + + self.traverse(rest, item) + } + _ => { + tracing::warn!( + "key: {} doesn't have a corresponding value: {:?}", + key, + value + ); + None + } + }, + None => Some(value.clone()), + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[tokio::test] + async fn test_can_query_item() -> anyhow::Result<()> { + let project = ProjectQueryEngine::new(&Project { + value: Value::Map( + [( + String::from("project"), + Value::Map( + [( + String::from("name"), + Value::String(String::from("something")), + )] + .into(), + ), + )] + .into(), + ), + root: PathBuf::new(), + }); + + let res = project.query(".project.name")?; + + assert_eq!(Some(Value::String("something".into())), res); + + Ok(()) + } +} diff --git a/crates/cuddle/src/main.rs b/crates/cuddle/src/main.rs index 70adf89..a591d43 100644 --- a/crates/cuddle/src/main.rs +++ b/crates/cuddle/src/main.rs @@ -1,6 +1,7 @@ +use cli::Cli; use cuddle_state::Cuddle; -use state::ValidatedState; +mod cli; mod cuddle_state; mod plan; mod project; @@ -24,68 +25,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -pub struct Cli { - cli: clap::Command, - cuddle: Cuddle, -} - -impl Cli { - pub fn new(cuddle: Cuddle) -> Self { - let cli = clap::Command::new("cuddle").subcommand_required(true); - - Self { cli, cuddle } - } - - pub async fn setup(mut self) -> anyhow::Result { - let commands = self.get_commands().await?; - - self.cli = self.cli.subcommands(commands); - - // TODO: Add global - // TODO: Add components - - Ok(self) - } - - pub async fn execute(self) -> anyhow::Result<()> { - match self - .cli - .get_matches_from(std::env::args()) - .subcommand() - .ok_or(anyhow::anyhow!("failed to find subcommand"))? - { - ("do", _args) => { - tracing::debug!("executing do"); - } - ("get", _args) => {} - _ => {} - } - - Ok(()) - } - - async fn get_commands(&self) -> anyhow::Result> { - Ok(vec![ - clap::Command::new("do").subcommand_required(true), - clap::Command::new("get"), - ]) - } - - async fn add_project_commands(&self) -> anyhow::Result> { - if let Some(_project) = self.cuddle.state.project.as_ref() { - // Add project level commands - return Ok(vec![]); - } - - Ok(Vec::new()) - } - - async fn add_plan_commands(self) -> anyhow::Result { - if let Some(_plan) = self.cuddle.state.plan.as_ref() { - // Add plan level commands - } - - Ok(self) - } -} diff --git a/crates/cuddle/src/state/validated_project.rs b/crates/cuddle/src/state/validated_project.rs index fa8892f..c057bbb 100644 --- a/crates/cuddle/src/state/validated_project.rs +++ b/crates/cuddle/src/state/validated_project.rs @@ -4,10 +4,12 @@ use std::{ }; use anyhow::anyhow; +use serde::Serialize; use toml::Table; use crate::project::CUDDLE_PROJECT_FILE; +#[derive(Clone)] pub struct Project { pub value: Value, pub root: PathBuf, @@ -29,6 +31,7 @@ impl Project { .ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?; let value: Value = project.into(); + let value = Value::Map([("project".to_string(), value)].into()); Ok(Self::new(value, root)) } @@ -46,6 +49,8 @@ impl Project { } } +#[derive(Clone, PartialEq, Eq, Debug, Serialize)] +#[serde(untagged)] pub enum Value { String(String), Bool(bool),