From 1c9a8a01164c28a3587c764b7e39073240892b72 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 24 Feb 2016 21:27:53 -0800 Subject: [PATCH] Perform interpolation on template parameters. --- Cargo.lock | 49 +++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ example.yml | 2 +- src/main.rs | 18 ++++++++----- src/parameter.rs | 43 ++++++++++++++++++------------- src/processor.rs | 66 ++++++++++++++++++++++++++++++++++++++---------- src/template.rs | 48 ++++++++++++++++++++--------------- 7 files changed, 169 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23159a1..b2bee4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,9 +3,19 @@ name = "ktmpl" version = "0.1.0" dependencies = [ "clap 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", "yaml-rust 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "aho-corasick" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ansi_term" version = "0.7.2" @@ -27,11 +37,50 @@ dependencies = [ "vec_map 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lazy_static" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "strsim" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "vec_map" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 77939aa..402f207 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,6 @@ version = "0.1.0" [dependencies] clap = "2.1.1" +lazy_static = "0.1.15" +regex = "0.1.54" yaml-rust = "0.3.0" diff --git a/example.yml b/example.yml index b264621..5e9765c 100644 --- a/example.yml +++ b/example.yml @@ -53,7 +53,7 @@ parameters: required: true - name: "MONGODB_USER" description: "Username for MongoDB user that will be used for accessing the database" - value: "password" + value: "username" required: true - name: "MONGODB_PASSWORD" description: "Password for the MongoDB user" diff --git a/src/main.rs b/src/main.rs index fd3812c..71c1994 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ extern crate clap; +#[macro_use] +extern crate lazy_static; +extern crate regex; extern crate yaml_rust as yaml; use std::collections::HashMap; @@ -9,7 +12,7 @@ use std::process::exit; use clap::{App, AppSettings, Arg}; -use parameter::param_map; +use parameter::user_values; use template::Template; mod parameter; @@ -46,18 +49,19 @@ fn real_main() -> Result<(), String> { ) .get_matches(); + let user_values = match matches.values_of("parameter") { + Some(parameters) => try!(user_values(parameters.map(|s| s.to_string()).collect())), + None => HashMap::new(), + }; + let filename = matches.value_of("template").expect("template wasn't provided"); let mut file = try!(File::open(filename).map_err(|err| err.description().to_owned())); let mut template_data = String::new(); try!(file.read_to_string(&mut template_data).map_err(|err| err.description().to_owned())); - let template = try!(Template::from_string(template_data)); - let parameters = match matches.values_of("parameter") { - Some(parameters) => try!(param_map(parameters.map(|s| s.to_string()).collect())), - None => HashMap::new(), - }; + let template = try!(Template::new(template_data, user_values)); - match template.process(parameters) { + match template.process() { Ok(manifests) => { println!("{}", manifests); diff --git a/src/parameter.rs b/src/parameter.rs index f1459cc..926f2bc 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -9,7 +9,7 @@ pub struct Parameter { pub name: String, pub parameter_type: Option, pub required: bool, - pub value: Option, + pub value: Option, } pub enum ParameterType { @@ -18,16 +18,11 @@ pub enum ParameterType { String, } -pub enum ParameterValue { - Bool(bool), - Int(i64), - String(String), -} - -pub type ParamMap = HashMap; +pub type ParamMap = HashMap; +pub type UserValues = HashMap; impl Parameter { - pub fn from_yaml(yaml: &Yaml) -> Result { + pub fn new(yaml: &Yaml, user_values: &UserValues) -> Result { let description = match yaml["description"] { Yaml::String(ref description) => Some(description.clone()), _ => None, @@ -45,11 +40,23 @@ impl Parameter { None => None, }; let required = yaml["required"].as_bool().unwrap_or(false); - let value = match yaml["value"] { - Yaml::Boolean(ref value) => Some(ParameterValue::Bool(value.clone())), - Yaml::Integer(ref value) => Some(ParameterValue::Int(value.clone())), - Yaml::String(ref value) => Some(ParameterValue::String(value.clone())), - _ => return Err("Parameter values must be boolean, i64, or string.".to_owned()), + let value = match user_values.get(&name) { + Some(value) => Some(value.clone()), + None => match yaml["value"] { + Yaml::Boolean(ref value) => Some(format!("{}", value)), + Yaml::Integer(ref value) => Some(format!("{}", value)), + Yaml::String(ref value) => Some(value.clone()), + _ => if required { + return Err( + format!( + "Parameter {} required and must be boolean, i64, or string.", + display_name.unwrap_or(name), + ) + ) + } else { + None + }, + } }; Ok(Parameter { @@ -77,18 +84,18 @@ impl FromStr for ParameterType { } } -pub fn param_map(parameters: Vec) -> Result { - let mut param_map = HashMap::new(); +pub fn user_values(parameters: Vec) -> Result { + let mut user_values = HashMap::new(); for parameter in parameters { let mut parts: Vec = parameter.split('=').map(|s| s.to_string()).collect(); if parts.len() == 2 { - param_map.insert(parts.remove(0), parts.remove(0)); + user_values.insert(parts.remove(0), parts.remove(0)); } else { return Err("Parameters must be supplied in the form KEY=VALUE.".to_string()); } } - Ok(param_map) + Ok(user_values) } diff --git a/src/processor.rs b/src/processor.rs index e9ab84c..4191d6f 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,39 +1,79 @@ use yaml::Yaml; use yaml::yaml::{Array, Hash}; +use regex::{Captures, Regex}; use parameter::ParamMap; -pub type ProcessorResult = Result<(), String>; +pub type ProcessorResult = Result, String>; pub fn process_yaml(yaml: &mut Yaml, parameters: &ParamMap) -> ProcessorResult { match yaml { - &mut Yaml::Array(ref mut array) => try!(process_array(array, parameters)), - &mut Yaml::Hash(ref mut hash) => try!(process_hash(hash, parameters)), - &mut Yaml::String(ref mut string) => try!(process_string(string, parameters)), - _ => {}, + &mut Yaml::Array(ref mut array) => process_array(array, parameters), + &mut Yaml::Hash(ref mut hash) => process_hash(hash, parameters), + &mut Yaml::String(ref mut string) => process_string(string, parameters), + _ => Ok(None), } - - Ok(()) } fn process_array(array: &mut Array, parameters: &ParamMap) -> ProcessorResult { for value in array { - try!(process_yaml(value, parameters)); + match process_yaml(value, parameters) { + Ok(Some(new_value)) => *value = new_value, + Err(error) => return Err(error), + _ => {}, + } } - Ok(()) + Ok(None) } fn process_hash(hash: &mut Hash, parameters: &ParamMap) -> ProcessorResult { for (_, value) in hash { - try!(process_yaml(value, parameters)); + match process_yaml(value, parameters) { + Ok(Some(new_value)) => *value = new_value, + Err(error) => return Err(error), + _ => {}, + } } - Ok(()) + Ok(None) } fn process_string(string: &mut String, parameters: &ParamMap) -> ProcessorResult { - // TODO: Interpolate any parameters. + lazy_static! { + static ref LITERAL_INTERPOLATION: Regex = Regex::new( + r"\$\({2}(.*)\){2}" + ).expect("Failed to compile regex."); + } + + lazy_static! { + static ref STRING_INTERPOLATION: Regex = Regex::new( + r"\$\((.*)\)" + ).expect("Failed to compile regex."); + } + + let interpolate = |captures: &Captures| -> String { + let key = captures.at(1).expect("Failed to extract regex capture group."); + + match parameters.get(key) { + Some(parameter) => parameter.value.clone().unwrap_or("~".to_owned()), + None => captures.at(0).expect("Failed to extract regex match.").to_owned(), + } + }; - Ok(()) + let replacement = LITERAL_INTERPOLATION.replace_all(string, &interpolate); + + let contains_literal_replacement = &replacement != string; + + let final_replacement = STRING_INTERPOLATION.replace_all(&replacement, &interpolate); + + let contains_string_replacement = &final_replacement != &replacement; + + if !contains_literal_replacement && !contains_string_replacement { + Ok(None) + } else if contains_literal_replacement && !contains_string_replacement { + Ok(Some(Yaml::from_str(&final_replacement))) + } else { + Ok(Some(Yaml::String(final_replacement))) + } } diff --git a/src/template.rs b/src/template.rs index b30213b..56f5d3c 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,18 +1,17 @@ -use std::collections::HashMap; use std::error::Error; use yaml::{EmitError, Yaml, YamlEmitter, YamlLoader}; -use parameter::Parameter; +use parameter::{ParamMap, Parameter, UserValues}; use processor::process_yaml; pub struct Template { pub objects: Vec, - pub parameters: Vec, + pub param_map: ParamMap, } impl Template { - pub fn from_string(string: String) -> Result { + pub fn new(string: String, user_values: UserValues) -> Result { let docs = try!(YamlLoader::load_from_str(&string).map_err(|err| err.description().to_owned())); if docs.len() != 1 { @@ -31,25 +30,27 @@ impl Template { template_objects.push(object.clone()); } - let mut template_parameters = vec![]; - let parameters = match doc["parameters"].as_vec() { - Some(parameters) => parameters, + let mut param_map = ParamMap::new(); + let parameter_specs = match doc["parameters"].as_vec() { + Some(parameter_specs) => parameter_specs, None => return Err("Key \"parameters\" must be present and must be an array.".to_owned()) }; - for parameter in parameters { - template_parameters.push(try!(Parameter::from_yaml(parameter))); + for parameter_spec in parameter_specs { + let parameter = try!(Parameter::new(parameter_spec, &user_values)); + + param_map.insert(parameter.name.clone(), parameter); } Ok(Template { objects: template_objects, - parameters: template_parameters, + param_map: param_map, }) } - pub fn process(mut self, parameters: HashMap) -> Result { + pub fn process(mut self) -> Result { for object in self.objects.iter_mut() { - try!(process_yaml(object, ¶meters)); + try!(process_yaml(object, &self.param_map)); } dump(self.objects) @@ -58,15 +59,22 @@ impl Template { fn dump(objects: Vec) -> Result { let mut manifests = String::new(); + let last = objects.len() - 1; + + for (i, object) in objects.iter().enumerate() { + { + let mut emitter = YamlEmitter::new(&mut manifests); + try!(emitter.dump(&object).map_err(|error| { + match error { + EmitError::FmtError(error) => format!("{}", error), + EmitError::BadHashmapKey => "Bad hashmap key in YAML structure.".to_owned(), + } + })); + } - for object in objects.iter() { - let mut emitter = YamlEmitter::new(&mut manifests); - try!(emitter.dump(&object).map_err(|error| { - match error { - EmitError::FmtError(error) => format!("{}", error), - EmitError::BadHashmapKey => "Bad hashmap key in YAML structure.".to_owned(), - } - })); + if i != last { + manifests.push_str("\n"); + } } Ok(manifests)