nimbus_fml/command_line/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5mod cli;
6pub(crate) mod commands;
7mod workflows;
8
9use crate::intermediate_representation::TargetLanguage;
10use crate::util::loaders::LoaderConfig;
11use anyhow::Result;
12use clap::Parser;
13use commands::{
14    CliCmd, GenerateExperimenterManifestCmd, GenerateSingleFileManifestCmd, GenerateStructCmd,
15    PrintChannelsCmd, ValidateCmd,
16};
17
18use std::{collections::BTreeMap, ffi::OsString, path::Path};
19
20use self::commands::PrintInfoCmd;
21
22const RELEASE_CHANNEL: &str = "release";
23
24pub fn do_main<I, T>(args: I, cwd: &Path) -> Result<()>
25where
26    I: IntoIterator<Item = T>,
27    T: Into<OsString> + Clone,
28{
29    let cmd = get_command_from_cli(args, cwd)?;
30    process_command(&cmd)
31}
32
33fn process_command(cmd: &CliCmd) -> Result<()> {
34    match cmd {
35        CliCmd::Generate(params) => workflows::generate_struct(params)?,
36        CliCmd::GenerateExperimenter(params) => workflows::generate_experimenter_manifest(params)?,
37        CliCmd::GenerateSingleFileManifest(params) => {
38            workflows::generate_single_file_manifest(params)?
39        }
40        CliCmd::FetchFile(files, nm) => workflows::fetch_file(files, nm)?,
41        CliCmd::Validate(params) => workflows::validate(params)?,
42        CliCmd::PrintChannels(params) => workflows::print_channels(params)?,
43        CliCmd::PrintInfo(params) => workflows::print_info(params)?,
44    };
45    Ok(())
46}
47
48fn get_command_from_cli<I, T>(args: I, cwd: &Path) -> Result<CliCmd>
49where
50    I: IntoIterator<Item = T>,
51    T: Into<OsString> + Clone,
52{
53    let app = cli::App::parse_from(args);
54
55    Ok(match app.subcommand {
56        cli::Command::Generate(cmd) => {
57            CliCmd::Generate(create_generate_command_from_cli(&cmd, cwd)?)
58        }
59        cli::Command::GenerateExperimenter(cmd) => {
60            CliCmd::GenerateExperimenter(create_generate_command_experimenter_from_cli(&cmd, cwd)?)
61        }
62        cli::Command::Fetch(cmd) => {
63            CliCmd::FetchFile(create_loader(&cmd.input, &cmd.loader_info, cwd)?, cmd.input)
64        }
65        cli::Command::SingleFile(cmd) => {
66            CliCmd::GenerateSingleFileManifest(create_single_file_from_cli(&cmd, cwd)?)
67        }
68        cli::Command::Validate(cmd) => {
69            CliCmd::Validate(create_validate_command_from_cli(&cmd, cwd)?)
70        }
71        cli::Command::Channels(cmd) => {
72            CliCmd::PrintChannels(create_print_channels_from_cli(&cmd, cwd)?)
73        }
74        cli::Command::Info(cmd) => CliCmd::PrintInfo(create_print_info_from_cli(&cmd, cwd)?),
75    })
76}
77
78fn create_single_file_from_cli(
79    cmd: &cli::SingleFile,
80    cwd: &Path,
81) -> Result<GenerateSingleFileManifestCmd> {
82    let manifest = cmd.input.clone();
83    let output = cwd.join(&cmd.output);
84    let channel = cmd
85        .channel
86        .clone()
87        .unwrap_or_else(|| RELEASE_CHANNEL.into());
88    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
89    Ok(GenerateSingleFileManifestCmd {
90        manifest,
91        output,
92        channel,
93        loader,
94    })
95}
96
97fn create_generate_command_experimenter_from_cli(
98    cmd: &cli::GenerateExperimenter,
99    cwd: &Path,
100) -> Result<GenerateExperimenterManifestCmd> {
101    let manifest = cmd.input.clone();
102    let load_from_ir =
103        TargetLanguage::ExperimenterJSON == TargetLanguage::from_extension(&manifest)?;
104    let output = cwd.join(&cmd.output);
105    let language = output.as_path().try_into()?;
106    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
107    let cmd = GenerateExperimenterManifestCmd {
108        manifest: cmd.input.clone(),
109        output,
110        language,
111        load_from_ir,
112        loader,
113    };
114    Ok(cmd)
115}
116
117fn create_generate_command_from_cli(cmd: &cli::Generate, cwd: &Path) -> Result<GenerateStructCmd> {
118    let manifest = cmd.input.clone();
119    let load_from_ir = matches!(
120        TargetLanguage::from_extension(&manifest),
121        Ok(TargetLanguage::ExperimenterJSON)
122    );
123    let output = cwd.join(&cmd.output);
124    let language = match cmd.language {
125        Some(s) => TargetLanguage::from(s),
126        None => output.as_path().try_into().map_err(|_| anyhow::anyhow!("Can't infer a target language from the file or directory, so specify a --language flag explicitly"))?,
127    };
128    let channel = cmd.channel.clone();
129    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
130    Ok(GenerateStructCmd {
131        language,
132        manifest,
133        output,
134        load_from_ir,
135        channel,
136        loader,
137    })
138}
139
140fn create_loader(
141    input_file: &str,
142    loader_info: &cli::LoaderInfo,
143    cwd: &Path,
144) -> Result<LoaderConfig> {
145    let cwd = cwd.to_path_buf();
146    let cache_dir = loader_info
147        .cache_dir
148        .as_ref()
149        .map(|f| Some(cwd.join(f)))
150        .unwrap_or_default();
151
152    let repo_files = loader_info
153        .repo_file
154        .iter()
155        .map(|s| s.to_string())
156        .collect();
157
158    let mut refs: BTreeMap<_, _> = Default::default();
159    match (LoaderConfig::repo_and_path(input_file), &loader_info.ref_) {
160        (Some((repo, _)), Some(ref_)) => refs.insert(repo, ref_.clone()),
161        _ => None,
162    };
163
164    Ok(LoaderConfig {
165        cache_dir,
166        repo_files,
167        cwd,
168        refs,
169    })
170}
171
172fn create_validate_command_from_cli(cmd: &cli::Validate, cwd: &Path) -> Result<ValidateCmd> {
173    let manifest = cmd.input.clone();
174    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
175    Ok(ValidateCmd { manifest, loader })
176}
177
178fn create_print_channels_from_cli(cmd: &cli::Channels, cwd: &Path) -> Result<PrintChannelsCmd> {
179    let manifest = cmd.input.clone();
180    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
181    let as_json = cmd.json;
182    Ok(PrintChannelsCmd {
183        manifest,
184        loader,
185        as_json,
186    })
187}
188
189fn create_print_info_from_cli(cmd: &cli::Info, cwd: &Path) -> Result<PrintInfoCmd> {
190    let manifest = cmd.input.clone();
191    let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
192    let as_json = cmd.json;
193
194    let channel = cmd.channel.clone();
195    let feature = cmd.feature.clone();
196
197    Ok(PrintInfoCmd {
198        manifest,
199        channel,
200        loader,
201        as_json,
202        feature,
203    })
204}
205
206#[cfg(test)]
207mod cli_tests {
208    use std::{env, path::PathBuf};
209
210    use super::*;
211
212    const FML_BIN: &str = "nimbus-fml";
213    const TEST_FILE: &str = "fixtures/fe/importing/simple/app.yaml";
214    const TEST_DIR: &str = "fixtures/fe/importing/including-imports";
215    const GENERATED_DIR: &str = "build/cli-test";
216    const CACHE_DIR: &str = "./build/cache";
217    const REPO_FILE_1: &str = "./repos.versions.json";
218    const REPO_FILE_2: &str = "./repos.local.json";
219
220    fn package_dir() -> Result<PathBuf> {
221        let string = env::var("CARGO_MANIFEST_DIR")?;
222        Ok(PathBuf::from(string))
223    }
224
225    // All these tests just exercise the command line parsing.
226    // Each of the tests construct a command struct from the command line, and then
227    // test the command struct against the expected values.
228
229    ///////////////////////////////////////////////////////////////////////////
230    #[test]
231    fn test_cli_generate_android_features_language_implied() -> Result<()> {
232        let cwd = package_dir()?;
233        let cmd = get_command_from_cli(
234            [
235                FML_BIN,
236                "generate",
237                "--channel",
238                "channel-test",
239                TEST_FILE,
240                "./Implied.kt",
241            ],
242            &cwd,
243        )?;
244
245        assert!(matches!(cmd, CliCmd::Generate(_)));
246
247        if let CliCmd::Generate(cmd) = cmd {
248            assert_eq!(cmd.channel, "channel-test");
249            assert_eq!(cmd.language, TargetLanguage::Kotlin);
250            assert!(!cmd.load_from_ir);
251            assert!(cmd.output.ends_with("Implied.kt"));
252            assert!(cmd.manifest.ends_with(TEST_FILE));
253        }
254        Ok(())
255    }
256
257    #[test]
258    fn test_cli_generate_ios_features_language_implied() -> Result<()> {
259        let cwd = package_dir()?;
260        let cmd = get_command_from_cli(
261            [
262                FML_BIN,
263                "generate",
264                "--channel",
265                "channel-test",
266                TEST_FILE,
267                "./Implied.swift",
268            ],
269            &cwd,
270        )?;
271
272        assert!(matches!(cmd, CliCmd::Generate(_)));
273
274        if let CliCmd::Generate(cmd) = cmd {
275            assert_eq!(cmd.channel, "channel-test");
276            assert_eq!(cmd.language, TargetLanguage::Swift);
277            assert!(!cmd.load_from_ir);
278            assert!(cmd.output.ends_with("Implied.swift"));
279            assert!(cmd.manifest.ends_with(TEST_FILE));
280        }
281        Ok(())
282    }
283
284    ///////////////////////////////////////////////////////////////////////////
285
286    #[test]
287    fn test_cli_generate_features_with_remote_flags() -> Result<()> {
288        let cwd = package_dir()?;
289        let cmd = get_command_from_cli(
290            [
291                FML_BIN,
292                "generate",
293                "--repo-file",
294                REPO_FILE_1,
295                "--repo-file",
296                REPO_FILE_2,
297                "--cache-dir",
298                CACHE_DIR,
299                TEST_FILE,
300                "./Implied.swift",
301                "--channel",
302                "channel-test",
303            ],
304            &cwd,
305        )?;
306
307        assert!(matches!(cmd, CliCmd::Generate(_)));
308
309        if let CliCmd::Generate(cmd) = cmd {
310            assert_eq!(cmd.channel, "channel-test");
311            assert_eq!(cmd.language, TargetLanguage::Swift);
312            assert!(!cmd.load_from_ir);
313            assert!(cmd.output.ends_with("Implied.swift"));
314            assert!(cmd.manifest.ends_with(TEST_FILE));
315        }
316        Ok(())
317    }
318
319    ///////////////////////////////////////////////////////////////////////////
320    #[test]
321    fn test_cli_generate_android_features_language_flag() -> Result<()> {
322        let cwd = package_dir()?;
323        let cmd = get_command_from_cli(
324            [
325                FML_BIN,
326                "generate",
327                "--channel",
328                "channel-test",
329                "--language",
330                "kotlin",
331                TEST_FILE,
332                "./build/generated",
333            ],
334            &cwd,
335        )?;
336
337        assert!(matches!(cmd, CliCmd::Generate(_)));
338
339        if let CliCmd::Generate(cmd) = cmd {
340            assert_eq!(cmd.channel, "channel-test");
341            assert_eq!(cmd.language, TargetLanguage::Kotlin);
342            assert!(!cmd.load_from_ir);
343            assert!(cmd.output.ends_with("build/generated"));
344            assert!(cmd.manifest.ends_with(TEST_FILE));
345        }
346        Ok(())
347    }
348
349    #[test]
350    fn test_cli_generate_ios_features_language_flag() -> Result<()> {
351        let cwd = package_dir()?;
352        let cmd = get_command_from_cli(
353            [
354                FML_BIN,
355                "generate",
356                "--channel",
357                "channel-test",
358                "--language",
359                "swift",
360                TEST_FILE,
361                "./build/generated",
362            ],
363            &cwd,
364        )?;
365
366        assert!(matches!(cmd, CliCmd::Generate(_)));
367
368        if let CliCmd::Generate(cmd) = cmd {
369            assert_eq!(cmd.channel, "channel-test");
370            assert_eq!(cmd.language, TargetLanguage::Swift);
371            assert!(!cmd.load_from_ir);
372            assert!(cmd.output.ends_with("build/generated"));
373            assert!(cmd.manifest.ends_with(TEST_FILE));
374        }
375        Ok(())
376    }
377
378    ///////////////////////////////////////////////////////////////////////////
379    #[test]
380    fn test_cli_generate_experimenter_android() -> Result<()> {
381        let cwd = package_dir()?;
382        let cmd = get_command_from_cli(
383            [
384                FML_BIN,
385                "generate-experimenter",
386                TEST_FILE,
387                ".experimenter.yaml",
388            ],
389            &cwd,
390        )?;
391
392        assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
393
394        if let CliCmd::GenerateExperimenter(cmd) = cmd {
395            assert_eq!(cmd.language, TargetLanguage::ExperimenterYAML);
396            assert!(!cmd.load_from_ir);
397            assert!(cmd.output.ends_with(".experimenter.yaml"));
398            assert!(cmd.manifest.ends_with(TEST_FILE));
399        }
400        Ok(())
401    }
402
403    #[test]
404    fn test_cli_generate_experimenter_ios() -> Result<()> {
405        let cwd = package_dir()?;
406        let cmd = get_command_from_cli(
407            [
408                FML_BIN,
409                "generate-experimenter",
410                "--channel",
411                "test-channel",
412                TEST_FILE,
413                ".experimenter.yaml",
414            ],
415            &cwd,
416        )?;
417
418        assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
419
420        if let CliCmd::GenerateExperimenter(cmd) = cmd {
421            assert_eq!(cmd.language, TargetLanguage::ExperimenterYAML);
422            assert!(!cmd.load_from_ir);
423            assert!(cmd.output.ends_with(".experimenter.yaml"));
424            assert!(cmd.manifest.ends_with(TEST_FILE));
425        }
426        Ok(())
427    }
428
429    #[test]
430    fn test_cli_generate_experimenter_with_json() -> Result<()> {
431        let cwd = package_dir()?;
432        let cmd = get_command_from_cli(
433            [
434                FML_BIN,
435                "generate-experimenter",
436                TEST_FILE,
437                ".experimenter.json",
438            ],
439            &cwd,
440        )?;
441
442        assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
443
444        if let CliCmd::GenerateExperimenter(cmd) = cmd {
445            assert_eq!(cmd.language, TargetLanguage::ExperimenterJSON);
446            assert!(!cmd.load_from_ir);
447            assert!(cmd.output.ends_with(".experimenter.json"));
448            assert!(cmd.manifest.ends_with(TEST_FILE));
449        }
450        Ok(())
451    }
452
453    #[test]
454    fn test_cli_generate_experimenter_with_remote_flags() -> Result<()> {
455        let cwd = package_dir()?;
456        let cmd = get_command_from_cli(
457            [
458                FML_BIN,
459                "generate-experimenter",
460                "--repo-file",
461                REPO_FILE_1,
462                "--repo-file",
463                REPO_FILE_2,
464                "--cache-dir",
465                CACHE_DIR,
466                TEST_FILE,
467                ".experimenter.json",
468            ],
469            &cwd,
470        )?;
471
472        assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
473
474        if let CliCmd::GenerateExperimenter(cmd) = cmd {
475            assert_eq!(cmd.language, TargetLanguage::ExperimenterJSON);
476            assert!(!cmd.load_from_ir);
477            assert!(cmd.output.ends_with(".experimenter.json"));
478            assert!(cmd.manifest.ends_with(TEST_FILE));
479        }
480        Ok(())
481    }
482
483    #[test]
484    fn test_cli_generate_features_for_directory_input() -> Result<()> {
485        let cwd = package_dir()?;
486        let cmd = get_command_from_cli(
487            [
488                FML_BIN,
489                "generate",
490                "--language",
491                "swift",
492                "--channel",
493                "release",
494                TEST_DIR,
495                GENERATED_DIR,
496            ],
497            &cwd,
498        )?;
499
500        assert!(matches!(cmd, CliCmd::Generate(_)));
501
502        if let CliCmd::Generate(cmd) = cmd {
503            assert_eq!(cmd.channel, "release");
504            assert_eq!(cmd.language, TargetLanguage::Swift);
505            assert!(!cmd.load_from_ir);
506            assert!(cmd.output.ends_with("build/cli-test"));
507            assert_eq!(&cmd.manifest, TEST_DIR);
508        }
509        Ok(())
510    }
511
512    ///////////////////////////////////////////////////////////////////////////
513    #[test]
514    fn test_cli_generate_validate() -> Result<()> {
515        let cwd = package_dir()?;
516        let cmd = get_command_from_cli([FML_BIN, "validate", TEST_FILE], &cwd)?;
517
518        assert!(matches!(cmd, CliCmd::Validate(_)));
519        assert!(matches!(cmd, CliCmd::Validate(c) if c.manifest.ends_with(TEST_FILE)));
520        Ok(())
521    }
522
523    ///////////////////////////////////////////////////////////////////////////
524    #[test]
525    fn test_cli_print_channels_command() -> Result<()> {
526        let cwd = package_dir()?;
527        let cmd = get_command_from_cli([FML_BIN, "channels", TEST_FILE], &cwd)?;
528
529        assert!(matches!(&cmd, CliCmd::PrintChannels(_)));
530        assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.manifest.ends_with(TEST_FILE)));
531        assert!(matches!(&cmd, CliCmd::PrintChannels(c) if !c.as_json));
532
533        let cmd = get_command_from_cli([FML_BIN, "channels", TEST_FILE, "--json"], &cwd)?;
534
535        assert!(matches!(&cmd, CliCmd::PrintChannels(_)));
536        assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.manifest.ends_with(TEST_FILE)));
537        assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.as_json));
538        Ok(())
539    }
540
541    ///////////////////////////////////////////////////////////////////////////
542    #[test]
543    fn test_cli_print_info_command() -> Result<()> {
544        let cwd = package_dir()?;
545        let cmd = get_command_from_cli([FML_BIN, "info", TEST_FILE, "--channel", "release"], &cwd)?;
546
547        assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
548        assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
549        assert!(
550            matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { channel: Some(channel), as_json, .. }) if channel.as_str() == "release" && !as_json )
551        );
552
553        let cmd = get_command_from_cli(
554            [FML_BIN, "info", TEST_FILE, "--channel", "beta", "--json"],
555            &cwd,
556        )?;
557
558        assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
559        assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
560        assert!(
561            matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { channel: Some(channel), as_json, .. }) if channel.as_str() == "beta" && *as_json )
562        );
563
564        let cmd = get_command_from_cli(
565            [
566                FML_BIN,
567                "info",
568                TEST_FILE,
569                "--feature",
570                "my-feature",
571                "--json",
572            ],
573            &cwd,
574        )?;
575
576        assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
577        assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
578        assert!(
579            matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { feature: Some(feature), as_json, .. }) if feature.as_str() == "my-feature" && *as_json )
580        );
581        Ok(())
582    }
583
584    ///////////////////////////////////////////////////////////////////////////
585    #[test]
586    fn test_cli_add_ref_arg() -> Result<()> {
587        let cwd = package_dir()?;
588        let cmd = get_command_from_cli(
589            [
590                FML_BIN,
591                "generate-experimenter",
592                "--ref",
593                "my-tag",
594                "@foo/bar/baz.fml.yaml",
595                "./baz.yaml",
596            ],
597            &cwd,
598        )?;
599
600        assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
601        assert!(
602            matches!(cmd, CliCmd::GenerateExperimenter(c) if c.loader.refs["@foo/bar"] == "my-tag")
603        );
604        Ok(())
605    }
606}