feat: add command get for doing queries
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
23d68caf71
commit
02dd805db4
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
ProjectSchema = {
|
ProjectSchema = {
|
||||||
name | String
|
name | String,..
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
crates/cuddle/src/cli.rs
Normal file
88
crates/cuddle/src/cli.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
124
crates/cuddle/src/cli/get_command.rs
Normal file
124
crates/cuddle/src/cli/get_command.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user