1pub(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)?, 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}