prosaic_project/
scaffold.rs1use std::fs;
4use std::path::Path;
5
6use crate::error::ProjectError;
7
8#[derive(Debug, Clone, Copy)]
9pub enum Starter {
10 Blank,
11 Changelog,
12 VocabPack,
13}
14
15impl std::str::FromStr for Starter {
16 type Err = String;
17
18 fn from_str(s: &str) -> Result<Self, Self::Err> {
19 match s {
20 "blank" => Ok(Self::Blank),
21 "changelog" => Ok(Self::Changelog),
22 "vocab-pack" => Ok(Self::VocabPack),
23 other => Err(format!(
24 "unknown starter `{other}`; expected: blank | changelog | vocab-pack"
25 )),
26 }
27 }
28}
29
30pub fn scaffold_project(name: &str, dir: &Path, starter: Starter) -> Result<(), ProjectError> {
31 if dir.exists() && fs::read_dir(dir).map(|d| d.count() > 0).unwrap_or(false) {
32 return Err(ProjectError::Io {
33 path: dir.display().to_string(),
34 cause: "directory exists and is not empty".to_string(),
35 });
36 }
37 fs::create_dir_all(dir).map_err(|e| ProjectError::Io {
38 path: dir.display().to_string(),
39 cause: e.to_string(),
40 })?;
41
42 let manifest = format!(
43 r#"name = "{name}"
44version = "0.1.0"
45language = "en"
46
47[engine]
48strictness = "strict"
49variation = "fixed"
50"#
51 );
52 write(&dir.join("prosaic.toml"), &manifest)?;
53
54 match starter {
55 Starter::Blank => {
56 fs::create_dir_all(dir.join("templates")).ok();
57 fs::create_dir_all(dir.join("partials")).ok();
58 fs::create_dir_all(dir.join("fixtures")).ok();
59 fs::create_dir_all(dir.join("tests")).ok();
60 }
61 Starter::Changelog => {
62 fs::create_dir_all(dir.join("templates")).ok();
63 fs::create_dir_all(dir.join("fixtures")).ok();
64 fs::create_dir_all(dir.join("tests")).ok();
65 write(
66 &dir.join("templates/code.added.toml"),
67 r#"key = "code.added"
68
69[[variants]]
70salience = "medium"
71body = "{name|refer} was added"
72"#,
73 )?;
74 write(
75 &dir.join("templates/code.modified.toml"),
76 r#"key = "code.modified"
77
78[[variants]]
79salience = "low"
80body = "{name|refer} was modified"
81
82[[variants]]
83salience = "medium"
84body = "{name|refer} was modified{?consumer_count}, affecting {consumer_count} {consumer_count|pluralize:consumer}{/?}"
85"#,
86 )?;
87 write(
88 &dir.join("fixtures/userservice-modified.json"),
89 r#"{"name": "UserService", "entity_type": "class", "consumer_count": 6}"#,
90 )?;
91 write(
92 &dir.join("fixtures/authguard-added.json"),
93 r#"{"name": "AuthGuard", "entity_type": "class"}"#,
94 )?;
95 write(
96 &dir.join("tests/sample-changeset.toml"),
97 r#"name = "sample-changeset"
98
99[[events]]
100template = "code.added"
101context = { name = "AuthGuard", entity_type = "class" }
102
103[[events]]
104template = "code.modified"
105context = { name = "UserService", entity_type = "class", consumer_count = 6 }
106"#,
107 )?;
108 write(
109 &dir.join("tests/authguard-added.toml"),
110 r#"name = "authguard-added"
111
112[[events]]
113template = "code.added"
114context = { name = "AuthGuard", entity_type = "class" }
115"#,
116 )?;
117 }
118 Starter::VocabPack => {
119 fs::create_dir_all(dir.join("templates")).ok();
120 fs::create_dir_all(dir.join("partials")).ok();
121 fs::create_dir_all(dir.join("tests")).ok();
122 write(
123 &dir.join("partials/impact_tail.toml"),
124 r#"name = "impact_tail"
125description = "Trailing 'affecting N consumers' clause."
126body = "{?consumer_count}, affecting {consumer_count} {consumer_count|pluralize:consumer}{/?}"
127"#,
128 )?;
129 write(
130 &dir.join("templates/code.modified.toml"),
131 r#"key = "code.modified"
132slots_required = ["name"]
133slots_optional = ["consumer_count"]
134
135[[variants]]
136salience = "low"
137body = "{name|refer} was modified"
138
139[[variants]]
140salience = "medium"
141body = "{name|refer} was modified{>impact_tail}"
142
143[[variants]]
144salience = "high"
145body = "{name|refer} has been substantially modified{>impact_tail}. Thorough review is recommended."
146"#,
147 )?;
148 }
149 }
150 Ok(())
151}
152
153fn write(path: &Path, content: &str) -> Result<(), ProjectError> {
154 fs::write(path, content).map_err(|e| ProjectError::Io {
155 path: path.display().to_string(),
156 cause: e.to_string(),
157 })
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use tempfile::tempdir;
164
165 #[test]
166 fn scaffold_blank_creates_layout() {
167 let tmp = tempdir().unwrap();
168 let dir = tmp.path().join("blank-proj");
169 scaffold_project("blank-proj", &dir, Starter::Blank).unwrap();
170 assert!(dir.join("prosaic.toml").exists());
171 assert!(dir.join("templates").is_dir());
172 assert!(dir.join("partials").is_dir());
173 assert!(dir.join("fixtures").is_dir());
174 assert!(dir.join("tests").is_dir());
175 }
176
177 #[test]
178 fn scaffold_changelog_creates_starter_templates() {
179 let tmp = tempdir().unwrap();
180 let dir = tmp.path().join("cl");
181 scaffold_project("cl", &dir, Starter::Changelog).unwrap();
182 assert!(dir.join("templates/code.added.toml").exists());
183 assert!(dir.join("templates/code.modified.toml").exists());
184 assert!(dir.join("fixtures/userservice-modified.json").exists());
185 assert!(dir.join("fixtures/authguard-added.json").exists());
186 assert!(dir.join("tests/sample-changeset.toml").exists());
187 assert!(dir.join("tests/authguard-added.toml").exists());
188
189 let project = crate::Project::load_from_dir(&dir).unwrap();
190 assert_eq!(project.fixtures.len(), 2);
191 assert_eq!(project.scenarios.len(), 2);
192
193 let engine = project.into_engine().unwrap();
194 let authguard = project.fixtures.get("authguard-added").unwrap();
195 let mut session = prosaic_core::Session::new();
196 let output = engine
197 .render(&mut session, "code.modified", authguard)
198 .unwrap();
199 assert!(output.contains("AuthGuard"));
200 }
201
202 #[test]
203 fn scaffold_vocab_pack_creates_partial() {
204 let tmp = tempdir().unwrap();
205 let dir = tmp.path().join("vp");
206 scaffold_project("vp", &dir, Starter::VocabPack).unwrap();
207 assert!(dir.join("partials/impact_tail.toml").exists());
208 assert!(dir.join("templates/code.modified.toml").exists());
209 }
210
211 #[test]
212 fn scaffold_into_nonempty_dir_errors() {
213 let tmp = tempdir().unwrap();
214 let dir = tmp.path().join("occupied");
215 std::fs::create_dir_all(&dir).unwrap();
216 std::fs::write(dir.join("README.md"), "x").unwrap();
217 let res = scaffold_project("occ", &dir, Starter::Blank);
218 assert!(res.is_err());
219 }
220}