Skip to content

Commit

Permalink
Merge pull request #16 from bottlerocket-os/helperz
Browse files Browse the repository at this point in the history
feat: enable extensions to expose template helpers
  • Loading branch information
cbgbt authored Oct 26, 2023
2 parents 66e3b7b + 80cd263 commit d516428
Show file tree
Hide file tree
Showing 28 changed files with 555 additions and 37 deletions.
33 changes: 5 additions & 28 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
[package]
name = "bottlerocket-settings-sdk"
version = "0.1.0"
license = "Apache-2.0 OR MIT"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
snafu = "0.7"
tracing = { version = "0.1", features = ["log"] }

[dev-dependencies]
anyhow = "1"
ctor = "0.2"
env_logger = "0.10"
log = "0.4"
maplit = "1"

[features]
default = ["extension", "proto1"]

# If extension is disabled, only the models structures are avalable.
extension = []

# Enable Bottlerocket settings extensions CLI proto1.
proto1 = []
[workspace]
members = [
"bottlerocket-settings-sdk",
"bottlerocket-template-helper",
]
29 changes: 29 additions & 0 deletions bottlerocket-settings-sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "bottlerocket-settings-sdk"
version = "0.1.0"
license = "Apache-2.0 OR MIT"
edition = "2021"

[dependencies]
bottlerocket-template-helper = { path = "../bottlerocket-template-helper", version = "0.1" }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
snafu = "0.7"
tracing = { version = "0.1", features = ["log"] }

[dev-dependencies]
anyhow = "1"
ctor = "0.2"
env_logger = "0.10"
log = "0.4"
maplit = "1"

[features]
default = ["extension", "proto1"]

# If extension is disabled, only the models structures are avalable.
extension = []

# Enable Bottlerocket settings extensions CLI proto1.
proto1 = []
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,17 @@ pub struct MigrateCommand {

#[derive(Args, Debug)]
pub struct TemplateHelperCommand {
/// The version of the setting which should be used
#[arg(long)]
pub setting_version: String,

/// The name of the helper to call
#[arg(long)]
pub helper_name: String,

/// The arguments for the given helper
#[arg(long, value_parser = parse_json)]
pub args: Vec<serde_json::Value>,
pub arg: Vec<serde_json::Value>,
}

/// Helper for `clap` to parse JSON values.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ pub mod error {
#[snafu(display("Set operation failed: {}", source))]
Set { source: BottlerocketSettingError },

#[snafu(display("Template helper execution failed: {}", source))]
TemplateHelper { source: BottlerocketSettingError },

#[snafu(display("Validate operation failed: {}", source))]
Validate { source: BottlerocketSettingError },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! with function name collisions if needed.
use super::{error, SettingsExtensionError};
use crate::cli::proto1::{
GenerateCommand, MigrateCommand, Proto1Command, SetCommand, ValidateCommand,
GenerateCommand, MigrateCommand, Proto1Command, SetCommand, TemplateHelperCommand,
ValidateCommand,
};
use crate::migrate::Migrator;
use crate::model::erased::AsTypeErasedModel;
Expand Down Expand Up @@ -51,9 +52,7 @@ where
Proto1Command::Generate(g) => extension.generate(g).and_then(json_stringify),
Proto1Command::Migrate(m) => extension.migrate(m).and_then(json_stringify),
Proto1Command::Validate(v) => extension.validate(v).map(|_| String::new()),
Proto1Command::Helper(_h) => {
todo!("https://github.com/bottlerocket-os/bottlerocket-settings-sdk/issues/3")
}
Proto1Command::Helper(h) => extension.template_helper(h).and_then(json_stringify),
}
}

Expand All @@ -76,6 +75,10 @@ pub trait Proto1: Debug {
&self,
args: ValidateCommand,
) -> Result<(), SettingsExtensionError<Self::MigratorErrorKind>>;
fn template_helper(
&self,
args: TemplateHelperCommand,
) -> Result<serde_json::Value, SettingsExtensionError<Self::MigratorErrorKind>>;
}

impl<Mi, Mo> Proto1 for SettingsExtension<Mi, Mo>
Expand Down Expand Up @@ -160,4 +163,17 @@ where
.validate(args.value, args.required_settings)
.context(error::ValidateSnafu)
}

fn template_helper(
&self,
args: TemplateHelperCommand,
) -> Result<serde_json::Value, SettingsExtensionError<Self::MigratorErrorKind>> {
self.model(&args.setting_version)
.context(error::NoSuchModelSnafu {
setting_version: args.setting_version,
})?
.as_model()
.execute_template_helper(&args.helper_name, args.arg)
.context(error::TemplateHelperSnafu)
}
}
116 changes: 116 additions & 0 deletions bottlerocket-settings-sdk/src/helper/mod.rs
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;
2 changes: 2 additions & 0 deletions src/lib.rs → bottlerocket-settings-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ tool wishes to invoke a settings extension and parse the output.
pub mod cli;
#[cfg(feature = "extension")]
pub mod extension;
pub mod helper;
#[cfg(feature = "extension")]
pub mod migrate;
pub mod model;

#[cfg(feature = "extension")]
pub use crate::extension::SettingsExtension;
pub use helper::{template_helper, HelperDef, HelperError};
#[cfg(feature = "extension")]
pub use migrate::{
LinearMigrator, LinearMigratorExtensionBuilder, LinearMigratorModel, LinearlyMigrateable,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! This module contains traits which erase the underlying [`SettingsModel`] types, allowing the
//! SDK to refer to the [`SettingsModel`]s as a collection of trait objects.
use super::{error, BottlerocketSetting, BottlerocketSettingError, GenerateResult, SettingsModel};
use snafu::ResultExt;
use snafu::{OptionExt, ResultExt};
use std::any::Any;
use std::fmt::Debug;
use tracing::{debug, instrument};
Expand Down Expand Up @@ -73,6 +73,13 @@ pub trait TypeErasedModel: Debug {
&self,
value: serde_json::Value,
) -> Result<Box<dyn Any>, BottlerocketSettingError>;

/// Executes a template helper associated with this model version.
fn execute_template_helper(
&self,
helper_name: &str,
args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, BottlerocketSettingError>;
}

/// A helper trait used to "upcast" supertraits over the [`TypeErasedModel`] trait.
Expand Down Expand Up @@ -191,6 +198,30 @@ impl<T: SettingsModel + 'static> TypeErasedModel for BottlerocketSetting<T> {
})
}

fn execute_template_helper(
&self,
helper_name: &str,
args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, BottlerocketSettingError> {
let all_helpers = T::template_helpers()
.map_err(Into::into)
.context(error::FetchTemplateHelpersSnafu)?;

let helper = all_helpers
.get(helper_name)
.context(error::FetchTemplateHelperSnafu {
helper_name: helper_name.to_string(),
helper_version: T::get_version(),
})?;

helper
.helper_fn(args)
.context(error::ExecuteTemplateHelperSnafu {
helper_name: helper_name.to_string(),
helper_version: T::get_version(),
})
}

#[instrument(skip(self), err)]
fn parse_erased(
&self,
Expand Down
41 changes: 40 additions & 1 deletion src/model/mod.rs → bottlerocket-settings-sdk/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Provides the [`SettingsModel`] trait interface required to model new settings in the
//! Bottlerocket API using the settings SDK.
use crate::HelperDef;
use serde::Deserialize;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Debug, marker::PhantomData};
use std::collections::HashMap;
use std::fmt::Debug;
use std::marker::PhantomData;

#[doc(hidden)]
pub mod erased;
Expand Down Expand Up @@ -98,6 +101,11 @@ pub trait SettingsModel: Sized + Serialize + DeserializeOwned + Debug {
_value: Self,
_validated_settings: Option<serde_json::Value>,
) -> Result<(), Self::ErrorKind>;

/// Returns the set of template helpers associated with this settings model.
fn template_helpers() -> Result<HashMap<String, Box<dyn HelperDef>>, Self::ErrorKind> {
Ok(HashMap::new())
}
}

/// This struct wraps [`SettingsModel`]s in a referencable object which is passed to the
Expand Down Expand Up @@ -169,6 +177,7 @@ where

mod error {
#![allow(missing_docs)]
use crate::HelperError;
use snafu::Snafu;

/// The error type returned when interacting with a user-defined
Expand All @@ -190,6 +199,36 @@ mod error {
source: serde_json::Error,
},

#[snafu(display(
"Failed to execute helper '{}@{}': {}",
helper_name,
helper_version,
source
))]
ExecuteTemplateHelper {
helper_name: String,
helper_version: &'static str,
source: HelperError,
},

#[snafu(display(
"Failed to find template helper '{}@{}' from settings extension",
helper_name,
helper_version,
))]
FetchTemplateHelper {
helper_name: String,
helper_version: &'static str,
},

#[snafu(display(
"Failed to request template helpers from settings extension: {}",
source
))]
FetchTemplateHelpers {
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},

#[snafu(display(
"Failed to run 'generate' on setting version '{}': {}",
version,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit d516428

Please sign in to comment.