1use std::fs;
4use std::path::{Path, PathBuf};
5use std::process;
6use std::sync::atomic::{AtomicU32, Ordering};
7
8use crate::naming::{contract_package, crate_name, stored_name};
9use crate::{
10 Aggregate, App, AreaClient, AreaContribution, AreaDefinition, Crate, Dependency, Effects,
11 EnumDeclaration, EnumVariant, FieldDeclaration, Function, Manifest, Method, Module, Route,
12 Tier, TypeMethod, TypeName,
13};
14
15static NEXT_DIRECTORY: AtomicU32 = AtomicU32::new(0);
16
17pub struct TemporaryDirectory {
18 root: PathBuf,
19}
20
21impl TemporaryDirectory {
22 #[must_use]
24 pub fn new(prefix: &str) -> Self {
25 let root = std::env::temp_dir().join(format!(
26 "{prefix}-{}-{}",
27 process::id(),
28 NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed)
29 ));
30 fs::create_dir_all(&root).expect("the temporary directory is creatable");
31 Self { root }
32 }
33
34 #[must_use]
35 pub fn root(&self) -> &Path {
36 &self.root
37 }
38
39 #[must_use]
40 pub fn path(&self, relative: &str) -> PathBuf {
41 self.root.join(relative)
42 }
43
44 #[expect(
45 clippy::must_use_candidate,
46 reason = "most callers only want the file written"
47 )]
48 pub fn write(&self, relative: &str, text: &str) -> PathBuf {
49 let path = self.path(relative);
50 let directory = path.parent().expect("a written file lives in a directory");
51 fs::create_dir_all(directory).expect("the fixture directory is creatable");
52 fs::write(&path, text).expect("the fixture file is writable");
53 path
54 }
55}
56
57impl Drop for TemporaryDirectory {
58 fn drop(&mut self) {
59 if let Err(error) = fs::remove_dir_all(&self.root) {
60 eprintln!("cannot remove {}: {error}", self.root.display());
61 }
62 }
63}
64
65#[must_use]
66pub fn manifest(package: &str, dependencies: &str) -> String {
67 format!(
68 "[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{dependencies}"
69 )
70}
71
72pub fn krate(package: &str, path: impl Into<PathBuf>) -> Crate {
73 Crate {
74 package: package.to_owned(),
75 crate_name: crate_name(package),
76 path: path.into(),
77 dependencies: Vec::new(),
78 sources: Vec::new(),
79 tests_and_examples: Vec::new(),
80 }
81}
82
83#[must_use]
84pub fn dependency(package: &str) -> Dependency {
85 Dependency {
86 package: package.to_owned(),
87 crate_name: crate_name(package),
88 }
89}
90
91pub fn module(package: &str, path: impl Into<PathBuf>) -> Module {
92 let krate = krate(package, path);
93 let manifest = Manifest {
94 name: krate.crate_name.clone(),
95 tier: Tier::Core,
96 context: None,
97 settings: Vec::new(),
98 file: krate.path.join("src/module.rs"),
99 line: 3,
100 declared: true,
101 };
102 Module {
103 krate,
104 manifest: Some(manifest),
105 config: None,
106 contract: None,
107 defined_areas: Vec::new(),
108 contract_declares_gen: false,
109 contract_exports: Vec::new(),
110 contract_traits: Vec::new(),
111 migrations: Vec::new(),
112 sqlite_migrations: Vec::new(),
113 areas: Vec::new(),
114 listeners: Vec::new(),
115 schedules: Vec::new(),
116 events: Vec::new(),
117 module_events: Vec::new(),
118 jobs: Vec::new(),
119 tasks: Vec::new(),
120 implementations: Vec::new(),
121 extensions: Vec::new(),
122 gateways: Vec::new(),
123 islands: Vec::new(),
124 components: Vec::new(),
125 languages: Vec::new(),
126 mails: Vec::new(),
127 plans: None,
128 private_items: Vec::new(),
129 unrouted_areas: Vec::new(),
130 code: crate::Code::default(),
131 }
132}
133
134#[must_use]
135pub fn area(module: &Module, name: &str, routes: Vec<Route>) -> AreaContribution {
136 AreaContribution {
137 area: name.to_owned(),
138 routes_file: module
139 .krate
140 .path
141 .join(format!("src/areas/{name}/routes.rs")),
142 declared: true,
143 routes,
144 screens: Vec::new(),
145 overrides: Vec::new(),
146 menu: None,
147 }
148}
149
150pub fn define_area(module: &mut Module, name: &str) {
151 let path = module.krate.path.join("contract");
152 let contract = krate(&contract_package(&module.krate.package), &path);
153 module.defined_areas.push(AreaDefinition {
154 name: name.to_owned(),
155 type_path: vec![
156 contract.crate_name.clone(),
157 "area".to_owned(),
158 "Member".to_owned(),
159 ],
160 prefix: Some("/account".to_owned()),
161 client: AreaClient::Html,
162 principal: vec!["Account".to_owned()],
163 implements_member_area: true,
164 opens_sign_up: false,
165 file: path.join("src/area.rs"),
166 line: 7,
167 });
168 module.contract = Some(contract);
169}
170
171#[must_use]
172pub fn route(method: Method, path: &str, handler: &str, line: usize) -> Route {
173 let handler = handler.split("::").map(str::to_owned).collect();
174 crate::routes::new_route(method, path.to_owned(), handler, line)
175}
176
177pub fn app(root: impl Into<PathBuf>, modules: Vec<Module>) -> App {
178 let root = root.into();
179 let mut shop = krate("shop", root.join("app"));
180 shop.dependencies = modules
181 .iter()
182 .flat_map(|module| std::iter::once(&module.krate).chain(&module.contract))
183 .map(|krate| dependency(&krate.package))
184 .collect();
185 App {
186 root,
187 app: shop,
188 modules,
189 identity: None,
190 package_events: Vec::new(),
191 packages: Vec::new(),
192 split: crate::Split::default(),
193 structure_ceilings: false,
194 }
195}
196
197#[must_use]
198pub fn type_name(written: &str) -> TypeName {
199 TypeName::of(&syn::parse_str(written).expect("a fixture type parses"))
200}
201
202#[must_use]
203pub fn field(name: &str, ty: &str, line: usize) -> FieldDeclaration {
204 FieldDeclaration {
205 name: name.to_owned(),
206 ty: type_name(ty),
207 constraints: Vec::new(),
208 bounds: Vec::new(),
209 line,
210 }
211}
212
213pub fn function(path: &str, file: impl Into<PathBuf>, line: usize) -> Function {
214 Function {
215 path: path.split("::").map(str::to_owned).collect(),
216 arguments: Vec::new(),
217 output: None,
218 renders: Vec::new(),
219 doc: None,
220 effects: Effects::default(),
221 file: file.into(),
222 line,
223 }
224}
225
226pub fn method(owner: &str, name: &str, file: impl Into<PathBuf>, line: usize) -> TypeMethod {
227 TypeMethod {
228 owner: owner.to_owned(),
229 name: name.to_owned(),
230 implements: None,
231 doc: None,
232 effects: Effects::default(),
233 file: file.into(),
234 line,
235 }
236}
237
238pub fn aggregate(name: &str, fields: Vec<FieldDeclaration>, file: impl Into<PathBuf>) -> Aggregate {
239 Aggregate {
240 name: name.to_owned(),
241 path: vec![name.to_owned()],
242 mirror: None,
243 fields,
244 children: Vec::new(),
245 behaviors: Vec::new(),
246 exposed: Vec::new(),
247 owner: None,
248 owner_on_struct: false,
249 doc: None,
250 file: file.into(),
251 line: 1,
252 }
253}
254
255#[must_use]
256pub fn enumeration(name: &str, variants: &[(&str, &[&str])]) -> EnumDeclaration {
257 EnumDeclaration {
258 name: name.to_owned(),
259 path: vec![name.to_owned()],
260 stored_names: variants
261 .iter()
262 .map(|(variant, _)| stored_name(variant))
263 .collect(),
264 variants: variants
265 .iter()
266 .map(|(variant, to)| EnumVariant {
267 name: (*variant).to_owned(),
268 to: to.iter().map(|target| (*target).to_owned()).collect(),
269 tone: None,
270 })
271 .collect(),
272 }
273}