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