use std::{net::SocketAddr, path::PathBuf}; use clap::{Parser, Subcommand}; use kdl::KdlDocument; use rusty_s3::{Bucket, Credentials, S3Action}; use tokio::io::AsyncWriteExt; use crate::{ model::{Context, Plan, Project, TemplateType}, plan_reconciler::PlanReconciler, state::SharedState, }; #[derive(Parser)] #[command(author, version, about, long_about = None, subcommand_required = true)] struct Command { #[command(subcommand)] command: Option, #[arg( env = "FOREST_PROJECT_PATH", long = "project-path", default_value = "." )] project_path: PathBuf, } #[derive(Subcommand)] enum Commands { Init {}, Template {}, Serve { #[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")] host: SocketAddr, #[arg(env = "FOREST_S3_ENDPOINT", long = "s3-endpoint")] s3_endpoint: String, #[arg(env = "FOREST_S3_REGION", long = "s3-region")] s3_region: String, #[arg(env = "FOREST_S3_BUCKET", long = "s3-bucket")] s3_bucket: String, #[arg(env = "FOREST_S3_USER", long = "s3-user")] s3_user: String, #[arg(env = "FOREST_S3_PASSWORD", long = "s3-password")] s3_password: String, }, } pub async fn execute() -> anyhow::Result<()> { let cli = Command::parse(); let project_path = &cli.project_path.canonicalize()?; let project_file_path = project_path.join("forest.kdl"); if !project_file_path.exists() { anyhow::bail!( "no 'forest.kdl' file was found at: {}", project_file_path.display().to_string() ); } let project_file = tokio::fs::read_to_string(&project_file_path).await?; let project_doc: KdlDocument = project_file.parse()?; let project: Project = project_doc.try_into()?; tracing::trace!("found a project name: {}", project.name); let plan = if let Some(plan_file_path) = PlanReconciler::new() .reconcile(&project, project_path) .await? { let plan_file = tokio::fs::read_to_string(&plan_file_path).await?; let plan_doc: KdlDocument = plan_file.parse()?; let plan: Plan = plan_doc.try_into()?; tracing::trace!("found a plan name: {}", project.name); Some(plan) } else { None }; let context = Context { project, plan }; match cli.command.unwrap() { Commands::Init {} => { tracing::info!("initializing project"); tracing::trace!("found context: {:?}", context); } Commands::Template {} => { tracing::info!("templating"); let Some(template) = context.project.templates else { return Ok(()); }; match template.ty { TemplateType::Jinja2 => { for entry in glob::glob(&format!( "{}/{}", project_path.display().to_string().trim_end_matches("/"), template.path.trim_start_matches("./"), )) .map_err(|e| anyhow::anyhow!("failed to read glob pattern: {}", e))? { let entry = entry.map_err(|e| anyhow::anyhow!("failed to read path: {}", e))?; let entry_name = entry.display().to_string(); let entry_rel = if entry.is_absolute() { entry.strip_prefix(project_path).map(|e| e.to_path_buf()) } else { Ok(entry.clone()) }; let rel_file_path = entry_rel .map(|p| { if p.file_name() .map(|f| f.to_string_lossy().ends_with(".jinja2")) .unwrap_or(false) { p.with_file_name( p.file_stem().expect("to be able to find a filename"), ) } else { p.to_path_buf() } }) .map_err(|e| { anyhow::anyhow!( "failed to find relative file: {}, project: {}, file: {}", e, project_path.display(), entry_name ) })?; let output_file_path = project_path .join(".forest/temp") .join(&template.output) .join(rel_file_path); let contents = tokio::fs::read_to_string(&entry).await.map_err(|e| { anyhow::anyhow!( "failed to read template: {}, err: {}", entry.display(), e ) })?; let mut env = minijinja::Environment::new(); env.add_template(&entry_name, &contents)?; let tmpl = env.get_template(&entry_name)?; let output = tmpl .render(minijinja::context! {}) .map_err(|e| anyhow::anyhow!("failed to render template: {}", e))?; tracing::info!("rendered template: {}", output); if let Some(parent) = output_file_path.parent() { tokio::fs::create_dir_all(parent).await.map_err(|e| { anyhow::anyhow!( "failed to create directory (path: {}) for output: {}", parent.display(), e ) })?; } let mut output_file = tokio::fs::File::create(&output_file_path) .await .map_err(|e| { anyhow::anyhow!( "failed to create file: {}, error: {}", output_file_path.display(), e ) })?; output_file.write_all(output.as_bytes()).await?; } } } } Commands::Serve { host, s3_endpoint, s3_bucket, s3_region, s3_user, s3_password, } => { tracing::info!("Starting server"); let creds = Credentials::new(s3_user, s3_password); let bucket = Bucket::new( url::Url::parse(&s3_endpoint)?, rusty_s3::UrlStyle::Path, s3_bucket, s3_region, )?; let put_object = bucket.put_object(Some(&creds), "some-object"); let _url = put_object.sign(std::time::Duration::from_secs(30)); let _state = SharedState::new().await?; } } Ok(()) }