es_fluent_cli_helpers/
generate.rs1use es_fluent::registry::FtlTypeInfo;
4use std::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(default)]
60 dry_run: bool,
61}
62
63#[derive(clap::Parser)]
65pub struct GeneratorArgs {
66 #[command(subcommand)]
67 action: Action,
68}
69
70#[derive(clap::Subcommand)]
71enum Action {
72 Generate {
74 #[arg(long, default_value_t = FluentParseMode::default())]
76 mode: FluentParseMode,
77 #[arg(long)]
79 dry_run: bool,
80 },
81 Clean {
83 #[arg(long)]
85 all: bool,
86 #[arg(long)]
88 dry_run: bool,
89 },
90}
91
92impl EsFluentGenerator {
93 pub fn run_cli(self) -> Result<bool, GeneratorError> {
95 use clap::Parser;
96 let args = GeneratorArgs::parse();
97
98 match args.action {
99 Action::Generate { mode, dry_run } => {
100 let mut generator = self;
101 generator.mode = mode;
102 generator.dry_run = dry_run;
103 generator.generate()
104 },
105 Action::Clean { all, dry_run } => self.clean(all, dry_run),
106 }
107 }
108
109 fn resolve_crate_name(&self) -> Result<String, GeneratorError> {
113 self.crate_name
114 .clone()
115 .map_or_else(Self::detect_crate_name, Ok)
116 }
117
118 fn resolve_output_path(&self) -> Result<PathBuf, GeneratorError> {
120 if let Some(path) = &self.output_path {
121 return Ok(path.clone());
122 }
123 let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()?;
124 Ok(config.assets_dir.join(&config.fallback_language))
125 }
126
127 fn resolve_assets_dir(&self) -> Result<PathBuf, GeneratorError> {
129 if let Some(path) = &self.assets_dir {
130 return Ok(path.clone());
131 }
132 let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()?;
133 Ok(config.assets_dir)
134 }
135
136 fn resolve_clean_paths(&self, all_locales: bool) -> Result<Vec<PathBuf>, GeneratorError> {
138 if !all_locales {
139 return Ok(vec![self.resolve_output_path()?]);
140 }
141
142 let assets_dir = self.resolve_assets_dir()?;
143 let mut paths: Vec<PathBuf> = std::fs::read_dir(&assets_dir)
144 .ok()
145 .map(|entries| {
146 entries
147 .filter_map(|e| e.ok())
148 .filter(|e| e.path().is_dir())
149 .map(|e| e.path())
150 .collect()
151 })
152 .unwrap_or_else(|| self.output_path.clone().into_iter().collect());
153
154 paths.sort();
156
157 Ok(paths)
158 }
159
160 pub fn generate(&self) -> Result<bool, GeneratorError> {
162 let crate_name = self.resolve_crate_name()?;
163 let output_path = self.resolve_output_path()?;
164 let type_infos = collect_type_infos(&crate_name);
165
166 self.validate_namespaces(&type_infos)?;
168
169 tracing::info!(
170 "Generating FTL files for {} types in crate '{}'",
171 type_infos.len(),
172 crate_name
173 );
174
175 let changed = es_fluent_generate::generate(
176 &crate_name,
177 output_path,
178 &type_infos,
179 self.mode.clone(),
180 self.dry_run,
181 )?;
182
183 Ok(changed)
184 }
185
186 fn validate_namespaces(
188 &self,
189 type_infos: &[&'static FtlTypeInfo],
190 ) -> Result<(), GeneratorError> {
191 let config = es_fluent_toml::I18nConfig::read_from_manifest_dir().ok();
192 let allowed = config.as_ref().and_then(|c| c.namespaces.as_ref());
193
194 if let Some(allowed_namespaces) = allowed {
195 for info in type_infos {
196 if let Some(ns) = info.namespace
197 && !allowed_namespaces.contains(&ns.to_string())
198 {
199 return Err(GeneratorError::InvalidNamespace {
200 namespace: ns.to_string(),
201 type_name: info.type_name.to_string(),
202 allowed: allowed_namespaces.clone(),
203 });
204 }
205 }
206 }
207
208 Ok(())
209 }
210
211 pub fn clean(&self, all_locales: bool, dry_run: bool) -> Result<bool, GeneratorError> {
213 let crate_name = self.resolve_crate_name()?;
214 let paths = self.resolve_clean_paths(all_locales)?;
215 let type_infos = collect_type_infos(&crate_name);
216
217 let mut any_changed = false;
218 for output_path in paths {
219 if !dry_run {
220 tracing::info!(
221 "Cleaning FTL files for {} types in crate '{}' at {}",
222 type_infos.len(),
223 crate_name,
224 output_path.display()
225 );
226 }
227
228 if es_fluent_generate::clean::clean(&crate_name, output_path, &type_infos, dry_run)? {
229 any_changed = true;
230 }
231 }
232
233 Ok(any_changed)
234 }
235
236 fn detect_crate_name() -> Result<String, GeneratorError> {
238 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
239 .map_err(|_| GeneratorError::CrateName("CARGO_MANIFEST_DIR not set".to_string()))?;
240 let manifest_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
241
242 cargo_metadata::MetadataCommand::new()
243 .exec()
244 .ok()
245 .and_then(|metadata| {
246 metadata
247 .packages
248 .iter()
249 .find(|pkg| pkg.manifest_path == manifest_path)
250 .map(|pkg| pkg.name.to_string())
251 })
252 .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
253 .ok_or_else(|| GeneratorError::CrateName("Could not determine crate name".to_string()))
254 }
255}
256
257fn collect_type_infos(crate_name: &str) -> Vec<&'static FtlTypeInfo> {
259 let crate_ident = crate_name.replace('-', "_");
260 es_fluent::registry::get_all_ftl_type_infos()
261 .filter(|info| {
262 info.module_path == crate_ident
263 || info.module_path.starts_with(&format!("{}::", crate_ident))
264 })
265 .collect()
266}