1use std::fmt;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5
6use sofie_check::{Finding, Screen, Screens};
7use sofie_model::{App, ModelError};
8
9use crate::agents_md::render_agents_md;
10use crate::contract_gen::render_contract_gens;
11use crate::env_example::{ENV_HEADER, render_env_examples};
12use crate::fix_command::wire_generated_code;
13use crate::forms::described_forms;
14use crate::module_roles::{missing_role_migrations, with_role_migrations};
15use crate::rustfmt::format_rust;
16use crate::source_edits::write;
17use crate::translations::AppLanguages;
18use crate::wiring::{HEADER, render_module_wiring, render_wiring};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum GenMode {
22 Write,
23 Check,
24}
25
26#[derive(Debug, PartialEq, Eq)]
27pub enum GenOutcome {
28 Written(PathBuf),
29 Removed(PathBuf),
30 UpToDate(PathBuf),
31 Stale(PathBuf),
32}
33
34#[derive(Debug)]
35pub enum GenError {
36 Model(ModelError),
37 Check(Vec<Finding>),
38 Rustfmt(String),
40 Io {
41 path: PathBuf,
42 source: io::Error,
43 },
44 Screen {
45 path: PathBuf,
46 message: String,
47 },
48 App(String),
49 Build(String),
51 Usage(String),
52}
53
54impl fmt::Display for GenError {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Model(error) => write!(formatter, "{error}"),
58 Self::Check(findings) => {
59 let rendered: Vec<String> = findings.iter().map(ToString::to_string).collect();
60 write!(formatter, "{}", rendered.join("\n"))
61 }
62 Self::Rustfmt(message)
63 | Self::App(message)
64 | Self::Build(message)
65 | Self::Usage(message) => {
66 write!(formatter, "{message}")
67 }
68 Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
69 Self::Screen { path, message } => write!(formatter, "{}: {message}", path.display()),
70 }
71 }
72}
73
74impl std::error::Error for GenError {}
75
76impl GenOutcome {
77 pub(crate) fn change(&self) -> Option<String> {
78 match self {
79 Self::Written(path) => Some(format!("wrote {}", path.display())),
80 Self::Removed(path) => Some(format!("removed {}", path.display())),
81 Self::UpToDate(_) | Self::Stale(_) => None,
82 }
83 }
84}
85
86pub fn gen_app(root: &Path, mode: GenMode) -> Result<Vec<GenOutcome>, GenError> {
88 generated_app(root, mode).map(|(_, outcomes)| outcomes)
89}
90
91fn generated_app(root: &Path, mode: GenMode) -> Result<(App, Vec<GenOutcome>), GenError> {
93 let mut app = sofie_model::load(root).map_err(GenError::Model)?;
94 let mut outcomes = Vec::new();
95 if mode == GenMode::Write {
96 let wired = wire_generated_code(&app)?;
97 if !wired.is_empty() {
98 app = sofie_model::load(root).map_err(GenError::Model)?;
99 outcomes.extend(wired.into_iter().map(|(file, _)| GenOutcome::Written(file)));
100 }
101 }
102 let (screens, languages) = checked_parts(&app)?;
103 outcomes.extend(sync_generated(&app, &screens, languages.as_ref(), mode)?);
104 Ok((app, outcomes))
105}
106
107pub fn regenerate(root: &Path) -> Result<Vec<String>, GenError> {
108 let outcomes = regenerated(root, GenMode::Write)?;
109 Ok(outcomes.iter().filter_map(GenOutcome::change).collect())
110}
111
112pub fn regenerated(root: &Path, mode: GenMode) -> Result<Vec<GenOutcome>, GenError> {
115 let (app, mut outcomes) = generated_app(root, mode)?;
116 let docs = regenerate_docs(&app, &outcomes, mode)?;
117 outcomes.extend(docs);
118 Ok(outcomes)
119}
120
121#[cfg(feature = "docs")]
124fn regenerate_docs(
125 app: &App,
126 outcomes: &[GenOutcome],
127 mode: GenMode,
128) -> Result<Vec<GenOutcome>, GenError> {
129 if !crate::docs::keeps_docs(app) {
130 return Ok(Vec::new());
131 }
132 if !outcomes.iter().any(|outcome| outcome.change().is_some()) {
133 return crate::docs::sync_docs(app, mode);
134 }
135 let reloaded = sofie_model::load(&app.root).map_err(GenError::Model)?;
136 crate::docs::sync_docs(&reloaded, mode)
137}
138
139#[cfg(not(feature = "docs"))]
140#[expect(
141 clippy::unnecessary_wraps,
142 reason = "the same signature as with `docs`"
143)]
144fn regenerate_docs(
145 _app: &App,
146 _outcomes: &[GenOutcome],
147 _mode: GenMode,
148) -> Result<Vec<GenOutcome>, GenError> {
149 Ok(Vec::new())
150}
151
152#[cfg(feature = "docs")]
153pub(crate) fn checked_model(root: &Path) -> Result<App, GenError> {
154 checked(root).map(|(app, _, _)| app)
155}
156
157#[cfg(feature = "docs")]
158fn checked(root: &Path) -> Result<(App, Screens, Option<AppLanguages>), GenError> {
159 let app = sofie_model::load(root).map_err(GenError::Model)?;
160 let (screens, languages) = checked_parts(&app)?;
161 Ok((app, screens, languages))
162}
163
164fn checked_parts(app: &App) -> Result<(Screens, Option<AppLanguages>), GenError> {
165 let screens = Screens::read(&app.modules);
166 let languages = AppLanguages::read(app)?;
167 let mut findings = sofie_check::check(app, &screens);
168 if let Some(languages) = &languages {
169 findings.extend(languages.findings(app, &screens)?);
170 }
171 if !findings.is_empty() {
172 return Err(GenError::Check(findings));
173 }
174 Ok((screens, languages))
175}
176
177pub(crate) fn sync_generated(
178 app: &App,
179 screens: &Screens,
180 languages: Option<&AppLanguages>,
181 mode: GenMode,
182) -> Result<Vec<GenOutcome>, GenError> {
183 let role_migrations = missing_role_migrations(app);
184 let app = &with_role_migrations(app, &role_migrations);
185 let mut files: Vec<(PathBuf, Option<String>)> = role_migrations
186 .into_iter()
187 .map(|(path, text)| (path, Some(text)))
188 .collect();
189 let wiring = render_wiring(app, languages, &described_forms(app, screens)?);
190 let wiring_file = app.wiring_file();
191 files.push((wiring_file.clone(), Some(formatted(&wiring, &wiring_file)?)));
192 for module in &app.modules {
193 let file = module.krate.wiring_file();
194 let text = formatted(&render_module_wiring(app, module), &file)?;
195 files.push((file, Some(text)));
196 }
197 let contract_gens = render_contract_gens(app, languages)
198 .map_err(|error| GenError::App(format!("reading the translation keys: {error}")))?;
199 for (path, text) in contract_gens {
200 let text = text
201 .as_deref()
202 .map(|text| formatted(text, &path))
203 .transpose()?;
204 files.push((path, text));
205 }
206 files.extend(render_env_examples(app)?);
207 #[cfg(feature = "typescript")]
208 files.push(crate::islands::types_file(app));
209 files.push(render_agents_md(app)?);
210 let mut outcomes = Vec::new();
211 for (path, text) in files {
212 outcomes.extend(sync_file(&path, text, mode)?);
213 }
214 Ok(outcomes)
215}
216
217fn formatted(rust: &str, file: &Path) -> Result<String, GenError> {
218 format_rust(rust, file).map_err(GenError::Rustfmt)
219}
220
221pub(crate) fn sync_file(
222 path: &Path,
223 text: Option<String>,
224 mode: GenMode,
225) -> Result<Option<GenOutcome>, GenError> {
226 let existing = read_existing(path)?;
227 let Some(text) = text else {
228 return remove_generated(path, existing.as_deref(), mode);
229 };
230 if existing.as_deref() == Some(text.as_str()) {
231 return Ok(Some(GenOutcome::UpToDate(path.to_path_buf())));
232 }
233 if mode == GenMode::Check {
234 return Ok(Some(GenOutcome::Stale(path.to_path_buf())));
235 }
236 write(path, &text)?;
237 Ok(Some(GenOutcome::Written(path.to_path_buf())))
238}
239
240fn remove_generated(
242 path: &Path,
243 existing: Option<&str>,
244 mode: GenMode,
245) -> Result<Option<GenOutcome>, GenError> {
246 if !existing.is_some_and(is_generated) {
247 return Ok(None);
248 }
249 if mode == GenMode::Check {
250 return Ok(Some(GenOutcome::Stale(path.to_path_buf())));
251 }
252 fs::remove_file(path).map_err(|source| io_error(path, source))?;
253 Ok(Some(GenOutcome::Removed(path.to_path_buf())))
254}
255
256fn is_generated(text: &str) -> bool {
257 #[cfg(feature = "docs")]
258 if text.starts_with(crate::docs::DOCS_HEADER) {
259 return true;
260 }
261 text.starts_with(HEADER) || text.starts_with(ENV_HEADER)
262}
263
264pub(crate) fn read_existing(path: &Path) -> Result<Option<String>, GenError> {
265 match fs::read_to_string(path) {
266 Ok(text) => Ok(Some(text)),
267 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
268 Err(source) => Err(io_error(path, source)),
269 }
270}
271
272pub(crate) fn readable_screen<'s>(
274 screens: &'s Screens,
275 file: &Path,
276) -> Result<&'s Screen, GenError> {
277 screens
278 .screen(file)
279 .as_ref()
280 .map_err(|error| io_error(file, io::Error::new(error.kind(), error.to_string())))
281}
282
283pub(crate) fn io_error(path: &Path, source: io::Error) -> GenError {
284 GenError::Io {
285 path: path.to_path_buf(),
286 source,
287 }
288}