1use serde::{Deserialize, Serialize};
10use tatara_lisp::DeriveTataraDomain;
11
12use crate::SpecError;
13
14#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
15#[tatara(keyword = "defprofile-format")]
16pub struct ProfileFormat {
17 pub name: String,
18 pub kind: ProfileKind,
19 #[serde(rename = "generationLinkPattern")]
22 pub generation_link_pattern: String,
23 #[serde(rename = "manifestPath")]
26 pub manifest_path: String,
27}
28
29#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum ProfileKind {
31 System,
34 User,
37 Ephemeral,
39}
40
41pub const CANONICAL_PROFILE_LISP: &str = include_str!("../specs/profile.lisp");
42
43pub fn load_canonical() -> Result<Vec<ProfileFormat>, SpecError> {
49 crate::loader::load_all::<ProfileFormat>(CANONICAL_PROFILE_LISP)
50}
51
52#[must_use]
58pub fn generation_link(format: &ProfileFormat, profile: &str, n: u32) -> String {
59 format
60 .generation_link_pattern
61 .replace("<profile>", profile)
62 .replace("<N>", &n.to_string())
63}
64
65#[must_use]
68pub fn next_generation(format: &ProfileFormat, profile: &str, current: u32) -> String {
69 generation_link(format, profile, current + 1)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn canonical_profile_parses() {
78 let formats = load_canonical().unwrap();
79 assert!(!formats.is_empty());
80 }
81
82 fn system_fmt() -> ProfileFormat {
85 load_canonical().unwrap().into_iter()
86 .find(|f| f.name == "cppnix-system-profile").unwrap()
87 }
88
89 #[test]
90 fn generation_link_substitutes_placeholders() {
91 let f = system_fmt();
92 let link = generation_link(&f, "system", 42);
93 assert_eq!(link, "system-42-link");
94 }
95
96 #[test]
97 fn next_generation_increments() {
98 let f = system_fmt();
99 let next = next_generation(&f, "system", 41);
100 assert_eq!(next, "system-42-link");
101 }
102
103 #[test]
104 fn three_profile_kinds_present() {
105 let formats = load_canonical().unwrap();
106 let kinds: std::collections::HashSet<ProfileKind> =
107 formats.iter().map(|f| f.kind).collect();
108 for required in [ProfileKind::System, ProfileKind::User, ProfileKind::Ephemeral] {
109 assert!(kinds.contains(&required), "missing profile kind {required:?}");
110 }
111 }
112}