1use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct Manifest {
12 pub project: Project,
13 #[serde(default)]
14 pub stack: Stack,
15 #[serde(default)]
16 pub features: Features,
17 #[serde(default)]
18 pub modules: Modules,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Project {
23 pub name: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct Stack {
28 pub framework: String,
29 pub database: String,
30 pub orm: String,
31}
32
33impl Default for Stack {
34 fn default() -> Self {
35 Self {
37 framework: "axum".to_string(),
38 database: "postgres".to_string(),
39 orm: "sqlx".to_string(),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
45pub struct Features {
46 #[serde(default)]
47 pub authentication: bool,
48 #[serde(default)]
49 pub admin: bool,
50 #[serde(default)]
51 pub openapi: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
55pub struct Modules {
56 #[serde(default)]
57 pub list: Vec<String>,
58}
59
60impl Manifest {
61 pub fn new(project_name: impl Into<String>) -> Self {
63 Self {
64 project: Project {
65 name: project_name.into(),
66 },
67 stack: Stack::default(),
68 features: Features::default(),
69 modules: Modules::default(),
70 }
71 }
72
73 pub fn from_toml(text: &str) -> Result<Self> {
75 toml::from_str(text).context("failed to parse gize.toml")
76 }
77
78 pub fn to_toml(&self) -> Result<String> {
80 toml::to_string_pretty(self).context("failed to serialize manifest")
81 }
82
83 pub fn add_module(&mut self, name: impl Into<String>) -> bool {
86 let name = name.into();
87 if self.modules.list.contains(&name) {
88 return false;
89 }
90 self.modules.list.push(name);
91 self.modules.list.sort();
92 true
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn roundtrips_through_toml() {
102 let mut m = Manifest::new("shop");
103 m.features.admin = true;
104 m.add_module("products");
105 let text = m.to_toml().unwrap();
106 let parsed = Manifest::from_toml(&text).unwrap();
107 assert_eq!(m, parsed);
108 }
109
110 #[test]
111 fn adding_module_is_idempotent() {
112 let mut m = Manifest::new("shop");
113 assert!(m.add_module("users"));
114 assert!(!m.add_module("users"));
115 assert_eq!(m.modules.list, vec!["users".to_string()]);
116 }
117
118 #[test]
119 fn parses_minimal_manifest() {
120 let text = r#"
121 [project]
122 name = "blog"
123 "#;
124 let m = Manifest::from_toml(text).unwrap();
125 assert_eq!(m.project.name, "blog");
126 assert_eq!(m.stack.framework, "axum");
128 }
129}