Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

thar-be-settings: allow formatting maps into configuration files #408

Merged
merged 1 commit into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 287 additions & 2 deletions workspaces/api/thar-be-settings/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// text at render time.

use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
use serde_json::value::Value;
use snafu::{OptionExt, ResultExt};

/// Potential errors during helper execution
Expand Down Expand Up @@ -33,15 +34,23 @@ mod error {
// handlebars::JsonValue is a serde_json::Value, which implements
// the 'Display' trait and should provide valuable context
#[snafu(display(
"Invalid (non-string) base64 template value: '{}' in template {}",
"Invalid base64 template value, expected {}, got '{}' in template {}",
tjkirch marked this conversation as resolved.
Show resolved Hide resolved
expected,
value,
template
))]
InvalidTemplateValue {
expected: &'static str,
value: handlebars::JsonValue,
template: String,
},

#[snafu(display(
"Missing data and fail-if-missing was set; see given line/col in template '{}'",
template,
))]
MissingTemplateData { template: String },

#[snafu(display(
"Unable to base64 decode string '{}' in template '{}': '{}'",
base64_string,
Expand Down Expand Up @@ -130,6 +139,7 @@ pub fn base64_decode(

// Create an &str from the serde_json::Value
let base64_str = base64_value.as_str().context(error::InvalidTemplateValue {
expected: "string",
value: base64_value.to_owned(),
template: template_name.to_owned(),
})?;
Expand All @@ -155,8 +165,183 @@ pub fn base64_decode(
Ok(())
}

/// `join_map` lets you join together strings in a map with given characters, for example when
/// you're writing values out to a configuration file.
///
/// The map is expected to be a single level deep, with string keys and string values.
///
/// The first parameter is the character to use to join keys to values; the second parameter is the
/// character to use to join pairs; the third parameter is the name of the map. The third
/// parameter is a literal string that describes the behavior you want if the map is missing from
/// settings; "fail-if-missing" to fail the template, or "no-fail-if-missing" to continue but write
/// out nothing for this invocation of the helper.
///
/// Example:
/// {{ join_map "=" "," "fail-if-missing" map }}
/// ...where `map` is: {"hi": "there", "whats": "up"}
/// ...will produce: "hi=there,whats=up"
pub fn join_map(
helper: &Helper<'_, '_>,
_: &Handlebars,
_: &Context,
renderctx: &mut RenderContext<'_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("Starting join_map helper");
let template_name = renderctx
.get_root_template_name()
.map(|i| i.to_string())
.unwrap_or_else(|| "dynamic template".to_string());
trace!("Template name: {}", &template_name);

trace!("Number of params: {}", helper.params().len());
if helper.params().len() != 4 {
return Err(RenderError::from(
error::TemplateHelperError::IncorrectNumberOfParams {
expected: 4,
received: helper.params().len(),
helper: helper.name().to_string(),
template: template_name,
},
));
}

// Pull out the parameters and confirm their types
let join_key_val = helper
.param(0)
.map(|v| v.value())
.context(error::Internal {
msg: "Missing param after confirming there are enough",
})?;
let join_key = join_key_val
.as_str()
.with_context(|| error::InvalidTemplateValue {
expected: "string",
value: join_key_val.to_owned(),
template: template_name.to_owned(),
})?;
trace!("Character used to join keys to values: {}", join_key);

let join_pairs_val = helper
.param(1)
.map(|v| v.value())
.context(error::Internal {
msg: "Missing param after confirming there are enough",
})?;
let join_pairs = join_pairs_val
.as_str()
.with_context(|| error::InvalidTemplateValue {
expected: "string",
value: join_pairs_val.to_owned(),
template: template_name.to_owned(),
})?;
trace!("Character used to join pairs: {}", join_pairs);

let fail_behavior_val = helper
.param(2)
.map(|v| v.value())
.context(error::Internal {
msg: "Missing param after confirming there are enough",
})?;
let fail_behavior_str =
fail_behavior_val
.as_str()
.with_context(|| error::InvalidTemplateValue {
expected: "string",
value: join_pairs_val.to_owned(),
template: template_name.to_owned(),
})?;
let fail_if_missing = match fail_behavior_str {
"fail-if-missing" => true,
"no-fail-if-missing" => false,
_ => {
return Err(RenderError::from(
error::TemplateHelperError::InvalidTemplateValue {
expected: "fail-if-missing or no-fail-if-missing",
value: fail_behavior_val.to_owned(),
template: template_name.to_owned(),
},
))
}
};
trace!(
"Will we fail if missing the specified map: {}",
fail_if_missing
);

let map_value = helper
.param(3)
.map(|v| v.value())
.context(error::Internal {
msg: "Missing param after confirming there are enough",
})?;
// If the requested setting is not set, we check the user's requested fail-if-missing behavior
// to determine whether to fail hard or just write nothing quietly.
if !map_value.is_object() {
if fail_if_missing {
return Err(RenderError::from(
error::TemplateHelperError::MissingTemplateData {
template: template_name.to_owned(),
},
));
} else {
return Ok(());
}
}
let map = map_value.as_object().context(error::Internal {
msg: "Already confirmed map is_object but as_object failed",
})?;
trace!("Map to join: {:?}", map);

// Join the key/value pairs with requested string
let mut pairs = Vec::new();
for (key, val_value) in map.into_iter() {
// We don't want the JSON form of scalars, we want the Display form of the Rust type inside.
let val = match val_value {
// these ones Display as their simple scalar selves
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.to_string(),
// Null not supported; probably don't want blanks in config files, and we don't have a
// use for this yet; consider carefully if/when we do
Value::Null => {
return Err(RenderError::from(
error::TemplateHelperError::InvalidTemplateValue {
expected: "non-null",
value: val_value.to_owned(),
template: template_name.to_owned(),
},
))
}
// composite types unsupported
Value::Array(_) | Value::Object(_) => {
return Err(RenderError::from(
error::TemplateHelperError::InvalidTemplateValue {
expected: "scalar",
value: val_value.to_owned(),
template: template_name.to_owned(),
},
))
}
};

// Do the actual key/value join.
pairs.push(format!("{}{}{}", key, join_key, val));
}

// Join all pairs with the given string.
let joined = pairs.join(join_pairs);
trace!("Joined output: {}", joined);

// Write the string out to the template
out.write(&joined).context(error::TemplateWrite {
template: template_name.to_owned(),
})?;
Ok(())
}

#[cfg(test)]
mod test {
mod test_base64_decode {
use super::*;
use handlebars::TemplateRenderError;
use serde::Serialize;
Expand Down Expand Up @@ -208,3 +393,103 @@ mod test {
.is_err());
}
}

#[cfg(test)]
mod test_join_map {
use super::*;
use handlebars::TemplateRenderError;
use serde::Serialize;
use serde_json::json;

// A thin wrapper around the handlebars render_template method that includes
// setup and registration of helpers
fn setup_and_render_template<T>(tmpl: &str, data: &T) -> Result<String, TemplateRenderError>
where
T: Serialize,
{
let mut registry = Handlebars::new();
registry.register_helper("join_map", Box::new(join_map));

registry.render_template(tmpl, data)
}

#[test]
fn single_pair() {
let result = setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({"map": {"hi": "there"}}),
)
.unwrap();
assert_eq!(result, "hi=there")
}

#[test]
fn basic() {
let result = setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({"map": {"hi": "there", "whats": "up"}}),
)
.unwrap();
assert_eq!(result, "hi=there,whats=up")
}

#[test]
fn number() {
let result = setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({"map": {"hi": 42}}),
)
.unwrap();
assert_eq!(result, "hi=42")
}

#[test]
fn boolean() {
let result = setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({"map": {"hi": true}}),
)
.unwrap();
assert_eq!(result, "hi=true")
}

#[test]
fn invalid_nested_map() {
setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({"map": {"hi": {"too": "deep"}}}),
)
.unwrap_err();
}

#[test]
fn fail_if_missing() {
setup_and_render_template(
"{{join_map \"=\" \",\" \"fail-if-missing\" map}}",
&json!({}),
)
// Requested failure if map was missing, should fail
.unwrap_err();
}

#[test]
fn no_fail_if_missing() {
let result = setup_and_render_template(
"{{join_map \"=\" \",\" \"no-fail-if-missing\" map}}",
&json!({}),
)
.unwrap();
// Requested no failure even if map was missing, should get no output
assert_eq!(result, "")
}

#[test]
fn invalid_fail_if_missing() {
setup_and_render_template(
"{{join_map \"=\" \",\" \"sup\" map}}",
&json!({}),
)
// Invalid failure mode 'sup'
.unwrap_err();
}
}
2 changes: 1 addition & 1 deletion workspaces/api/thar-be-settings/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn write_config_files(

// Get all settings values for config file templates
debug!("Requesting settings values");
let settings = settings::get_settings_from_template(&args.socket_path, &template_registry)?;
let settings = settings::get_settings_from_template(&args.socket_path)?;

// Ensure all files render properly
info!("Rendering config files...");
Expand Down
15 changes: 1 addition & 14 deletions workspaces/api/thar-be-settings/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use itertools::join;
use std::path::Path;

use crate::client;
use crate::template::TemplateKeys;
use crate::Result;

use apiserver::model;
Expand All @@ -11,24 +9,13 @@ use apiserver::model;
/// their values from the API
pub fn get_settings_from_template<P>(
socket_path: P,
registry: &handlebars::Handlebars,
) -> Result<model::Settings>
where
P: AsRef<Path>,
{
// Using the template registry, pull the keys out of the templates
// and query the API to get a structure of Settings which we can
// use to render the templates
debug!("Gathering keys from configuration file templates");
let settings_to_query = registry.get_all_template_keys()?;

debug!("Requesting settings values for template keys");
let query = join(&settings_to_query, ",");

// Query the settings
debug!("Querying API for settings data");
let settings: model::Settings =
client::get_json(socket_path, "/settings", Some(("keys", query)))?;
client::get_json(socket_path, "/settings", None as Option<(String, String)>)?;

trace!("Settings values: {:?}", settings);
Ok(settings)
Expand Down
Loading