Skip to main content

es_fluent_cli/commands/
generate.rs

1//! Generate command implementation.
2
3use crate::commands::{
4    GenerationVerb, WorkspaceArgs, WorkspaceCrates, parallel_generate,
5    render_generation_results_with_dry_run,
6};
7use crate::core::{CliError, FluentParseMode, GenerationAction};
8use crate::utils::ui;
9use clap::Parser;
10
11/// Arguments for the generate command.
12#[derive(Parser)]
13pub struct GenerateArgs {
14    #[command(flatten)]
15    pub workspace: WorkspaceArgs,
16
17    /// Parse mode for FTL generation
18    #[arg(short, long, value_enum, default_value_t = FluentParseMode::default())]
19    pub mode: FluentParseMode,
20
21    /// Dry run - show what would be generated without making changes.
22    #[arg(long)]
23    pub dry_run: bool,
24
25    /// Force rebuild of the runner, ignoring the staleness cache.
26    #[arg(long)]
27    pub force_run: bool,
28}
29
30/// Run the generate command.
31pub fn run_generate(args: GenerateArgs) -> Result<(), CliError> {
32    let workspace = WorkspaceCrates::discover(args.workspace)?;
33
34    if !workspace.print_discovery(ui::print_header) {
35        return Ok(());
36    }
37
38    let results = parallel_generate(
39        &workspace.workspace_info,
40        &workspace.valid,
41        &GenerationAction::Generate {
42            mode: args.mode,
43            dry_run: args.dry_run,
44        },
45        args.force_run,
46    );
47    let has_errors =
48        render_generation_results_with_dry_run(&results, args.dry_run, GenerationVerb::Generate);
49
50    if has_errors {
51        std::process::exit(1);
52    }
53
54    Ok(())
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::generation::cache::{RunnerCache, compute_content_hash};
61    use std::fs;
62    use std::time::SystemTime;
63    use tempfile::tempdir;
64
65    fn create_test_crate_workspace() -> tempfile::TempDir {
66        let temp = tempdir().unwrap();
67
68        fs::create_dir_all(temp.path().join("src")).unwrap();
69        fs::create_dir_all(temp.path().join("i18n/en")).unwrap();
70        fs::write(
71            temp.path().join("Cargo.toml"),
72            r#"[package]
73name = "test-app"
74version = "0.1.0"
75edition = "2024"
76"#,
77        )
78        .unwrap();
79        fs::write(temp.path().join("src/lib.rs"), "pub struct Demo;\n").unwrap();
80        fs::write(
81            temp.path().join("i18n.toml"),
82            "fallback_language = \"en\"\nassets_dir = \"i18n\"\n",
83        )
84        .unwrap();
85        fs::write(temp.path().join("i18n/en/test-app.ftl"), "hello = Hello\n").unwrap();
86
87        temp
88    }
89
90    #[cfg(unix)]
91    fn set_executable(path: &std::path::Path) {
92        use std::os::unix::fs::PermissionsExt;
93        let mut perms = fs::metadata(path).expect("metadata").permissions();
94        perms.set_mode(0o755);
95        fs::set_permissions(path, perms).expect("set permissions");
96    }
97
98    #[cfg(not(unix))]
99    fn set_executable(_path: &std::path::Path) {}
100
101    fn setup_fake_runner_and_cache(temp: &tempfile::TempDir) {
102        let binary_path = temp.path().join("target/debug/es-fluent-runner");
103        fs::create_dir_all(binary_path.parent().unwrap()).expect("create target/debug");
104        fs::write(&binary_path, "#!/bin/sh\necho generated\n").expect("write runner");
105        set_executable(&binary_path);
106
107        let src_dir = temp.path().join("src");
108        let i18n_toml = temp.path().join("i18n.toml");
109        let hash = compute_content_hash(&src_dir, Some(&i18n_toml));
110        let mtime = fs::metadata(&binary_path)
111            .and_then(|m| m.modified())
112            .expect("runner mtime")
113            .duration_since(SystemTime::UNIX_EPOCH)
114            .expect("mtime duration")
115            .as_secs();
116
117        let temp_dir = es_fluent_derive_core::get_es_fluent_temp_dir(temp.path());
118        fs::create_dir_all(&temp_dir).expect("create temp dir");
119        let mut crate_hashes = indexmap::IndexMap::new();
120        crate_hashes.insert("test-app".to_string(), hash);
121        RunnerCache {
122            crate_hashes,
123            runner_mtime: mtime,
124            cli_version: env!("CARGO_PKG_VERSION").to_string(),
125        }
126        .save(&temp_dir)
127        .expect("save runner cache");
128    }
129
130    #[test]
131    fn run_generate_returns_ok_when_package_filter_matches_nothing() {
132        let temp = create_test_crate_workspace();
133        let result = run_generate(GenerateArgs {
134            workspace: WorkspaceArgs {
135                path: Some(temp.path().to_path_buf()),
136                package: Some("missing-crate".to_string()),
137            },
138            mode: FluentParseMode::default(),
139            dry_run: false,
140            force_run: false,
141        });
142
143        assert!(result.is_ok());
144    }
145
146    #[test]
147    fn run_generate_executes_with_fake_runner() {
148        let temp = create_test_crate_workspace();
149        setup_fake_runner_and_cache(&temp);
150
151        let result = run_generate(GenerateArgs {
152            workspace: WorkspaceArgs {
153                path: Some(temp.path().to_path_buf()),
154                package: None,
155            },
156            mode: FluentParseMode::default(),
157            dry_run: false,
158            force_run: false,
159        });
160
161        assert!(result.is_ok());
162    }
163}