feat: add command get for doing queries

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-08-25 22:13:50 +02:00
parent 23d68caf71
commit 02dd805db4
Signed by: kjuulh
GPG Key ID: D85D7535F18F35FA
7 changed files with 246 additions and 67 deletions

25
Cargo.lock generated
View File

@ -175,6 +175,7 @@ dependencies = [
"dotenv", "dotenv",
"fs_extra", "fs_extra",
"serde", "serde",
"serde_json",
"tokio", "tokio",
"toml", "toml",
"tracing", "tracing",
@ -251,6 +252,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -399,6 +406,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -425,6 +438,18 @@ dependencies = [
"syn", "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]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.7" version = "0.6.7"

View File

@ -16,3 +16,4 @@ serde = { version = "1.0.197", features = ["derive"] }
uuid = { version = "1.7.0", features = ["v4"] } uuid = { version = "1.7.0", features = ["v4"] }
toml = "0.8.19" toml = "0.8.19"
fs_extra = "1.3.0" fs_extra = "1.3.0"
serde_json = "1.0.127"

View File

@ -1,5 +1,5 @@
{ {
ProjectSchema = { ProjectSchema = {
name | String name | String,..
} }
} }

88
crates/cuddle/src/cli.rs Normal file
View File

@ -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<ValidatedState>,
}
impl Cli {
pub fn new(cuddle: Cuddle<ValidatedState>) -> Self {
let cli = clap::Command::new("cuddle").subcommand_required(true);
Self { cli, cuddle }
}
pub async fn setup(mut self) -> anyhow::Result<Self> {
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<Vec<clap::Command>> {
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::<String>("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<Vec<clap::Command>> {
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<Self> {
if let Some(_plan) = self.cuddle.state.plan.as_ref() {
// Add plan level commands
}
Ok(self)
}
}

View File

@ -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<ValidatedState>) -> 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<String> {
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<Option<Value>> {
let parts = query
.split('.')
.filter(|i| !i.is_empty())
.collect::<Vec<&str>>();
Ok(self.traverse(&parts, &self.project.value))
}
fn traverse(&self, query: &[&str], value: &Value) -> Option<Value> {
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(())
}
}

View File

@ -1,6 +1,7 @@
use cli::Cli;
use cuddle_state::Cuddle; use cuddle_state::Cuddle;
use state::ValidatedState;
mod cli;
mod cuddle_state; mod cuddle_state;
mod plan; mod plan;
mod project; mod project;
@ -24,68 +25,3 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub struct Cli {
cli: clap::Command,
cuddle: Cuddle<ValidatedState>,
}
impl Cli {
pub fn new(cuddle: Cuddle<ValidatedState>) -> Self {
let cli = clap::Command::new("cuddle").subcommand_required(true);
Self { cli, cuddle }
}
pub async fn setup(mut self) -> anyhow::Result<Self> {
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<Vec<clap::Command>> {
Ok(vec![
clap::Command::new("do").subcommand_required(true),
clap::Command::new("get"),
])
}
async fn add_project_commands(&self) -> anyhow::Result<Vec<clap::Command>> {
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<Self> {
if let Some(_plan) = self.cuddle.state.plan.as_ref() {
// Add plan level commands
}
Ok(self)
}
}

View File

@ -4,10 +4,12 @@ use std::{
}; };
use anyhow::anyhow; use anyhow::anyhow;
use serde::Serialize;
use toml::Table; use toml::Table;
use crate::project::CUDDLE_PROJECT_FILE; use crate::project::CUDDLE_PROJECT_FILE;
#[derive(Clone)]
pub struct Project { pub struct Project {
pub value: Value, pub value: Value,
pub root: PathBuf, pub root: PathBuf,
@ -29,6 +31,7 @@ impl Project {
.ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?; .ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?;
let value: Value = project.into(); let value: Value = project.into();
let value = Value::Map([("project".to_string(), value)].into());
Ok(Self::new(value, root)) Ok(Self::new(value, root))
} }
@ -46,6 +49,8 @@ impl Project {
} }
} }
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
#[serde(untagged)]
pub enum Value { pub enum Value {
String(String), String(String),
Bool(bool), Bool(bool),