Revision control

Copy as Markdown

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub(crate) mod commands;
mod workflows;
use crate::intermediate_representation::TargetLanguage;
use crate::util::loaders::LoaderConfig;
use anyhow::{bail, Result};
use clap::{App, ArgMatches};
use commands::{
CliCmd, GenerateExperimenterManifestCmd, GenerateSingleFileManifestCmd, GenerateStructCmd,
PrintChannelsCmd, ValidateCmd,
};
use std::{
collections::BTreeMap,
ffi::OsString,
path::{Path, PathBuf},
};
use self::commands::PrintInfoCmd;
const RELEASE_CHANNEL: &str = "release";
pub fn do_main<I, T>(args: I, cwd: &Path) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let cmd = get_command_from_cli(args, cwd)?;
process_command(&cmd)
}
fn process_command(cmd: &CliCmd) -> Result<()> {
match cmd {
CliCmd::Generate(params) => workflows::generate_struct(params)?,
CliCmd::GenerateExperimenter(params) => workflows::generate_experimenter_manifest(params)?,
CliCmd::GenerateSingleFileManifest(params) => {
workflows::generate_single_file_manifest(params)?
}
CliCmd::FetchFile(files, nm) => workflows::fetch_file(files, nm)?,
CliCmd::Validate(params) => workflows::validate(params)?,
CliCmd::PrintChannels(params) => workflows::print_channels(params)?,
CliCmd::PrintInfo(params) => workflows::print_info(params)?,
};
Ok(())
}
fn get_command_from_cli<I, T>(args: I, cwd: &Path) -> Result<CliCmd>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let yaml = clap::load_yaml!("cli.yaml");
let matches = App::from_yaml(yaml).get_matches_from(args);
Ok(match matches.subcommand() {
("generate", Some(matches)) => {
CliCmd::Generate(create_generate_command_from_cli(matches, cwd)?)
}
("generate-experimenter", Some(matches)) => CliCmd::GenerateExperimenter(
create_generate_command_experimenter_from_cli(matches, cwd)?,
),
("fetch", Some(matches)) => {
CliCmd::FetchFile(create_loader(matches, cwd)?, input_file(matches)?)
}
("single-file", Some(matches)) => {
CliCmd::GenerateSingleFileManifest(create_single_file_from_cli(matches, cwd)?)
}
("validate", Some(matches)) => {
CliCmd::Validate(create_validate_command_from_cli(matches, cwd)?)
}
("channels", Some(matches)) => {
CliCmd::PrintChannels(create_print_channels_from_cli(matches, cwd)?)
}
("info", Some(matches)) => CliCmd::PrintInfo(create_print_info_from_cli(matches, cwd)?),
(word, _) => unimplemented!("Command {} not implemented", word),
})
}
fn create_single_file_from_cli(
matches: &ArgMatches,
cwd: &Path,
) -> Result<GenerateSingleFileManifestCmd> {
let manifest = input_file(matches)?;
let output =
file_path("output", matches, cwd).or_else(|_| file_path("OUTPUT", matches, cwd))?;
let channel = matches
.value_of("channel")
.map(str::to_string)
.unwrap_or_else(|| RELEASE_CHANNEL.into());
let loader = create_loader(matches, cwd)?;
Ok(GenerateSingleFileManifestCmd {
manifest,
output,
channel,
loader,
})
}
fn create_generate_command_experimenter_from_cli(
matches: &ArgMatches,
cwd: &Path,
) -> Result<GenerateExperimenterManifestCmd> {
let manifest = input_file(matches)?;
let load_from_ir =
TargetLanguage::ExperimenterJSON == TargetLanguage::from_extension(&manifest)?;
let output =
file_path("output", matches, cwd).or_else(|_| file_path("OUTPUT", matches, cwd))?;
let language = output.as_path().try_into()?;
let _channel = matches.value_of("channel").map(str::to_string);
let loader = create_loader(matches, cwd)?;
let cmd = GenerateExperimenterManifestCmd {
manifest,
output,
language,
load_from_ir,
loader,
};
Ok(cmd)
}
fn create_generate_command_from_cli(matches: &ArgMatches, cwd: &Path) -> Result<GenerateStructCmd> {
let manifest = input_file(matches)?;
let load_from_ir = matches!(
TargetLanguage::from_extension(&manifest),
Ok(TargetLanguage::ExperimenterJSON)
);
let output =
file_path("output", matches, cwd).or_else(|_| file_path("OUTPUT", matches, cwd))?;
let language = match matches.value_of("language") {
Some(s) => TargetLanguage::try_from(s)?, // the language from the cli will always be recognized
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"))?,
};
let channel = matches
.value_of("channel")
.map(str::to_string)
.expect("A channel should be specified with --channel");
let loader = create_loader(matches, cwd)?;
Ok(GenerateStructCmd {
language,
manifest,
output,
load_from_ir,
channel,
loader,
})
}
fn create_loader(matches: &ArgMatches, cwd: &Path) -> Result<LoaderConfig> {
let cwd = cwd.to_path_buf();
let cache_dir = matches
.value_of("cache-dir")
.map(|f| Some(cwd.join(f)))
.unwrap_or_default();
let files = matches.values_of("repo-file").unwrap_or_default();
let repo_files = files.into_iter().map(|s| s.to_string()).collect();
let manifest = input_file(matches)?;
let _ref = matches.value_of("ref").map(String::from);
let mut refs: BTreeMap<_, _> = Default::default();
match (LoaderConfig::repo_and_path(&manifest), _ref) {
(Some((repo, _)), Some(ref_)) => refs.insert(repo, ref_),
_ => None,
};
Ok(LoaderConfig {
cache_dir,
repo_files,
cwd,
refs,
})
}
fn create_validate_command_from_cli(matches: &ArgMatches, cwd: &Path) -> Result<ValidateCmd> {
let manifest = input_file(matches)?;
let loader = create_loader(matches, cwd)?;
Ok(ValidateCmd { manifest, loader })
}
fn create_print_channels_from_cli(matches: &ArgMatches, cwd: &Path) -> Result<PrintChannelsCmd> {
let manifest = input_file(matches)?;
let loader = create_loader(matches, cwd)?;
let as_json = matches.is_present("json");
Ok(PrintChannelsCmd {
manifest,
loader,
as_json,
})
}
fn create_print_info_from_cli(matches: &ArgMatches, cwd: &Path) -> Result<PrintInfoCmd> {
let manifest = input_file(matches)?;
let loader = create_loader(matches, cwd)?;
let as_json = matches.is_present("json");
let channel = matches.value_of("channel").map(str::to_string);
let feature = matches.value_of("feature").map(str::to_string);
Ok(PrintInfoCmd {
manifest,
channel,
loader,
as_json,
feature,
})
}
fn input_file(args: &ArgMatches) -> Result<String> {
args.value_of("INPUT")
.map(String::from)
.ok_or_else(|| anyhow::anyhow!("INPUT file or directory is needed, but not specified"))
}
fn file_path(name: &str, args: &ArgMatches, cwd: &Path) -> Result<PathBuf> {
let mut abs = cwd.to_path_buf();
match args.value_of(name) {
Some(suffix) => {
abs.push(suffix);
Ok(abs)
}
_ => bail!("A file path is needed for {}", name),
}
}
#[cfg(test)]
mod cli_tests {
use std::env;
use super::*;
const FML_BIN: &str = "nimbus-fml";
const TEST_FILE: &str = "fixtures/fe/importing/simple/app.yaml";
const TEST_DIR: &str = "fixtures/fe/importing/including-imports";
const GENERATED_DIR: &str = "build/cli-test";
const CACHE_DIR: &str = "./build/cache";
const REPO_FILE_1: &str = "./repos.versions.json";
const REPO_FILE_2: &str = "./repos.local.json";
fn package_dir() -> Result<PathBuf> {
let string = env::var("CARGO_MANIFEST_DIR")?;
Ok(PathBuf::from(string))
}
// All these tests just exercise the command line parsing.
// Each of the tests construct a command struct from the command line, and then
// test the command struct against the expected values.
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_generate_android_features_language_implied() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--channel",
"channel-test",
TEST_FILE,
"./Implied.kt",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "channel-test");
assert_eq!(cmd.language, TargetLanguage::Kotlin);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("Implied.kt"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_ios_features_language_implied() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--channel",
"channel-test",
TEST_FILE,
"./Implied.swift",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "channel-test");
assert_eq!(cmd.language, TargetLanguage::Swift);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("Implied.swift"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_generate_features_with_remote_flags() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--repo-file",
REPO_FILE_1,
"--repo-file",
REPO_FILE_2,
"--cache-dir",
CACHE_DIR,
TEST_FILE,
"./Implied.swift",
"--channel",
"channel-test",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "channel-test");
assert_eq!(cmd.language, TargetLanguage::Swift);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("Implied.swift"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_generate_android_features_language_flag() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--channel",
"channel-test",
"--language",
"kotlin",
TEST_FILE,
"./build/generated",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "channel-test");
assert_eq!(cmd.language, TargetLanguage::Kotlin);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("build/generated"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_ios_features_language_flag() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--channel",
"channel-test",
"--language",
"swift",
TEST_FILE,
"./build/generated",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "channel-test");
assert_eq!(cmd.language, TargetLanguage::Swift);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("build/generated"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_generate_experimenter_android() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate-experimenter",
TEST_FILE,
".experimenter.yaml",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
if let CliCmd::GenerateExperimenter(cmd) = cmd {
assert_eq!(cmd.language, TargetLanguage::ExperimenterYAML);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with(".experimenter.yaml"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_experimenter_ios() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate-experimenter",
"--channel",
"test-channel",
TEST_FILE,
".experimenter.yaml",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
if let CliCmd::GenerateExperimenter(cmd) = cmd {
assert_eq!(cmd.language, TargetLanguage::ExperimenterYAML);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with(".experimenter.yaml"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_experimenter_with_json() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate-experimenter",
TEST_FILE,
".experimenter.json",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
if let CliCmd::GenerateExperimenter(cmd) = cmd {
assert_eq!(cmd.language, TargetLanguage::ExperimenterJSON);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with(".experimenter.json"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_experimenter_with_remote_flags() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate-experimenter",
"--repo-file",
REPO_FILE_1,
"--repo-file",
REPO_FILE_2,
"--cache-dir",
CACHE_DIR,
TEST_FILE,
".experimenter.json",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
if let CliCmd::GenerateExperimenter(cmd) = cmd {
assert_eq!(cmd.language, TargetLanguage::ExperimenterJSON);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with(".experimenter.json"));
assert!(cmd.manifest.ends_with(TEST_FILE));
}
Ok(())
}
#[test]
fn test_cli_generate_features_for_directory_input() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate",
"--language",
"swift",
"--channel",
"release",
TEST_DIR,
GENERATED_DIR,
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::Generate(_)));
if let CliCmd::Generate(cmd) = cmd {
assert_eq!(cmd.channel, "release");
assert_eq!(cmd.language, TargetLanguage::Swift);
assert!(!cmd.load_from_ir);
assert!(cmd.output.ends_with("build/cli-test"));
assert_eq!(&cmd.manifest, TEST_DIR);
}
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_generate_validate() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli([FML_BIN, "validate", TEST_FILE], &cwd)?;
assert!(matches!(cmd, CliCmd::Validate(_)));
assert!(matches!(cmd, CliCmd::Validate(c) if c.manifest.ends_with(TEST_FILE)));
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_print_channels_command() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli([FML_BIN, "channels", TEST_FILE], &cwd)?;
assert!(matches!(&cmd, CliCmd::PrintChannels(_)));
assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.manifest.ends_with(TEST_FILE)));
assert!(matches!(&cmd, CliCmd::PrintChannels(c) if !c.as_json));
let cmd = get_command_from_cli([FML_BIN, "channels", TEST_FILE, "--json"], &cwd)?;
assert!(matches!(&cmd, CliCmd::PrintChannels(_)));
assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.manifest.ends_with(TEST_FILE)));
assert!(matches!(&cmd, CliCmd::PrintChannels(c) if c.as_json));
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_print_info_command() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli([FML_BIN, "info", TEST_FILE, "--channel", "release"], &cwd)?;
assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
assert!(
matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { channel: Some(channel), as_json, .. }) if channel.as_str() == "release" && !as_json )
);
let cmd = get_command_from_cli(
[FML_BIN, "info", TEST_FILE, "--channel", "beta", "--json"],
&cwd,
)?;
assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
assert!(
matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { channel: Some(channel), as_json, .. }) if channel.as_str() == "beta" && *as_json )
);
let cmd = get_command_from_cli(
[
FML_BIN,
"info",
TEST_FILE,
"--feature",
"my-feature",
"--json",
],
&cwd,
)?;
assert!(matches!(&cmd, CliCmd::PrintInfo(_)));
assert!(matches!(&cmd, CliCmd::PrintInfo(c) if c.manifest.ends_with(TEST_FILE)));
assert!(
matches!(&cmd, CliCmd::PrintInfo(PrintInfoCmd { feature: Some(feature), as_json, .. }) if feature.as_str() == "my-feature" && *as_json )
);
Ok(())
}
///////////////////////////////////////////////////////////////////////////
#[test]
fn test_cli_add_ref_arg() -> Result<()> {
let cwd = package_dir()?;
let cmd = get_command_from_cli(
[
FML_BIN,
"generate-experimenter",
"--ref",
"my-tag",
"@foo/bar/baz.fml.yaml",
"./baz.yaml",
],
&cwd,
)?;
assert!(matches!(cmd, CliCmd::GenerateExperimenter(_)));
assert!(
matches!(cmd, CliCmd::GenerateExperimenter(c) if c.loader.refs["@foo/bar"] == "my-tag")
);
Ok(())
}
}