feat: add command get for doing queries

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

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
{
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 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<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 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),