es_fluent_cli_helpers/
generate.rs1use es_fluent::registry::FtlTypeInfo;
4use std::path::{Path, PathBuf};
5
6pub use es_fluent_generate::FluentParseMode;
7pub use es_fluent_generate::error::FluentGenerateError;
8
9#[derive(Debug, thiserror::Error)]
11pub enum GeneratorError {
12 #[error("Configuration error: {0}")]
14 Config(#[from] es_fluent_toml::I18nConfigError),
15
16 #[error("Failed to detect crate name: {0}")]
18 CrateName(String),
19
20 #[error("Generation error: {0}")]
22 Generate(#[from] FluentGenerateError),
23
24 #[error(
26 "Invalid namespace '{namespace}' for type '{type_name}'. Allowed namespaces: {allowed:?}"
27 )]
28 InvalidNamespace {
29 namespace: String,
30 type_name: String,
31 allowed: Vec<String>,
32 },
33}
34
35#[derive(bon::Builder)]
40pub struct EsFluentGenerator {
41 #[builder(default)]
44 mode: FluentParseMode,
45
46 #[builder(into)]
48 crate_name: Option<String>,
49
50 #[builder(into)]
52 output_path: Option<PathBuf>,
53
54 #[builder(into)]
56 assets_dir: Option<PathBuf>,
57
58 #[builder(into)]
60 manifest_dir: Option<PathBuf>,
61
62 #[builder(default)]
64 dry_run: bool,
65}
66
67#[derive(clap::Parser)]
69pub struct GeneratorArgs {
70 #[command(subcommand)]
71 action: Action,
72}
73
74#[derive(clap::Subcommand)]
75enum Action {
76 Generate {
78 #[arg(long, default_value_t = FluentParseMode::default())]
80 mode: FluentParseMode,
81 #[arg(long)]
83 dry_run: bool,
84 },
85 Clean {
87 #[arg(long)]
89 all: bool,
90 #[arg(long)]
92 dry_run: bool,
93 },
94}
95
96impl EsFluentGenerator {
97 pub fn run_cli(self) -> Result<bool, GeneratorError> {
99 use clap::Parser;
100 let args = GeneratorArgs::parse();
101
102 match args.action {
103 Action::Generate { mode, dry_run } => {
104 let mut generator = self;
105 generator.mode = mode;
106 generator.dry_run = dry_run;
107 generator.generate()
108 },
109 Action::Clean { all, dry_run } => self.clean(all, dry_run),
110 }
111 }
112
113 fn resolve_crate_name(&self) -> Result<String, GeneratorError> {
117 self.crate_name
118 .clone()
119 .map_or_else(Self::detect_crate_name, Ok)
120 }
121
122 fn resolve_output_path(&self) -> Result<PathBuf, GeneratorError> {
124 if let Some(path) = &self.output_path {
125 return Ok(path.clone());
126 }
127 let manifest_dir = self.resolve_manifest_dir()?;
128 Ok(es_fluent_toml::I18nConfig::output_dir_from_manifest_dir(
129 &manifest_dir,
130 )?)
131 }
132
133 fn resolve_assets_dir(&self) -> Result<PathBuf, GeneratorError> {
135 if let Some(path) = &self.assets_dir {
136 return Ok(path.clone());
137 }
138 let manifest_dir = self.resolve_manifest_dir()?;
139 Ok(es_fluent_toml::I18nConfig::assets_dir_from_manifest_dir(
140 &manifest_dir,
141 )?)
142 }
143
144 fn resolve_manifest_dir(&self) -> Result<PathBuf, GeneratorError> {
146 if let Some(path) = &self.manifest_dir {
147 return Ok(path.clone());
148 }
149
150 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
151 .map_err(|_| GeneratorError::CrateName("CARGO_MANIFEST_DIR not set".to_string()))?;
152 Ok(PathBuf::from(manifest_dir))
153 }
154
155 fn resolve_clean_paths(&self, all_locales: bool) -> Result<Vec<PathBuf>, GeneratorError> {
157 if !all_locales {
158 return Ok(vec![self.resolve_output_path()?]);
159 }
160
161 let assets_dir = self.resolve_assets_dir()?;
162 let mut paths: Vec<PathBuf> = std::fs::read_dir(&assets_dir)
163 .ok()
164 .map(|entries| {
165 entries
166 .filter_map(|e| e.ok())
167 .filter(|e| e.path().is_dir())
168 .map(|e| e.path())
169 .collect()
170 })
171 .unwrap_or_else(|| self.output_path.clone().into_iter().collect());
172
173 paths.sort();
175
176 Ok(paths)
177 }
178
179 pub fn generate(&self) -> Result<bool, GeneratorError> {
181 let crate_name = self.resolve_crate_name()?;
182 let output_path = self.resolve_output_path()?;
183 let manifest_dir = self.resolve_manifest_dir()?;
184 let type_infos = collect_type_infos(&crate_name);
185
186 self.validate_namespaces(&type_infos, &manifest_dir)?;
188
189 tracing::info!(
190 "Generating FTL files for {} types in crate '{}'",
191 type_infos.len(),
192 crate_name
193 );
194
195 let changed = es_fluent_generate::generate(
196 &crate_name,
197 output_path,
198 &manifest_dir,
199 &type_infos,
200 self.mode.clone(),
201 self.dry_run,
202 )?;
203
204 Ok(changed)
205 }
206
207 fn validate_namespaces(
209 &self,
210 type_infos: &[&'static FtlTypeInfo],
211 manifest_dir: &Path,
212 ) -> Result<(), GeneratorError> {
213 let config = es_fluent_toml::I18nConfig::from_manifest_dir(manifest_dir).ok();
214 let allowed = config.as_ref().and_then(|c| c.namespaces.as_ref());
215
216 if let Some(allowed_namespaces) = allowed {
217 for info in type_infos {
218 if let Some(ns) = info.resolved_namespace(manifest_dir)
219 && !allowed_namespaces.contains(&ns)
220 {
221 return Err(GeneratorError::InvalidNamespace {
222 namespace: ns,
223 type_name: info.type_name.to_string(),
224 allowed: allowed_namespaces.clone(),
225 });
226 }
227 }
228 }
229
230 Ok(())
231 }
232
233 pub fn clean(&self, all_locales: bool, dry_run: bool) -> Result<bool, GeneratorError> {
235 let crate_name = self.resolve_crate_name()?;
236 let paths = self.resolve_clean_paths(all_locales)?;
237 let manifest_dir = self.resolve_manifest_dir()?;
238 let type_infos = collect_type_infos(&crate_name);
239
240 let mut any_changed = false;
241 for output_path in paths {
242 if !dry_run {
243 tracing::info!(
244 "Cleaning FTL files for {} types in crate '{}' at {}",
245 type_infos.len(),
246 crate_name,
247 output_path.display()
248 );
249 }
250
251 if es_fluent_generate::clean::clean(
252 &crate_name,
253 output_path,
254 &manifest_dir,
255 &type_infos,
256 dry_run,
257 )? {
258 any_changed = true;
259 }
260 }
261
262 Ok(any_changed)
263 }
264
265 fn detect_crate_name() -> Result<String, GeneratorError> {
267 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
268 .map_err(|_| GeneratorError::CrateName("CARGO_MANIFEST_DIR not set".to_string()))?;
269 let manifest_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
270
271 cargo_metadata::MetadataCommand::new()
272 .exec()
273 .ok()
274 .and_then(|metadata| {
275 metadata
276 .packages
277 .iter()
278 .find(|pkg| pkg.manifest_path == manifest_path)
279 .map(|pkg| pkg.name.to_string())
280 })
281 .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
282 .ok_or_else(|| GeneratorError::CrateName("Could not determine crate name".to_string()))
283 }
284}
285
286fn collect_type_infos(crate_name: &str) -> Vec<&'static FtlTypeInfo> {
288 let crate_ident = crate_name.replace('-', "_");
289 es_fluent::registry::get_all_ftl_type_infos()
290 .filter(|info| {
291 info.module_path == crate_ident
292 || info.module_path.starts_with(&format!("{}::", crate_ident))
293 })
294 .collect()
295}