Skip to main content

prosaic_project/
scaffold.rs

1//! Starter project templates for `prosaic new`.
2
3use 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}