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 let mut app = sofie_model::load(root).map_err(GenError::Model)?;
89 let mut outcomes = Vec::new();
90 if mode == GenMode::Write {
91 let wired = wire_generated_code(&app)?;
92 if !wired.is_empty() {
93 app = sofie_model::load(root).map_err(GenError::Model)?;
94 outcomes.extend(wired.into_iter().map(|(file, _)| GenOutcome::Written(file)));
95 }
96 }
97 let (screens, languages) = checked_parts(&app)?;
98 outcomes.extend(sync_generated(&app, &screens, languages.as_ref(), mode)?);
99 Ok(outcomes)
100}
101
102pub fn regenerate(root: &Path) -> Result<Vec<String>, GenError> {
103 let outcomes = regenerated(root, GenMode::Write)?;
104 Ok(outcomes.iter().filter_map(GenOutcome::change).collect())
105}
106
107pub fn regenerated(root: &Path, mode: GenMode) -> Result<Vec<GenOutcome>, GenError> {
110 let mut outcomes = gen_app(root, mode)?;
111 outcomes.extend(regenerate_docs(root, mode)?);
112 Ok(outcomes)
113}
114
115#[cfg(feature = "docs")]
116fn regenerate_docs(root: &Path, mode: GenMode) -> Result<Vec<GenOutcome>, GenError> {
117 let app = checked_model(root)?;
118 if !crate::docs::keeps_docs(&app) {
119 return Ok(Vec::new());
120 }
121 crate::docs::sync_docs(&app, mode)
122}
123
124#[cfg(not(feature = "docs"))]
125#[expect(
126 clippy::unnecessary_wraps,
127 reason = "the same signature as with `docs`"
128)]
129fn regenerate_docs(_root: &Path, _mode: GenMode) -> Result<Vec<GenOutcome>, GenError> {
130 Ok(Vec::new())
131}
132
133#[cfg(feature = "docs")]
134pub(crate) fn checked_model(root: &Path) -> Result<App, GenError> {
135 checked(root).map(|(app, _, _)| app)
136}
137
138#[cfg(feature = "docs")]
139fn checked(root: &Path) -> Result<(App, Screens, Option<AppLanguages>), GenError> {
140 let app = sofie_model::load(root).map_err(GenError::Model)?;
141 let (screens, languages) = checked_parts(&app)?;
142 Ok((app, screens, languages))
143}
144
145fn checked_parts(app: &App) -> Result<(Screens, Option<AppLanguages>), GenError> {
146 let screens = Screens::read(&app.modules);
147 let languages = AppLanguages::read(app)?;
148 let mut findings = sofie_check::check(app, &screens);
149 if let Some(languages) = &languages {
150 findings.extend(languages.findings(app, &screens)?);
151 }
152 if !findings.is_empty() {
153 return Err(GenError::Check(findings));
154 }
155 Ok((screens, languages))
156}
157
158pub(crate) fn sync_generated(
159 app: &App,
160 screens: &Screens,
161 languages: Option<&AppLanguages>,
162 mode: GenMode,
163) -> Result<Vec<GenOutcome>, GenError> {
164 let role_migrations = missing_role_migrations(app);
165 let app = &with_role_migrations(app, &role_migrations);
166 let mut files: Vec<(PathBuf, Option<String>)> = role_migrations
167 .into_iter()
168 .map(|(path, text)| (path, Some(text)))
169 .collect();
170 let wiring = render_wiring(app, languages, &described_forms(app, screens)?);
171 let wiring_file = app.wiring_file();
172 files.push((wiring_file.clone(), Some(formatted(&wiring, &wiring_file)?)));
173 for module in &app.modules {
174 let file = module.krate.wiring_file();
175 let text = formatted(&render_module_wiring(app, module), &file)?;
176 files.push((file, Some(text)));
177 }
178 let contract_gens = render_contract_gens(app, languages)
179 .map_err(|error| GenError::App(format!("reading the translation keys: {error}")))?;
180 for (path, text) in contract_gens {
181 let text = text
182 .as_deref()
183 .map(|text| formatted(text, &path))
184 .transpose()?;
185 files.push((path, text));
186 }
187 files.extend(render_env_examples(app)?);
188 #[cfg(feature = "typescript")]
189 files.push(crate::islands::types_file(app));
190 files.push(render_agents_md(app)?);
191 let mut outcomes = Vec::new();
192 for (path, text) in files {
193 outcomes.extend(sync_file(&path, text, mode)?);
194 }
195 Ok(outcomes)
196}
197
198fn formatted(rust: &str, file: &Path) -> Result<String, GenError> {
199 format_rust(rust, file).map_err(GenError::Rustfmt)
200}
201
202pub(crate) fn sync_file(
203 path: &Path,
204 text: Option<String>,
205 mode: GenMode,
206) -> Result<Option<GenOutcome>, GenError> {
207 let existing = read_existing(path)?;
208 let Some(text) = text else {
209 return remove_generated(path, existing.as_deref(), mode);
210 };
211 if existing.as_deref() == Some(text.as_str()) {
212 return Ok(Some(GenOutcome::UpToDate(path.to_path_buf())));
213 }
214 if mode == GenMode::Check {
215 return Ok(Some(GenOutcome::Stale(path.to_path_buf())));
216 }
217 write(path, &text)?;
218 Ok(Some(GenOutcome::Written(path.to_path_buf())))
219}
220
221fn remove_generated(
223 path: &Path,
224 existing: Option<&str>,
225 mode: GenMode,
226) -> Result<Option<GenOutcome>, GenError> {
227 if !existing.is_some_and(is_generated) {
228 return Ok(None);
229 }
230 if mode == GenMode::Check {
231 return Ok(Some(GenOutcome::Stale(path.to_path_buf())));
232 }
233 fs::remove_file(path).map_err(|source| io_error(path, source))?;
234 Ok(Some(GenOutcome::Removed(path.to_path_buf())))
235}
236
237fn is_generated(text: &str) -> bool {
238 #[cfg(feature = "docs")]
239 if text.starts_with(crate::docs::DOCS_HEADER) {
240 return true;
241 }
242 text.starts_with(HEADER) || text.starts_with(ENV_HEADER)
243}
244
245pub(crate) fn read_existing(path: &Path) -> Result<Option<String>, GenError> {
246 match fs::read_to_string(path) {
247 Ok(text) => Ok(Some(text)),
248 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
249 Err(source) => Err(io_error(path, source)),
250 }
251}
252
253pub(crate) fn readable_screen<'s>(
255 screens: &'s Screens,
256 file: &Path,
257) -> Result<&'s Screen, GenError> {
258 screens
259 .screen(file)
260 .as_ref()
261 .map_err(|error| io_error(file, io::Error::new(error.kind(), error.to_string())))
262}
263
264pub(crate) fn io_error(path: &Path, source: io::Error) -> GenError {
265 GenError::Io {
266 path: path.to_path_buf(),
267 source,
268 }
269}