1mod 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}