feat: can load project files

This commit is contained in:
Kasper Juul Hermansen 2025-03-23 20:36:40 +01:00
parent dca625af31
commit 28a1d09974
Signed by: kjuulh
SSH Key Fingerprint: SHA256:RjXh0p7U6opxnfd3ga/Y9TCo18FYlHFdSpRIV72S/QM
7 changed files with 89 additions and 10 deletions

View File

@ -1,12 +1,13 @@
use std::{net::SocketAddr, path::PathBuf}; use std::{net::SocketAddr, path::PathBuf};
use anyhow::Context as AnyContext;
use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version}; use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version};
use colored_json::ToColoredJson; use colored_json::ToColoredJson;
use kdl::KdlDocument; use kdl::KdlDocument;
use rusty_s3::{Bucket, Credentials, S3Action}; use rusty_s3::{Bucket, Credentials, S3Action};
use crate::{ use crate::{
model::{Context, ForestFile, Plan}, model::{Context, ForestFile, Plan, Project, WorkspaceProject},
plan_reconciler::PlanReconciler, plan_reconciler::PlanReconciler,
state::SharedState, state::SharedState,
}; };
@ -86,11 +87,36 @@ pub async fn execute() -> anyhow::Result<()> {
tracing::trace!("running as workspace"); tracing::trace!("running as workspace");
// 1. For each member load the project // 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)); println!("{}", output.to_colored_json_auto().unwrap_or(output));
// TODO: 1a (optional). Resolve dependencies // TODO: 1a (optional). Resolve dependencies
// 2. Reconcile plans // 2. Reconcile plans
// 3. Provide context and aggregated commands for projects // 3. Provide context and aggregated commands for projects
} }
ForestFile::Project(project) => { ForestFile::Project(project) => {

View File

@ -13,7 +13,9 @@ pub struct Context {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Plan { pub struct Plan {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Templates>, pub templates: Option<Templates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<Scripts>, pub scripts: Option<Scripts>,
} }
@ -164,6 +166,12 @@ pub struct Global {
items: BTreeMap<String, GlobalVariable>, items: BTreeMap<String, GlobalVariable>,
} }
impl Global {
fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl From<&Global> for minijinja::Value { impl From<&Global> for minijinja::Value {
fn from(value: &Global) -> Self { fn from(value: &Global) -> Self {
Self::from_serialize(&value.items) Self::from_serialize(&value.items)
@ -312,10 +320,16 @@ impl TryFrom<&KdlNode> for Scripts {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<ProjectPlan>, pub plan: Option<ProjectPlan>,
#[serde(skip_serializing_if = "Global::is_empty")]
pub global: Global, pub global: Global,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Templates>, pub templates: Option<Templates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<Scripts>, pub scripts: Option<Scripts>,
} }
@ -375,7 +389,7 @@ impl TryFrom<KdlDocument> for Project {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct WorkspaceMember { pub struct WorkspaceMember {
pub name: String, pub path: String,
} }
impl TryFrom<&kdl::KdlNode> for WorkspaceMember { impl TryFrom<&kdl::KdlNode> for WorkspaceMember {
@ -383,7 +397,7 @@ impl TryFrom<&kdl::KdlNode> for WorkspaceMember {
fn try_from(value: &kdl::KdlNode) -> Result<Self, Self::Error> { fn try_from(value: &kdl::KdlNode) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
name: value path: value
.entries() .entries()
.first() .first()
.ok_or(anyhow::anyhow!( .ok_or(anyhow::anyhow!(
@ -399,7 +413,7 @@ impl TryFrom<&kdl::KdlNode> for WorkspaceMember {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Workspace { pub struct Workspace {
members: Vec<WorkspaceMember>, pub members: Vec<WorkspaceMember>,
} }
impl TryFrom<KdlDocument> for Workspace { impl TryFrom<KdlDocument> for Workspace {
@ -453,3 +467,30 @@ impl TryFrom<KdlDocument> for ForestFile {
anyhow::bail!("a forest.kdl file must be either a project, workspace or plan") 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<KdlDocument> for WorkspaceProject {
type Error = anyhow::Error;
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
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")
}
}

View File

@ -1,9 +1,9 @@
workspace { workspace {
members { members {
member "./projects/a" member "projects/a"
member "./projects/b" member "projects/b"
member "./plan/a" member "plan/a"
member "./plan/b" member "plan/b"
member "./components/*" // member "components/*"
} }
} }

View File

@ -0,0 +1,3 @@
plan {
name a
}

View File

@ -0,0 +1,3 @@
plan {
name b
}

View File

@ -0,0 +1,3 @@
project {
name a
}

View File

@ -0,0 +1,3 @@
project {
name b
}