feat: add cli

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-08-24 15:40:44 +02:00
parent 61db3da695
commit 5e88ffdbc9
Signed by: kjuulh
GPG Key ID: D85D7535F18F35FA
4 changed files with 214 additions and 142 deletions

View File

@ -0,0 +1,68 @@
use crate::plan::{ClonedPlan, Plan};
use crate::project::ProjectPlan;
use crate::state::{self, ValidatedState};
pub struct Start {}
pub struct PrepareProject {
project: Option<ProjectPlan>,
}
pub struct PreparePlan {
project: Option<ProjectPlan>,
plan: Option<ClonedPlan>,
}
pub struct Cuddle<S = Start> {
pub state: S,
}
// Cuddle maintains the context for cuddle to use
// Stage 1 figure out which state to display
// Stage 2 prepare plan
// Stage 3 validate settings, build actions, prepare
impl Cuddle<Start> {
pub fn default() -> Self {
Self { state: Start {} }
}
pub async fn prepare_project(&self) -> anyhow::Result<Cuddle<PrepareProject>> {
let project = ProjectPlan::from_current_path().await?;
Ok(Cuddle {
state: PrepareProject { project },
})
}
}
impl Cuddle<PrepareProject> {
pub async fn prepare_plan(&self) -> anyhow::Result<Cuddle<PreparePlan>> {
let plan = if let Some(project) = &self.state.project {
Plan::new().clone_from_project(project).await?
} else {
None
};
Ok(Cuddle {
state: PreparePlan {
project: self.state.project.clone(),
plan,
},
})
}
}
impl Cuddle<PreparePlan> {
pub async fn build_state(&self) -> anyhow::Result<Cuddle<ValidatedState>> {
let state = if let Some(project) = &self.state.project {
let state = state::State::new();
let raw_state = state.build_state(project, &self.state.plan).await?;
state.validate_state(&raw_state).await?
} else {
ValidatedState::default()
};
Ok(Cuddle { state })
}
}

View File

@ -1,7 +1,7 @@
use plan::{ClonedPlan, Plan};
use project::ProjectPlan;
use state::ValidatedState;
use cuddle_state::Cuddle;
use state::{validated_project::Project, ValidatedState};
mod cuddle_state;
mod plan;
mod project;
mod schema_validator;
@ -12,7 +12,7 @@ async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
tracing_subscriber::fmt::init();
let _cuddle = Cuddle::default()
let cuddle = Cuddle::default()
.prepare_project()
.await?
.prepare_plan()
@ -20,71 +20,76 @@ async fn main() -> anyhow::Result<()> {
.build_state()
.await?;
Cli::new(cuddle).setup().await?.execute().await?;
Ok(())
}
struct Start {}
struct PrepareProject {
project: Option<ProjectPlan>,
pub struct Cli {
cli: clap::Command,
cuddle: Cuddle<ValidatedState>,
}
struct PreparePlan {
project: Option<ProjectPlan>,
plan: Option<ClonedPlan>,
impl Cli {
pub fn new(cuddle: Cuddle<ValidatedState>) -> Self {
let cli = clap::Command::new("cuddle").subcommand_required(true);
Self { cli, cuddle }
}
struct Cuddle<S = Start> {
state: S,
pub async fn setup(mut self) -> anyhow::Result<Self> {
let s = self
.add_default()
.await?
.add_project_commands()
.await?
.add_plan_commands()
.await?;
// TODO: Add global
// TODO: Add components
Ok(s)
}
// Cuddle maintains the context for cuddle to use
// Stage 1 figure out which state to display
// Stage 2 prepare plan
// Stage 3 validate settings, build actions, prepare
impl Cuddle {}
impl Cuddle<Start> {
pub fn default() -> Self {
Self { state: Start {} }
pub async fn execute(mut 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");
}
_ => {}
}
pub async fn prepare_project(&self) -> anyhow::Result<Cuddle<PrepareProject>> {
let project = ProjectPlan::from_current_path().await?;
Ok(Cuddle {
state: PrepareProject { project },
})
}
Ok(())
}
impl Cuddle<PrepareProject> {
pub async fn prepare_plan(&self) -> anyhow::Result<Cuddle<PreparePlan>> {
let plan = if let Some(project) = &self.state.project {
Plan::new().clone_from_project(project).await?
} else {
None
};
async fn add_default(mut self) -> anyhow::Result<Self> {
self.cli = self
.cli
.subcommand(clap::Command::new("do").alias("x"))
.subcommand(clap::Command::new("get"));
Ok(Cuddle {
state: PreparePlan {
project: self.state.project.clone(),
plan,
},
})
}
Ok(self)
}
impl Cuddle<PreparePlan> {
pub async fn build_state(&self) -> anyhow::Result<Cuddle<ValidatedState>> {
let state = if let Some(project) = &self.state.project {
let state = state::State::new();
let raw_state = state.build_state(project, &self.state.plan).await?;
async fn add_project_commands(mut self) -> anyhow::Result<Self> {
if let Some(project) = self.cuddle.state.project.as_ref() {
// Add project level commands
}
state.validate_state(&raw_state).await?
} else {
ValidatedState::default()
};
Ok(self)
}
Ok(Cuddle { state })
async fn add_plan_commands(mut self) -> anyhow::Result<Self> {
if let Some(plan) = self.cuddle.state.plan.as_ref() {
// Add plan level commands
}
Ok(self)
}
}

View File

@ -6,6 +6,8 @@ use crate::{
schema_validator::SchemaValidator,
};
pub mod validated_project;
pub struct State {}
impl State {
pub fn new() -> Self {
@ -51,87 +53,8 @@ pub struct RawState {
#[derive(Default)]
pub struct ValidatedState {
project: Option<Project>,
plan: Option<Plan>,
}
mod validated_project {
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use anyhow::anyhow;
use toml::Table;
use crate::project::CUDDLE_PROJECT_FILE;
pub struct Project {
value: Value,
pub root: PathBuf,
}
impl Project {
pub fn new(value: Value, root: &Path) -> Self {
Self {
value,
root: root.to_path_buf(),
}
}
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
let table: Table = toml::from_str(content)?;
let config = Config::default();
let project = table
.get("project")
.ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?;
let value: Value = project.into();
Ok(Self::new(value, root))
}
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
let cuddle_file = path.join(CUDDLE_PROJECT_FILE);
if !cuddle_file.exists() {
anyhow::bail!("no cuddle.toml project file found");
}
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?;
Self::from_file(&cuddle_project_file, path)
}
}
#[derive(Default)]
struct Config {
project: BTreeMap<String, Value>,
}
pub enum Value {
String(String),
Bool(bool),
Array(Vec<Value>),
Map(BTreeMap<String, Value>),
}
impl From<&toml::Value> for Value {
fn from(value: &toml::Value) -> Self {
match value {
toml::Value::String(s) => Self::String(s.clone()),
toml::Value::Integer(i) => Self::String(i.to_string()),
toml::Value::Float(f) => Self::String(f.to_string()),
toml::Value::Boolean(b) => Self::Bool(*b),
toml::Value::Datetime(dt) => Self::String(dt.to_string()),
toml::Value::Array(array) => Self::Array(array.iter().map(|i| i.into()).collect()),
toml::Value::Table(tbl) => {
Self::Map(tbl.iter().map(|(k, v)| (k.clone(), v.into())).collect())
}
}
}
}
pub project: Option<Project>,
pub plan: Option<Plan>,
}
pub struct Plan {}

View File

@ -0,0 +1,76 @@
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use anyhow::anyhow;
use toml::Table;
use crate::project::CUDDLE_PROJECT_FILE;
pub struct Project {
value: Value,
pub root: PathBuf,
}
impl Project {
pub fn new(value: Value, root: &Path) -> Self {
Self {
value,
root: root.to_path_buf(),
}
}
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
let table: Table = toml::from_str(content)?;
let config = Config::default();
let project = table
.get("project")
.ok_or(anyhow!("cuddle.toml doesn't provide a [project] table"))?;
let value: Value = project.into();
Ok(Self::new(value, root))
}
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
let cuddle_file = path.join(CUDDLE_PROJECT_FILE);
if !cuddle_file.exists() {
anyhow::bail!("no cuddle.toml project file found");
}
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?;
Self::from_file(&cuddle_project_file, path)
}
}
#[derive(Default)]
struct Config {
project: BTreeMap<String, Value>,
}
pub enum Value {
String(String),
Bool(bool),
Array(Vec<Value>),
Map(BTreeMap<String, Value>),
}
impl From<&toml::Value> for Value {
fn from(value: &toml::Value) -> Self {
match value {
toml::Value::String(s) => Self::String(s.clone()),
toml::Value::Integer(i) => Self::String(i.to_string()),
toml::Value::Float(f) => Self::String(f.to_string()),
toml::Value::Boolean(b) => Self::Bool(*b),
toml::Value::Datetime(dt) => Self::String(dt.to_string()),
toml::Value::Array(array) => Self::Array(array.iter().map(|i| i.into()).collect()),
toml::Value::Table(tbl) => {
Self::Map(tbl.iter().map(|(k, v)| (k.clone(), v.into())).collect())
}
}
}
}