use std::path::{Path, PathBuf}; use fs_extra::dir::CopyOptions; use serde::Deserialize; use crate::project::{self, ProjectPlan}; pub const CUDDLE_PLAN_FOLDER: &str = "plan"; pub const CUDDLE_PROJECT_WORKSPACE: &str = ".cuddle"; pub const CUDDLE_PLAN_FILE: &str = "cuddle.plan.toml"; pub trait PlanPathExt { fn plan_path(&self) -> PathBuf; } impl PlanPathExt for project::ProjectPlan { fn plan_path(&self) -> PathBuf { self.root .join(CUDDLE_PROJECT_WORKSPACE) .join(CUDDLE_PLAN_FOLDER) } } pub struct RawPlan { pub config: RawPlanConfig, pub root: PathBuf, } impl RawPlan { pub fn new(config: RawPlanConfig, root: &Path) -> Self { Self { config, root: root.to_path_buf(), } } pub fn from_file(content: &str, root: &Path) -> anyhow::Result { let config: RawPlanConfig = toml::from_str(content)?; Ok(Self::new(config, root)) } pub async fn from_path(path: &Path) -> anyhow::Result { let cuddle_file = path.join(CUDDLE_PLAN_FILE); tracing::trace!( path = cuddle_file.display().to_string(), "searching for cuddle.toml project file" ); if !cuddle_file.exists() { anyhow::bail!("no cuddle.toml project file found"); } let cuddle_plan_file = tokio::fs::read_to_string(cuddle_file).await?; Self::from_file(&cuddle_plan_file, path) } } #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct RawPlanConfig { pub plan: RawPlanConfigSection, } #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct RawPlanConfigSection { pub schema: Option, } #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(untagged)] pub enum RawPlanSchema { Nickel { nickel: PathBuf }, } pub struct Plan {} impl Plan { pub fn new() -> Self { Self {} } pub async fn clone_from_project( &self, project: &ProjectPlan, ) -> anyhow::Result> { if !project.plan_path().exists() { if project.has_plan() { self.prepare_plan(project).await?; } match project.get_plan() { project::Plan::None => Ok(None), project::Plan::Git(url) => Ok(Some(self.git_plan(project, url).await?)), project::Plan::Folder(folder) => { Ok(Some(self.folder_plan(project, &folder).await?)) } } } else { match project.get_plan() { project::Plan::Folder(folder) => { self.clean_plan(project).await?; self.prepare_plan(project).await?; Ok(Some(self.folder_plan(project, &folder).await?)) } project::Plan::Git(_git) => Ok(Some(ClonedPlan {})), project::Plan::None => Ok(None), } } } async fn prepare_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> { tracing::trace!("preparing workspace"); tokio::fs::create_dir_all(project.plan_path()).await?; Ok(()) } async fn clean_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> { tracing::trace!("clean plan"); tokio::fs::remove_dir_all(project.plan_path()).await?; Ok(()) } async fn git_plan(&self, project: &ProjectPlan, url: String) -> anyhow::Result { let mut cmd = tokio::process::Command::new("git"); cmd.args(["clone", &url, &project.plan_path().display().to_string()]); tracing::debug!(url = url, "cloning git plan"); let output = cmd.output().await?; if !output.status.success() { anyhow::bail!( "failed to clone: {}, output: {} {}", url, std::str::from_utf8(&output.stdout)?, std::str::from_utf8(&output.stderr)?, ) } Ok(ClonedPlan {}) } async fn folder_plan(&self, project: &ProjectPlan, path: &Path) -> anyhow::Result { tracing::trace!( src = path.display().to_string(), dest = project.plan_path().display().to_string(), "copying src into plan dest" ); let mut items_stream = tokio::fs::read_dir(path).await?; let mut items = Vec::new(); while let Some(item) = items_stream.next_entry().await? { items.push(item.path()); } fs_extra::copy_items( &items, project.plan_path(), &CopyOptions::default() .overwrite(true) .depth(0) .copy_inside(false), )?; Ok(ClonedPlan {}) } } pub struct ClonedPlan {} #[cfg(test)] mod tests { use super::*; #[test] fn test_can_parse_schema_plan() -> anyhow::Result<()> { let plan = RawPlan::from_file( r##" [plan] schema = {nickel = "contract.ncl"} "##, &PathBuf::new(), )?; assert_eq!( RawPlanConfig { plan: RawPlanConfigSection { schema: Some(RawPlanSchema::Nickel { nickel: "contract.ncl".into() }), } }, plan.config, ); Ok(()) } }