-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit doesn't yet add support for extensions to call helpers, but it does add the HelperDef trait needed to define them, as well as a procedural macro to make it easier to define a helper given a function.
- Loading branch information
Showing
7 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
[workspace] | ||
members = [ | ||
"bottlerocket-settings-sdk", | ||
"bottlerocket-template-helper", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
//! Provides types for creating custom helper functions for use in Bottlerocket's templating engine. | ||
//! | ||
//! See the documentation of [`HelperDef`] for more information. | ||
pub use bottlerocket_template_helper::template_helper; | ||
|
||
/// This trait allows users to create custom helper functions for use in Bottlerocket's templating | ||
/// configuration system. | ||
/// | ||
/// Helpers are used to run arbitrary rendering code when writing config files. | ||
/// | ||
/// # Helper Definitions | ||
/// Any type that implements [`HelperDef`] can be used as a helper. You can use the | ||
/// [`template_helper`] annotation to generate a function that implements [`HelperDef`] for you, so | ||
/// long as: | ||
/// * Your function arguments implement [`serde::Deserialize`] | ||
/// * Your return value is a `Result<T, E>` where `T` implements [`serde::Serialize`] | ||
/// and `E` implements `Into<Box<dyn std::error::Error>>`. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use bottlerocket_settings_sdk::helper::{HelperDef, template_helper}; | ||
/// use serde_json::json; | ||
/// | ||
/// #[template_helper(ident = join_strings_helper)] | ||
/// fn join_strings(lhs: String, rhs: String) -> Result<String, anyhow::Error> { | ||
/// Ok(format!("{}{}", lhs, rhs)) | ||
/// } | ||
/// | ||
/// assert_eq!( | ||
/// join_strings_helper.helper_fn(vec![json!("hello "), json!("world")]).unwrap(), | ||
/// json!("hello world") | ||
/// ); | ||
/// | ||
/// ``` | ||
pub trait HelperDef { | ||
/// Executes the helper. | ||
/// | ||
/// All inputs are provided as a list of JSON values, and a resulting JSON value is expected as | ||
/// output. | ||
fn helper_fn(&self, args: Vec<serde_json::Value>) -> Result<serde_json::Value, HelperError>; | ||
} | ||
|
||
impl<F: Fn(Vec<serde_json::Value>) -> Result<serde_json::Value, HelperError>> HelperDef for F { | ||
fn helper_fn(&self, args: Vec<serde_json::Value>) -> Result<serde_json::Value, HelperError> { | ||
self(args) | ||
} | ||
} | ||
|
||
#[macro_export] | ||
/// Creates a map of helper names to helper definitions. | ||
/// | ||
/// This macro is useful for providing template helpers from a settings model: | ||
/// | ||
/// ``` | ||
/// # use std::collections::HashMap; | ||
/// use bottlerocket_settings_sdk::{ | ||
/// HelperDef, provide_template_helpers, template_helper}; | ||
/// | ||
/// #[template_helper(ident = exclaim_helper)] | ||
/// fn exclaim(s: String) -> Result<String, anyhow::Error> { | ||
/// Ok(format!("{}!", s)) | ||
/// } | ||
/// | ||
/// fn template_helpers() -> HashMap<String, Box<dyn HelperDef>> { | ||
/// provide_template_helpers! { | ||
/// "exclaim" => exclaim_helper, | ||
/// } | ||
/// } | ||
/// ``` | ||
macro_rules! provide_template_helpers { | ||
($($helper_name:expr => $helper:ident),* $(,)?) => { | ||
{ | ||
let mut helpers = std::collections::HashMap::new(); | ||
$( | ||
helpers.insert( | ||
$helper_name.to_string(), | ||
Box::new($helper)as Box<dyn bottlerocket_settings_sdk::HelperDef> | ||
); | ||
)* | ||
helpers | ||
} | ||
}; | ||
} | ||
|
||
mod error { | ||
#![allow(missing_docs)] | ||
use snafu::Snafu; | ||
|
||
/// Error type used in helper definitions. | ||
#[derive(Debug, Snafu)] | ||
#[snafu(visibility(pub))] | ||
pub enum HelperError { | ||
#[snafu(display( | ||
"Helper called with incorrect arity: expected {} args, but {} provided", | ||
expected_args, | ||
provided_args | ||
))] | ||
Arity { | ||
expected_args: usize, | ||
provided_args: usize, | ||
}, | ||
|
||
#[snafu(display("Failed to execute helper: {}", source))] | ||
HelperExecute { | ||
source: Box<dyn std::error::Error + Send + Sync + 'static>, | ||
}, | ||
|
||
#[snafu(display("Failed to parse incoming value from JSON: {}", source))] | ||
JSONParse { source: serde_json::Error }, | ||
|
||
#[snafu(display("Failed to parse outgoing value to JSON: {}", source))] | ||
JSONSerialize { source: serde_json::Error }, | ||
} | ||
} | ||
pub use error::HelperError; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[package] | ||
name = "bottlerocket-template-helper" | ||
version = "0.1.0" | ||
license = "Apache-2.0 OR MIT" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[lib] | ||
path = "src/lib.rs" | ||
proc-macro = true | ||
|
||
[dependencies] | ||
darling = "0.20" | ||
proc-macro2 = "1" | ||
quote = "1" | ||
serde_json = "1" | ||
syn = { version = "1", default-features = false, features = ["full", "parsing", "printing", "proc-macro", "visit-mut"] } | ||
|
||
|
||
[dev-dependencies] | ||
anyhow = "1" | ||
bottlerocket-settings-sdk = { path = "../bottlerocket-settings-sdk", version = "0.1" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
//! This crate provides a procedural macro for defining template helpers in settings extensions. | ||
//! See the documentation in [`bottlerocket-settings-sdk::helper`] for more information. | ||
use darling::{ast::NestedMeta, FromMeta}; | ||
use proc_macro::TokenStream; | ||
use quote::quote; | ||
use syn::{self, FnArg, ItemFn}; | ||
|
||
#[derive(FromMeta)] | ||
struct MacroArgs { | ||
ident: syn::Ident, | ||
vis: Option<String>, | ||
} | ||
|
||
/// Defines a [`bottlerocket-settings-sdk::helper::HelperDef`] based on a given function. | ||
/// | ||
/// This macro requires: | ||
/// * Your function arguments implement [`serde::Deserialize`] | ||
/// * Your return value is a `Result<T, E>` where `T` implements [`serde::Serialize`] | ||
/// and `E` implements `Into<Box<dyn std::error::Error>>`. | ||
/// | ||
/// To define a `HelperDef` called `my_helper` based on a function, you could do something like: | ||
/// | ||
/// ``` | ||
/// use bottlerocket_settings_sdk::helper::{HelperDef, template_helper}; | ||
/// | ||
/// #[template_helper(ident = my_helper)] | ||
/// fn help_with(list_of_things: Vec<String>) -> Result<Vec<String>, anyhow::Error> { | ||
/// Ok(list_of_things | ||
/// .into_iter() | ||
/// .map(|s| format!("Helped with '{s}'!")) | ||
/// .collect()) | ||
/// } | ||
/// ``` | ||
#[proc_macro_attribute] | ||
pub fn template_helper(args: TokenStream, input: TokenStream) -> TokenStream { | ||
let args: MacroArgs = | ||
MacroArgs::from_list(&NestedMeta::parse_meta_list(args.into()).unwrap()).unwrap(); | ||
|
||
let helper_fn_name = args.ident; | ||
|
||
let fn_ast: ItemFn = syn::parse2(input.into()).unwrap(); | ||
let fn_name = fn_ast.sig.ident.clone(); | ||
|
||
let num_args = fn_ast.sig.inputs.len(); | ||
let arg_types: Vec<Box<syn::Type>> = fn_ast | ||
.sig | ||
.inputs | ||
.iter() | ||
.map(|arg| match arg { | ||
FnArg::Receiver(_) => { | ||
panic!("template_helper macro does not work on methods that take `self`") | ||
} | ||
FnArg::Typed(t) => t.ty.clone(), | ||
}) | ||
.collect(); | ||
|
||
let mut helper_fn: ItemFn = syn::parse2(quote! { | ||
fn #helper_fn_name( | ||
args: Vec<serde_json::Value>, | ||
) -> std::result::Result< | ||
serde_json::Value, | ||
bottlerocket_settings_sdk::HelperError | ||
> { | ||
if args.len() != #num_args { | ||
return Err(bottlerocket_settings_sdk::HelperError::Arity { | ||
expected_args: #num_args, | ||
provided_args: args.len(), | ||
}); | ||
} | ||
|
||
// Call the input function with our dynamically generated list of arguments. | ||
// We know that `args` is the correct length because we checked above, so we can let | ||
// the macro unwrap values that it takes. | ||
let mut args = args.into_iter(); | ||
#fn_name(#( | ||
{ | ||
let arg: #arg_types = match serde_json::from_value(args.next().unwrap()) { | ||
Ok(parsed) => parsed, | ||
Err(e) => return Err(bottlerocket_settings_sdk::HelperError::JSONParse { source: e }) | ||
}; | ||
arg | ||
} | ||
),*) | ||
.map_err(|e| bottlerocket_settings_sdk::HelperError::HelperExecute { | ||
source: e.into(), | ||
}) | ||
.and_then(|result| serde_json::to_value(result).map_err(|e| { | ||
bottlerocket_settings_sdk::HelperError::JSONSerialize { source: e } | ||
})) | ||
} | ||
}) | ||
.unwrap(); | ||
|
||
if let Some(visibility) = args.vis { | ||
helper_fn.vis = syn::parse_str(&visibility).unwrap(); | ||
} | ||
|
||
quote! { | ||
#fn_ast | ||
|
||
#helper_fn | ||
} | ||
.into() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use anyhow::Result; | ||
use bottlerocket_settings_sdk::HelperDef; | ||
use bottlerocket_template_helper::template_helper; | ||
use serde_json::json; | ||
|
||
#[template_helper(ident = join_strings_helper)] | ||
fn join_strings(lhs: String, rhs: String) -> Result<String> { | ||
Ok(lhs + &rhs) | ||
} | ||
|
||
#[test] | ||
fn call_join_strings() { | ||
assert_eq!( | ||
join_strings_helper | ||
.helper_fn(vec![json!("hello "), json!("world!")]) | ||
.unwrap(), | ||
json!("hello world!"), | ||
); | ||
|
||
assert!(join_strings_helper(vec![json!("too"), json!("many"), json!("args")]).is_err()); | ||
|
||
assert!(join_strings_helper | ||
.helper_fn(vec![json!("too"), json!("many"), json!("args")]) | ||
.is_err()); | ||
|
||
assert!(join_strings_helper(vec![json!("too few args")]).is_err()); | ||
} | ||
|
||
#[template_helper(ident = no_args_helper)] | ||
fn no_args() -> Result<String> { | ||
Ok(String::new()) | ||
} | ||
|
||
#[test] | ||
fn call_no_args() { | ||
assert_eq!(no_args_helper(vec![]).unwrap(), json!("")); | ||
assert!(no_args_helper(vec![json!("sneaky arg")]).is_err()); | ||
} |