Skip to content

Commit

Permalink
Merge pull request #40 from jjcomer/refactor
Browse files Browse the repository at this point in the history
Refactor cli and server logic
  • Loading branch information
jjcomer authored Feb 10, 2020
2 parents 2e7296e + ad73498 commit c834f2f
Show file tree
Hide file tree
Showing 9 changed files with 1,690 additions and 1,633 deletions.
1,894 changes: 948 additions & 946 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ doc = false

[package]
name = 'hogan'
version = '0.7.1'
version = '0.7.2'
authors = ['Jonathan Morley <[email protected]>']
edition = '2018'

Expand Down
215 changes: 215 additions & 0 deletions src/app/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
use crate::app::config::App;
use crate::app::config::AppCommon;
use failure::Error;
use hogan::config::ConfigDir;
use hogan::template::TemplateDir;
use regex::Regex;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::ErrorKind::AlreadyExists;
use std::io::Write;
use std::path::PathBuf;

pub fn cli(
templates_path: PathBuf,
environments_regex: Regex,
templates_regex: Regex,
common: AppCommon,
ignore_existing: bool,
) -> Result<(), Error> {
let handlebars = hogan::transform::handlebars(common.strict);

let template_dir = TemplateDir::new(templates_path)?;
let mut templates = template_dir.find(templates_regex);
println!("Loaded {} template file(s)", templates.len());

let config_dir = ConfigDir::new(common.configs_url, &common.ssh_key)?;
let environments = config_dir.find(App::config_regex(&environments_regex)?);
println!("Loaded {} config file(s)", environments.len());

for environment in environments {
println!("Updating templates for {}", environment.environment);

for template in &mut templates {
debug!("Transforming {:?}", template.path);

let rendered = template.render(&handlebars, &environment)?;
trace!("Rendered: {:?}", rendered.contents);

if ignore_existing {
if let Err(e) = match OpenOptions::new()
.write(true)
.create_new(true)
.open(&rendered.path)
{
Ok(ref mut f) => f.write_all(&rendered.contents),
Err(ref e) if e.kind() == AlreadyExists => {
println!("Skipping {:?} - config already exists.", rendered.path);
trace!("Skipping {:?} - config already exists.", rendered.path);
Ok(())
}
Err(e) => Err(e),
} {
bail!("Error transforming {:?} due to {}", rendered.path, e)
}
} else if let Err(e) = File::create(&rendered.path)?.write_all(&rendered.contents) {
bail!("Error transforming {:?} due to {}", rendered.path, e)
}
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use assert_cmd;
use dir_diff;
use fs_extra;
use predicates;
use tempfile;

use self::assert_cmd::prelude::*;
use self::fs_extra::dir;
use self::predicates::prelude::*;
use std::io::Write;
use std::path::Path;
use std::process::Command;

#[cfg(not(all(target_env = "msvc", target_arch = "x86_64")))]
#[test]
fn test_transform() {
let temp_dir = tempfile::tempdir().unwrap();

fs_extra::copy_items(
&vec!["tests/fixtures/projects/templates"],
temp_dir.path(),
&dir::CopyOptions::new(),
)
.unwrap();

let templates_path = temp_dir.path().join("templates");

let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();

let cmd = cmd.args(&[
"transform",
"--configs",
"tests/fixtures/configs",
"--templates",
templates_path.to_str().unwrap(),
]);

cmd.assert().success();

cmd.assert().stdout(
predicate::str::contains(format!(r#"Finding Files: {:?}"#, templates_path)).from_utf8(),
);

cmd.assert().stdout(
predicate::str::contains(
r"regex: /^[^.]*(\w+\.)*template([-.].+)?\.(config|ya?ml|properties)/",
)
.from_utf8(),
);

cmd.assert()
.stdout(predicate::str::contains("Loaded 6 template file(s)").from_utf8());

cmd.assert().stdout(
predicate::str::contains(r#"Finding Files: "tests/fixtures/configs""#).from_utf8(),
);

cmd.assert()
.stdout(predicate::str::contains(r#"regex: /config\..+\.json$/"#).from_utf8());

cmd.assert()
.stdout(predicate::str::contains("Loaded 4 config file(s)").from_utf8());

for environment in ["EMPTY", "ENVTYPE", "TEST", "TEST2"].iter() {
cmd.assert().stdout(
predicate::str::contains(format!("Updating templates for {}", environment))
.from_utf8(),
);
}

assert!(!dir_diff::is_different(
&templates_path.join("project-1"),
&Path::new("tests/fixtures/projects/rendered/project-1")
)
.unwrap());

assert!(!dir_diff::is_different(
&templates_path.join("project-2"),
&Path::new("tests/fixtures/projects/rendered/project-2")
)
.unwrap());
}

#[cfg(not(all(target_env = "msvc", target_arch = "x86_64")))]
#[test]
fn test_ignore_existing() {
let temp_dir = tempfile::tempdir().unwrap();

fs_extra::copy_items(
&vec!["tests/fixtures/projects/templates"],
temp_dir.path(),
&dir::CopyOptions::new(),
)
.unwrap();

let templates_path = temp_dir.path().join("templates");

let ignore_path = templates_path.join("project-1/Web.EMPTY.config");
if let Ok(ref mut f) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(&ignore_path)
{
f.write_all(b"Hamburger.")
.expect("Failed to create test file for ignore.")
}

let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
let cmd = cmd.args(&[
"transform",
"--configs",
"tests/fixtures/configs",
"--templates",
templates_path.to_str().unwrap(),
"-i",
]);

cmd.assert().success();

// assert that running the command with the ignore flag
// did not overwrite the manually created project-1/Web.EMPTY.config
let data2 =
std::fs::read_to_string(&ignore_path).expect("Failed to read test file for ignore.");
assert!(data2 == "Hamburger.");

// after running the command again without the ignore flag
// assert that the configs now match those in the rendered directory
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
let cmd = cmd.args(&[
"transform",
"--configs",
"tests/fixtures/configs",
"--templates",
templates_path.to_str().unwrap(),
]);
cmd.assert().success();

assert!(!dir_diff::is_different(
&templates_path.join("project-1"),
&Path::new("tests/fixtures/projects/rendered/project-1")
)
.unwrap());

assert!(!dir_diff::is_different(
&templates_path.join("project-2"),
&Path::new("tests/fixtures/projects/rendered/project-2")
)
.unwrap());
}
}
139 changes: 139 additions & 0 deletions src/app/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use failure::Error;
use hogan::config::ConfigUrl;
use regex::{Regex, RegexBuilder};
use std::path::PathBuf;
use structopt;
use structopt::clap::AppSettings;
use structopt::StructOpt;

/// Transform templates with handlebars
#[derive(StructOpt, Debug)]
#[structopt(setting = AppSettings::InferSubcommands)]
pub struct App {
/// Sets the level of verbosity
#[structopt(short = "v", long = "verbose", parse(from_occurrences), global = true)]
pub verbosity: usize,

#[structopt(subcommand)]
pub cmd: AppCommand,
}

#[derive(StructOpt, Debug)]
pub enum AppCommand {
/// Transform handlebars template files against config files
#[structopt(name = "transform")]
Transform {
#[structopt(flatten)]
common: AppCommon,

/// Filter environments to render templates for
#[structopt(
short = "e",
long = "environments-filter",
parse(try_from_str = App::parse_regex),
default_value = ".+",
value_name = "REGEX"
)]
environments_regex: Regex,

/// Template source (recursive)
#[structopt(
short = "t",
long = "templates",
parse(from_os_str),
default_value = ".",
value_name = "DIR"
)]
templates_path: PathBuf,

/// Filter templates to transform
#[structopt(
short = "f",
long = "templates-filter",
parse(try_from_str = App::parse_regex),
default_value = "^[^.]*(\\w+\\.)*template([-.].+)?\\.(config|ya?ml|properties)",
value_name = "REGEX"
)]
templates_regex: Regex,

/// Ignore existing config files intead of overwriting
#[structopt(short = "i", long = "ignore-existing")]
ignore_existing: bool,
},
/// Respond to HTTP requests to transform a template
#[structopt(name = "server")]
Server {
#[structopt(flatten)]
common: AppCommon,

/// Port to serve requests on
#[structopt(short = "p", long = "port", default_value = "80", value_name = "PORT")]
port: u16,

/// The address for the server to bind on
#[structopt(
short = "b",
long = "address",
default_value = "0.0.0.0",
value_name = "ADDRESS"
)]
address: String,

/// Set the size of the SHA LRU cache
#[structopt(long = "cache", default_value = "100", value_name = "CACHE_SIZE")]
cache_size: usize,

/// Filter environments to render templates for
#[structopt(
short = "e",
long = "environments-filter",
parse(try_from_str = App::parse_regex),
default_value = ".+",
value_name = "REGEX"
)]
environments_regex: Regex,

/// If datadog monitoring is enabled
#[structopt(short = "d", long = "datadog")]
datadog: bool,
},
}

#[derive(StructOpt, Debug)]
pub struct AppCommon {
/// Config source. Accepts file and git URLs. Paths within a git repository may be appended
/// to a git URL, and branches may be specified as a URL fragment (recursive if applicable)
#[structopt(short = "c", long = "configs", value_name = "URL")]
pub configs_url: ConfigUrl,

/// SSH key to use if configs URL requires authentication
#[structopt(
short = "k",
long = "ssh-key",
parse(from_str = App::parse_path_buf),
default_value = "~/.ssh/id_rsa",
value_name = "FILE"
)]
pub ssh_key: PathBuf,

/// Throw errors if values do not exist in configs
#[structopt(short = "s", long = "strict")]
pub strict: bool,
}

impl App {
pub fn config_regex(environment: &Regex) -> Result<Regex, Error> {
App::parse_regex(&format!("config\\.{}\\.json$", environment))
}

pub fn parse_regex(src: &str) -> Result<Regex, Error> {
RegexBuilder::new(src)
.case_insensitive(true)
.build()
.map_err(|e| e.into())
}

pub fn parse_path_buf(src: &str) -> PathBuf {
PathBuf::from(shellexpand::tilde(src).into_owned())
}
}
File renamed without changes.
4 changes: 4 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod cli;
pub mod config;
mod datadogstatsd;
pub mod server;
Loading

0 comments on commit c834f2f

Please sign in to comment.