use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{discover_environments, environment::EnvironmentError, EnvironmentResource}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Entrypoint { pub name: String, pub function: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ProjectModel { pub root: PathBuf, pub environments: Vec, pub entrypoints: BTreeMap, pub default_entrypoint: String, pub required_config_file: Option, } #[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum ProjectModelError { #[error("environment discovery failed: {0}")] Environment(String), #[error("unknown Disasmer entrypoint `{name}`; available entrypoints: {available:?}")] UnknownEntrypoint { name: String, available: Vec, }, } impl ProjectModel { pub fn discover_without_config(root: &Path) -> Result { let environments = discover_environments(root).map_err(|err| { ProjectModelError::Environment(match err { EnvironmentError::Read { path, source } => { format!("failed to read {}: {source}", path.display()) } }) })?; Ok(Self { root: root.to_path_buf(), environments, entrypoints: default_entrypoints(), default_entrypoint: "build".to_owned(), required_config_file: None, }) } pub fn select_entrypoint(&self, name: Option<&str>) -> Result<&Entrypoint, ProjectModelError> { let name = name.unwrap_or(&self.default_entrypoint); self.entrypoints .get(name) .ok_or_else(|| ProjectModelError::UnknownEntrypoint { name: name.to_owned(), available: self.entrypoints.keys().cloned().collect(), }) } } fn default_entrypoints() -> BTreeMap { ["build", "test", "package", "release", "watch"] .into_iter() .map(|name| { ( name.to_owned(), Entrypoint { name: name.to_owned(), function: format!("{name}_main"), }, ) }) .collect() } #[cfg(test)] mod tests { use std::fs; use super::*; #[test] fn project_works_without_hand_written_configuration_file() { let temp = tempfile::tempdir().unwrap(); fs::create_dir_all(temp.path().join("envs/linux")).unwrap(); fs::write( temp.path().join("envs/linux/Containerfile"), "FROM alpine\n", ) .unwrap(); let model = ProjectModel::discover_without_config(temp.path()).unwrap(); assert_eq!(model.required_config_file, None); assert_eq!(model.environments[0].name, "linux"); assert_eq!(model.select_entrypoint(None).unwrap().name, "build"); } #[test] fn project_can_define_multiple_default_entrypoints() { let temp = tempfile::tempdir().unwrap(); let model = ProjectModel::discover_without_config(temp.path()).unwrap(); assert_eq!( model.select_entrypoint(Some("test")).unwrap().function, "test_main" ); assert_eq!( model.select_entrypoint(Some("release")).unwrap().function, "release_main" ); } #[test] fn unknown_entrypoint_lists_available_choices() { let temp = tempfile::tempdir().unwrap(); let model = ProjectModel::discover_without_config(temp.path()).unwrap(); let error = model.select_entrypoint(Some("deploy")).unwrap_err(); assert!(matches!(error, ProjectModelError::UnknownEntrypoint { .. })); } }