//! Domain vocabulary for Bento — the types every module speaks. //! //! Bento's axes are App x Target x Step. A `(app, target)` resolves to a Rhai //! recipe; the recipe walks the canonical [`Step`] sequence. Newtypes carry //! the boundary parse: a [`Target`] exists because some `"platform/arch"` //! string validated, so downstream code never re-parses. use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; // --------------------------------------------------------------------- // App identifier // --------------------------------------------------------------------- /// An app Bento can release: `goingson`, `balanced_breakfast`, `audiofiles`. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct AppId(String); impl AppId { pub fn new(s: impl Into) -> Self { Self(s.into()) } pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for AppId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl From<&str> for AppId { fn from(s: &str) -> Self { Self(s.to_owned()) } } // --------------------------------------------------------------------- // Platform / Arch / Target // --------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Platform { Macos, Ios, Linux, Windows, Android, } impl Platform { pub fn as_str(self) -> &'static str { match self { Platform::Macos => "macos", Platform::Ios => "ios", Platform::Linux => "linux", Platform::Windows => "windows", Platform::Android => "android", } } } impl FromStr for Platform { type Err = String; fn from_str(s: &str) -> Result { Ok(match s { "macos" => Platform::Macos, "ios" => Platform::Ios, "linux" => Platform::Linux, "windows" => Platform::Windows, "android" => Platform::Android, other => return Err(format!("unknown platform `{other}`")), }) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Arch { Aarch64, X86_64, Universal, } impl Arch { pub fn as_str(self) -> &'static str { match self { Arch::Aarch64 => "aarch64", Arch::X86_64 => "x86_64", Arch::Universal => "universal", } } } impl FromStr for Arch { type Err = String; fn from_str(s: &str) -> Result { Ok(match s { "aarch64" => Arch::Aarch64, "x86_64" => Arch::X86_64, "universal" => Arch::Universal, other => return Err(format!("unknown arch `{other}`")), }) } } /// A build target: `(platform, arch)`, rendered `platform/arch` /// (e.g. `macos/aarch64`). This is the dispatch unit — a target only runs on a /// host that declares it can build it natively. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Target { pub platform: Platform, pub arch: Arch, } impl Target { pub fn new(platform: Platform, arch: Arch) -> Self { Self { platform, arch } } } impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}/{}", self.platform.as_str(), self.arch.as_str()) } } impl FromStr for Target { type Err = String; fn from_str(s: &str) -> Result { let (p, a) = s.split_once('/').ok_or_else(|| format!("target `{s}` is not `platform/arch`"))?; Ok(Target::new(p.parse()?, a.parse()?)) } } // Round-trip through JSON / TOML as the `platform/arch` string. impl Serialize for Target { fn serialize(&self, s: S) -> Result { s.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for Target { fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; s.parse().map_err(serde::de::Error::custom) } } // --------------------------------------------------------------------- // Step // --------------------------------------------------------------------- /// The canonical release step sequence. A recipe marks transitions by calling /// the `step(name)` host function; not every platform uses every step (Linux /// skips sign/notarize/staple). The TUI renders these as matrix columns. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Step { Checkout, Prebuild, Build, Sign, Notarize, Staple, Verify, Package, Publish, Collect, } impl Step { /// All steps in canonical order — the matrix column set. pub const ALL: [Step; 10] = [ Step::Checkout, Step::Prebuild, Step::Build, Step::Sign, Step::Notarize, Step::Staple, Step::Verify, Step::Package, Step::Publish, Step::Collect, ]; pub fn as_str(self) -> &'static str { match self { Step::Checkout => "checkout", Step::Prebuild => "prebuild", Step::Build => "build", Step::Sign => "sign", Step::Notarize => "notarize", Step::Staple => "staple", Step::Verify => "verify", Step::Package => "package", Step::Publish => "publish", Step::Collect => "collect", } } } impl fmt::Display for Step { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl FromStr for Step { type Err = String; fn from_str(s: &str) -> Result { Step::ALL .into_iter() .find(|st| st.as_str() == s) .ok_or_else(|| format!("unknown step `{s}`")) } } // --------------------------------------------------------------------- // Version (semver) // --------------------------------------------------------------------- /// App semver (e.g. `0.4.1`), read from `tauri.conf.json` or supplied to /// `/build`. Stored as TEXT. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Version(semver::Version); impl Version { pub fn parse(s: &str) -> Result { semver::Version::parse(s) .map(Self) .map_err(|e| format!("invalid semver `{s}`: {e}")) } } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl FromStr for Version { type Err = String; fn from_str(s: &str) -> Result { Version::parse(s) } } impl Serialize for Version { fn serialize(&self, s: S) -> Result { s.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for Version { fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; Version::parse(&s).map_err(serde::de::Error::custom) } } // --------------------------------------------------------------------- // StepRunId — primary key of `step_runs`, used to key live tails (Ord so the // TUI can iterate chronologically, like Sando's GateRunId). // --------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct StepRunId(pub i64); impl fmt::Display for StepRunId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Outcome of a step / target / build. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Status { Pending, Running, Ok, Failed, } impl Status { pub fn as_str(self) -> &'static str { match self { Status::Pending => "pending", Status::Running => "running", Status::Ok => "ok", Status::Failed => "failed", } } } #[cfg(test)] mod tests { use super::*; #[test] fn target_roundtrips() { let t: Target = "macos/aarch64".parse().unwrap(); assert_eq!(t.platform, Platform::Macos); assert_eq!(t.arch, Arch::Aarch64); assert_eq!(t.to_string(), "macos/aarch64"); } #[test] fn target_rejects_garbage() { assert!("macos".parse::().is_err()); assert!("mac/aarch64".parse::().is_err()); assert!("macos/sparc".parse::().is_err()); } #[test] fn target_json_is_the_string() { let t: Target = "linux/x86_64".parse().unwrap(); assert_eq!(serde_json::to_string(&t).unwrap(), "\"linux/x86_64\""); let back: Target = serde_json::from_str("\"linux/x86_64\"").unwrap(); assert_eq!(back, t); } #[test] fn step_roundtrips() { for s in Step::ALL { assert_eq!(s.as_str().parse::().unwrap(), s); } assert!("frobnicate".parse::().is_err()); } #[test] fn version_parses_semver() { assert_eq!(Version::parse("0.4.1").unwrap().to_string(), "0.4.1"); assert!(Version::parse("v0.4").is_err()); } }