diff --git a/crates/forest/src/cli.rs b/crates/forest/src/cli.rs index e5cf02e..ba5fba7 100644 --- a/crates/forest/src/cli.rs +++ b/crates/forest/src/cli.rs @@ -1,12 +1,13 @@ use std::{net::SocketAddr, path::PathBuf}; +use anyhow::Context as AnyContext; use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version}; use colored_json::ToColoredJson; use kdl::KdlDocument; use rusty_s3::{Bucket, Credentials, S3Action}; use crate::{ - model::{Context, ForestFile, Plan}, + model::{Context, ForestFile, Plan, Project, WorkspaceProject}, plan_reconciler::PlanReconciler, state::SharedState, }; @@ -86,11 +87,36 @@ pub async fn execute() -> anyhow::Result<()> { tracing::trace!("running as workspace"); // 1. For each member load the project - let output = serde_json::to_string_pretty(&workspace)?; + + let mut workspace_members = Vec::new(); + + for member in workspace.members { + let workspace_member_path = project_path.join(&member.path); + + let project_file_path = workspace_member_path.join("forest.kdl"); + if !project_file_path.exists() { + anyhow::bail!( + "no 'forest.kdl' file was found at: {}", + workspace_member_path.display().to_string() + ); + } + + let project_file = tokio::fs::read_to_string(&project_file_path).await?; + let doc: KdlDocument = project_file.parse()?; + let project: WorkspaceProject = doc.try_into().context(format!( + "workspace member: {} failed to parse", + &member.path + ))?; + + workspace_members.push(project); + } + + let output = serde_json::to_string_pretty(&workspace_members)?; println!("{}", output.to_colored_json_auto().unwrap_or(output)); // TODO: 1a (optional). Resolve dependencies // 2. Reconcile plans + // 3. Provide context and aggregated commands for projects } ForestFile::Project(project) => { diff --git a/crates/forest/src/model.rs b/crates/forest/src/model.rs index dc287ee..b7a6a3e 100644 --- a/crates/forest/src/model.rs +++ b/crates/forest/src/model.rs @@ -13,7 +13,9 @@ pub struct Context { #[derive(Debug, Clone, Serialize)] pub struct Plan { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub templates: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub scripts: Option, } @@ -164,6 +166,12 @@ pub struct Global { items: BTreeMap, } +impl Global { + fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + impl From<&Global> for minijinja::Value { fn from(value: &Global) -> Self { Self::from_serialize(&value.items) @@ -312,10 +320,16 @@ impl TryFrom<&KdlNode> for Scripts { #[derive(Debug, Clone, Serialize)] pub struct Project { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub plan: Option, + + #[serde(skip_serializing_if = "Global::is_empty")] pub global: Global, + #[serde(skip_serializing_if = "Option::is_none")] pub templates: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub scripts: Option, } @@ -375,7 +389,7 @@ impl TryFrom for Project { #[derive(Debug, Clone, Serialize)] pub struct WorkspaceMember { - pub name: String, + pub path: String, } impl TryFrom<&kdl::KdlNode> for WorkspaceMember { @@ -383,7 +397,7 @@ impl TryFrom<&kdl::KdlNode> for WorkspaceMember { fn try_from(value: &kdl::KdlNode) -> Result { Ok(Self { - name: value + path: value .entries() .first() .ok_or(anyhow::anyhow!( @@ -399,7 +413,7 @@ impl TryFrom<&kdl::KdlNode> for WorkspaceMember { #[derive(Debug, Clone, Serialize)] pub struct Workspace { - members: Vec, + pub members: Vec, } impl TryFrom for Workspace { @@ -453,3 +467,30 @@ impl TryFrom for ForestFile { anyhow::bail!("a forest.kdl file must be either a project, workspace or plan") } } + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] +pub enum WorkspaceProject { + Plan(Plan), + Project(Project), +} + +impl TryFrom for WorkspaceProject { + type Error = anyhow::Error; + + fn try_from(value: KdlDocument) -> Result { + if value.get("plan").is_some() && value.get("project").is_some() { + anyhow::bail!("a forest.kdl file cannot contain both a plan and project") + } + + if value.get("project").is_some() { + return Ok(Self::Project(value.try_into()?)); + } + + if value.get("plan").is_some() { + return Ok(Self::Plan(value.try_into()?)); + } + + anyhow::bail!("a forest.kdl file must be either a project, workspace or plan") + } +} diff --git a/examples/workspace/forest.kdl b/examples/workspace/forest.kdl index 1441b1b..00bc625 100644 --- a/examples/workspace/forest.kdl +++ b/examples/workspace/forest.kdl @@ -1,9 +1,9 @@ workspace { members { - member "./projects/a" - member "./projects/b" - member "./plan/a" - member "./plan/b" - member "./components/*" + member "projects/a" + member "projects/b" + member "plan/a" + member "plan/b" + // member "components/*" } } diff --git a/examples/workspace/plan/a/forest.kdl b/examples/workspace/plan/a/forest.kdl index e69de29..cfca6f9 100644 --- a/examples/workspace/plan/a/forest.kdl +++ b/examples/workspace/plan/a/forest.kdl @@ -0,0 +1,3 @@ +plan { + name a +} diff --git a/examples/workspace/plan/b/forest.kdl b/examples/workspace/plan/b/forest.kdl index e69de29..2870836 100644 --- a/examples/workspace/plan/b/forest.kdl +++ b/examples/workspace/plan/b/forest.kdl @@ -0,0 +1,3 @@ +plan { + name b +} diff --git a/examples/workspace/projects/a/forest.kdl b/examples/workspace/projects/a/forest.kdl index e69de29..52e1cf8 100644 --- a/examples/workspace/projects/a/forest.kdl +++ b/examples/workspace/projects/a/forest.kdl @@ -0,0 +1,3 @@ +project { + name a +} diff --git a/examples/workspace/projects/b/forest.kdl b/examples/workspace/projects/b/forest.kdl index e69de29..005227c 100644 --- a/examples/workspace/projects/b/forest.kdl +++ b/examples/workspace/projects/b/forest.kdl @@ -0,0 +1,3 @@ +project { + name b +}