Skip to main content

max / makenotwork

9.3 KB · 344 lines History Blame Raw
1 //! Domain vocabulary for Bento — the types every module speaks.
2 //!
3 //! Bento's axes are App x Target x Step. A `(app, target)` resolves to a Rhai
4 //! recipe; the recipe walks the canonical [`Step`] sequence. Newtypes carry
5 //! the boundary parse: a [`Target`] exists because some `"platform/arch"`
6 //! string validated, so downstream code never re-parses.
7
8 use serde::{Deserialize, Serialize};
9 use std::fmt;
10 use std::str::FromStr;
11
12 // ---------------------------------------------------------------------
13 // App identifier
14 // ---------------------------------------------------------------------
15
16 /// An app Bento can release: `goingson`, `balanced_breakfast`, `audiofiles`.
17 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18 #[serde(transparent)]
19 pub struct AppId(String);
20
21 impl AppId {
22 pub fn new(s: impl Into<String>) -> Self {
23 Self(s.into())
24 }
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28 }
29
30 impl fmt::Display for AppId {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 self.0.fmt(f)
33 }
34 }
35
36 impl From<&str> for AppId {
37 fn from(s: &str) -> Self {
38 Self(s.to_owned())
39 }
40 }
41
42 // ---------------------------------------------------------------------
43 // Platform / Arch / Target
44 // ---------------------------------------------------------------------
45
46 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47 #[serde(rename_all = "snake_case")]
48 pub enum Platform {
49 Macos,
50 Ios,
51 Linux,
52 Windows,
53 Android,
54 }
55
56 impl Platform {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Platform::Macos => "macos",
60 Platform::Ios => "ios",
61 Platform::Linux => "linux",
62 Platform::Windows => "windows",
63 Platform::Android => "android",
64 }
65 }
66 }
67
68 impl FromStr for Platform {
69 type Err = String;
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 Ok(match s {
72 "macos" => Platform::Macos,
73 "ios" => Platform::Ios,
74 "linux" => Platform::Linux,
75 "windows" => Platform::Windows,
76 "android" => Platform::Android,
77 other => return Err(format!("unknown platform `{other}`")),
78 })
79 }
80 }
81
82 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83 #[serde(rename_all = "snake_case")]
84 pub enum Arch {
85 Aarch64,
86 X86_64,
87 Universal,
88 }
89
90 impl Arch {
91 pub fn as_str(self) -> &'static str {
92 match self {
93 Arch::Aarch64 => "aarch64",
94 Arch::X86_64 => "x86_64",
95 Arch::Universal => "universal",
96 }
97 }
98 }
99
100 impl FromStr for Arch {
101 type Err = String;
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Ok(match s {
104 "aarch64" => Arch::Aarch64,
105 "x86_64" => Arch::X86_64,
106 "universal" => Arch::Universal,
107 other => return Err(format!("unknown arch `{other}`")),
108 })
109 }
110 }
111
112 /// A build target: `(platform, arch)`, rendered `platform/arch`
113 /// (e.g. `macos/aarch64`). This is the dispatch unit — a target only runs on a
114 /// host that declares it can build it natively.
115 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116 pub struct Target {
117 pub platform: Platform,
118 pub arch: Arch,
119 }
120
121 impl Target {
122 pub fn new(platform: Platform, arch: Arch) -> Self {
123 Self { platform, arch }
124 }
125 }
126
127 impl fmt::Display for Target {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "{}/{}", self.platform.as_str(), self.arch.as_str())
130 }
131 }
132
133 impl FromStr for Target {
134 type Err = String;
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 let (p, a) = s.split_once('/').ok_or_else(|| format!("target `{s}` is not `platform/arch`"))?;
137 Ok(Target::new(p.parse()?, a.parse()?))
138 }
139 }
140
141 // Round-trip through JSON / TOML as the `platform/arch` string.
142 impl Serialize for Target {
143 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
144 s.serialize_str(&self.to_string())
145 }
146 }
147
148 impl<'de> Deserialize<'de> for Target {
149 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
150 let s = String::deserialize(d)?;
151 s.parse().map_err(serde::de::Error::custom)
152 }
153 }
154
155 // ---------------------------------------------------------------------
156 // Step
157 // ---------------------------------------------------------------------
158
159 /// The canonical release step sequence. A recipe marks transitions by calling
160 /// the `step(name)` host function; not every platform uses every step (Linux
161 /// skips sign/notarize/staple). The TUI renders these as matrix columns.
162 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163 #[serde(rename_all = "snake_case")]
164 pub enum Step {
165 Checkout,
166 Prebuild,
167 Build,
168 Sign,
169 Notarize,
170 Staple,
171 Verify,
172 Package,
173 Publish,
174 Collect,
175 }
176
177 impl Step {
178 /// All steps in canonical order — the matrix column set.
179 pub const ALL: [Step; 10] = [
180 Step::Checkout,
181 Step::Prebuild,
182 Step::Build,
183 Step::Sign,
184 Step::Notarize,
185 Step::Staple,
186 Step::Verify,
187 Step::Package,
188 Step::Publish,
189 Step::Collect,
190 ];
191
192 pub fn as_str(self) -> &'static str {
193 match self {
194 Step::Checkout => "checkout",
195 Step::Prebuild => "prebuild",
196 Step::Build => "build",
197 Step::Sign => "sign",
198 Step::Notarize => "notarize",
199 Step::Staple => "staple",
200 Step::Verify => "verify",
201 Step::Package => "package",
202 Step::Publish => "publish",
203 Step::Collect => "collect",
204 }
205 }
206 }
207
208 impl fmt::Display for Step {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str(self.as_str())
211 }
212 }
213
214 impl FromStr for Step {
215 type Err = String;
216 fn from_str(s: &str) -> Result<Self, Self::Err> {
217 Step::ALL
218 .into_iter()
219 .find(|st| st.as_str() == s)
220 .ok_or_else(|| format!("unknown step `{s}`"))
221 }
222 }
223
224 // ---------------------------------------------------------------------
225 // Version (semver)
226 // ---------------------------------------------------------------------
227
228 /// App semver (e.g. `0.4.1`), read from `tauri.conf.json` or supplied to
229 /// `/build`. Stored as TEXT.
230 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
231 pub struct Version(semver::Version);
232
233 impl Version {
234 pub fn parse(s: &str) -> Result<Self, String> {
235 semver::Version::parse(s)
236 .map(Self)
237 .map_err(|e| format!("invalid semver `{s}`: {e}"))
238 }
239 }
240
241 impl fmt::Display for Version {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 self.0.fmt(f)
244 }
245 }
246
247 impl FromStr for Version {
248 type Err = String;
249 fn from_str(s: &str) -> Result<Self, Self::Err> {
250 Version::parse(s)
251 }
252 }
253
254 impl Serialize for Version {
255 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
256 s.serialize_str(&self.to_string())
257 }
258 }
259
260 impl<'de> Deserialize<'de> for Version {
261 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
262 let s = String::deserialize(d)?;
263 Version::parse(&s).map_err(serde::de::Error::custom)
264 }
265 }
266
267 // ---------------------------------------------------------------------
268 // StepRunId — primary key of `step_runs`, used to key live tails (Ord so the
269 // TUI can iterate chronologically, like Sando's GateRunId).
270 // ---------------------------------------------------------------------
271
272 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
273 #[serde(transparent)]
274 pub struct StepRunId(pub i64);
275
276 impl fmt::Display for StepRunId {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 self.0.fmt(f)
279 }
280 }
281
282 /// Outcome of a step / target / build.
283 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284 #[serde(rename_all = "snake_case")]
285 pub enum Status {
286 Pending,
287 Running,
288 Ok,
289 Failed,
290 }
291
292 impl Status {
293 pub fn as_str(self) -> &'static str {
294 match self {
295 Status::Pending => "pending",
296 Status::Running => "running",
297 Status::Ok => "ok",
298 Status::Failed => "failed",
299 }
300 }
301 }
302
303 #[cfg(test)]
304 mod tests {
305 use super::*;
306
307 #[test]
308 fn target_roundtrips() {
309 let t: Target = "macos/aarch64".parse().unwrap();
310 assert_eq!(t.platform, Platform::Macos);
311 assert_eq!(t.arch, Arch::Aarch64);
312 assert_eq!(t.to_string(), "macos/aarch64");
313 }
314
315 #[test]
316 fn target_rejects_garbage() {
317 assert!("macos".parse::<Target>().is_err());
318 assert!("mac/aarch64".parse::<Target>().is_err());
319 assert!("macos/sparc".parse::<Target>().is_err());
320 }
321
322 #[test]
323 fn target_json_is_the_string() {
324 let t: Target = "linux/x86_64".parse().unwrap();
325 assert_eq!(serde_json::to_string(&t).unwrap(), "\"linux/x86_64\"");
326 let back: Target = serde_json::from_str("\"linux/x86_64\"").unwrap();
327 assert_eq!(back, t);
328 }
329
330 #[test]
331 fn step_roundtrips() {
332 for s in Step::ALL {
333 assert_eq!(s.as_str().parse::<Step>().unwrap(), s);
334 }
335 assert!("frobnicate".parse::<Step>().is_err());
336 }
337
338 #[test]
339 fn version_parses_semver() {
340 assert_eq!(Version::parse("0.4.1").unwrap().to_string(), "0.4.1");
341 assert!(Version::parse("v0.4").is_err());
342 }
343 }
344