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 code: crate::Code::default(),
130 }
131}
132
133#[must_use]
134pub fn area(module: &Module, name: &str, routes: Vec<Route>) -> AreaContribution {
135 AreaContribution {
136 area: name.to_owned(),
137 routes_file: module
138 .krate
139 .path
140 .join(format!("src/areas/{name}/routes.rs")),
141 declared: true,
142 routes,
143 screens: Vec::new(),
144 overrides: Vec::new(),
145 menu: None,
146 }
147}
148
149pub fn define_area(module: &mut Module, name: &str) {
150 let path = module.krate.path.join("contract");
151 let contract = krate(&contract_package(&module.krate.package), &path);
152 module.defined_areas.push(AreaDefinition {
153 name: name.to_owned(),
154 type_path: vec![
155 contract.crate_name.clone(),
156 "area".to_owned(),
157 "Member".to_owned(),
158 ],
159 prefix: Some("/account".to_owned()),
160 client: AreaClient::Html,
161 principal: vec!["Account".to_owned()],
162 implements_member_area: true,
163 opens_sign_up: false,
164 file: path.join("src/area.rs"),
165 line: 7,
166 });
167 module.contract = Some(contract);
168}
169
170#[must_use]
171pub fn route(method: Method, path: &str, handler: &str, line: usize) -> Route {
172 let handler = handler.split("::").map(str::to_owned).collect();
173 crate::routes::new_route(method, path.to_owned(), handler, line)
174}
175
176pub fn app(root: impl Into<PathBuf>, modules: Vec<Module>) -> App {
177 let root = root.into();
178 let mut shop = krate("shop", root.join("app"));
179 shop.dependencies = modules
180 .iter()
181 .flat_map(|module| std::iter::once(&module.krate).chain(&module.contract))
182 .map(|krate| dependency(&krate.package))
183 .collect();
184 App {
185 root,
186 app: shop,
187 modules,
188 identity: None,
189 package_events: Vec::new(),
190 packages: Vec::new(),
191 split: crate::Split::default(),
192 structure_ceilings: false,
193 }
194}
195
196#[must_use]
197pub fn type_name(written: &str) -> TypeName {
198 TypeName::of(&syn::parse_str(written).expect("a fixture type parses"))
199}
200
201#[must_use]
202pub fn field(name: &str, ty: &str, line: usize) -> FieldDeclaration {
203 FieldDeclaration {
204 name: name.to_owned(),
205 ty: type_name(ty),
206 constraints: Vec::new(),
207 bounds: Vec::new(),
208 line,
209 }
210}
211
212pub fn function(path: &str, file: impl Into<PathBuf>, line: usize) -> Function {
213 Function {
214 path: path.split("::").map(str::to_owned).collect(),
215 arguments: Vec::new(),
216 output: None,
217 renders: Vec::new(),
218 doc: None,
219 effects: Effects::default(),
220 file: file.into(),
221 line,
222 }
223}
224
225pub fn method(owner: &str, name: &str, file: impl Into<PathBuf>, line: usize) -> TypeMethod {
226 TypeMethod {
227 owner: owner.to_owned(),
228 name: name.to_owned(),
229 implements: None,
230 doc: None,
231 effects: Effects::default(),
232 file: file.into(),
233 line,
234 }
235}
236
237pub fn aggregate(name: &str, fields: Vec<FieldDeclaration>, file: impl Into<PathBuf>) -> Aggregate {
238 Aggregate {
239 name: name.to_owned(),
240 path: vec![name.to_owned()],
241 mirror: None,
242 fields,
243 children: Vec::new(),
244 behaviors: Vec::new(),
245 exposed: Vec::new(),
246 owner: None,
247 owner_on_struct: false,
248 doc: None,
249 file: file.into(),
250 line: 1,
251 }
252}
253
254#[must_use]
255pub fn enumeration(name: &str, variants: &[(&str, &[&str])]) -> EnumDeclaration {
256 EnumDeclaration {
257 name: name.to_owned(),
258 path: vec![name.to_owned()],
259 stored_names: variants
260 .iter()
261 .map(|(variant, _)| stored_name(variant))
262 .collect(),
263 variants: variants
264 .iter()
265 .map(|(variant, to)| EnumVariant {
266 name: (*variant).to_owned(),
267 to: to.iter().map(|target| (*target).to_owned()).collect(),
268 tone: None,
269 })
270 .collect(),
271 }
272}