use crate::{specs::*, util::*};
use amq_protocol_types::{AMQPType, AMQPValue};
use handlebars::{
self, to_json, BlockContext, BlockParams, Context, Handlebars, Helper, HelperDef, HelperResult, JsonValue,
Output, RenderContext, RenderError, Renderable, ScopedJson,
};
use serde_json::{self, Value};
use std::{collections::HashMap, fs::File, io::Write, path::Path};
pub type CodeGenerator<'a> = Handlebars<'a>;
pub trait HandlebarsAMQPExtension {
fn register_amqp_helpers(self) -> Self;
fn simple_codegen(
out_dir: &str,
target: &str,
template_name: &str,
template: &str,
var_name: &str,
) {
Self::simple_codegen_with_data(out_dir, target, template_name, template, var_name, None);
}
fn simple_codegen_with_data(
out_dir: &str,
target: &str,
template_name: &str,
template: &str,
var_name: &str,
data: Option<Value>,
);
}
impl<'a> HandlebarsAMQPExtension for CodeGenerator<'a> {
fn register_amqp_helpers(mut self) -> CodeGenerator<'a> {
self.register_escape_fn(handlebars::no_escape);
self.register_helper("camel", Box::new(CamelHelper));
self.register_helper("snake", Box::new(SnakeHelper));
self.register_helper("snake_type", Box::new(SnakeTypeHelper));
self.register_helper("sanitize_name", Box::new(SanitizeNameHelper));
self.register_helper("pass_by_ref", Box::new(PassByRefHelper));
self.register_helper("use_str_ref", Box::new(UseStrRefHelper));
self.register_helper("method_has_flag", Box::new(MethodHasFlagHelper));
self.register_helper("each_argument", Box::new(EachArgumentHelper));
self.register_helper("amqp_value_ref", Box::new(AMQPValueRefHelper));
self
}
fn simple_codegen_with_data(
out_dir: &str,
target: &str,
template_name: &str,
template: &str,
var_name: &str,
metadata: Option<Value>,
) {
let dest_path = Path::new(out_dir).join(format!("{}.rs", target));
let mut f =
File::create(&dest_path).unwrap_or_else(|_| panic!("Failed to create {}.rs", target));
let specs = AMQProtocolDefinition::load(metadata);
let mut codegen = CodeGenerator::default().register_amqp_helpers();
let mut data = HashMap::new();
codegen.set_strict_mode(true);
codegen
.register_template_string(template_name, template.to_string())
.unwrap_or_else(|e| panic!("Failed to register {} template: {}", template_name, e));
data.insert(
var_name.to_string(),
serde_json::to_value(specs)
.unwrap_or_else(|e| panic!("Failed to serialize specs: {}", e)),
);
writeln!(
f,
"{}",
codegen
.render(template_name, &data)
.unwrap_or_else(|err| panic!(
"Failed to render {} template: {}",
template_name, err
))
)
.unwrap_or_else(|e| panic!("Failed to generate {}.rs: {}", target, e));
}
}
pub struct CamelHelper;
impl HelperDef for CamelHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"camel\""))?;
let param = value
.value()
.as_str()
.ok_or_else(|| RenderError::new("Non-string param given to helper \"camel\""))?;
out.write(&camel_case(param))?;
Ok(())
}
}
pub struct SnakeHelper;
impl HelperDef for SnakeHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("First param not found for helper \"snake\""))?;
let raw = h
.param(1)
.and_then(|raw| raw.value().as_bool())
.unwrap_or(true);
let param = value
.value()
.as_str()
.ok_or_else(|| RenderError::new("Non-string first param given to helper \"snake\""))?;
out.write(&snake_case(param, raw))?;
Ok(())
}
}
pub struct SnakeTypeHelper;
impl HelperDef for SnakeTypeHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"snake_type\""))?;
let param: AMQPType = serde_json::from_value(value.value().clone())
.map_err(|_| RenderError::new("Param is not an AMQPType for helper \"snake_type\""))?;
out.write(&snake_case(¶m.to_string(), true))?;
Ok(())
}
}
pub struct SanitizeNameHelper;
impl HelperDef for SanitizeNameHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"sanitize_name\""))?;
let param = value.value().as_str().ok_or_else(|| {
RenderError::new("Non-string param given to helper \"sanitize_name\"")
})?;
out.write(¶m.replace('-', "_"))?;
Ok(())
}
}
pub struct PassByRefHelper;
impl HelperDef for PassByRefHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"pass_by_ref\""))?;
let param: AMQPType = serde_json::from_value(value.value().clone())
.map_err(|_| RenderError::new("Param is not an AMQPType for helper \"pass_by_ref\""))?;
let pass_by_ref = match param {
AMQPType::ShortString
| AMQPType::LongString
| AMQPType::FieldArray
| AMQPType::FieldTable
| AMQPType::ByteArray => true,
_ => false,
};
Ok(Some(ScopedJson::Derived(JsonValue::from(pass_by_ref))))
}
}
pub struct UseStrRefHelper;
impl HelperDef for UseStrRefHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"use_str_ref\""))?;
let param = serde_json::from_value::<AMQPType>(value.value().clone()).ok();
let use_str_ref = match param {
Some(AMQPType::ShortString) | Some(AMQPType::LongString) => true,
_ => false,
};
Ok(Some(ScopedJson::Derived(JsonValue::from(use_str_ref))))
}
}
pub struct MethodHasFlagHelper;
impl HelperDef for MethodHasFlagHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
let arg0 = h.param(0).ok_or_else(|| {
RenderError::new("First param not found for helper \"method_has_flag\"")
})?;
let arg1 = h.param(1).ok_or_else(|| {
RenderError::new("Second param not found for helper \"method_has_flag\"")
})?;
let method = serde_json::from_value::<AMQPMethod>(arg0.value().clone()).map_err(|_| {
RenderError::new("Non-AMQPMethod first param given to helper \"method_has_flag\"")
})?;
let flag = arg1.value().as_str().ok_or_else(|| {
RenderError::new("Non-string second param given to helper \"method_has_flag\"")
})?;
let has_flag = method.arguments.iter().any(|arg| match arg {
AMQPArgument::Value(_) => false,
AMQPArgument::Flags(f) => f.flags.iter().any(|f| f.name == flag),
});
Ok(Some(ScopedJson::Derived(JsonValue::from(has_flag))))
}
}
pub struct EachArgumentHelper;
impl HelperDef for EachArgumentHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
r: &'reg Handlebars<'_>,
ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let value = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"each_argument\""))?;
if let Some(t) = h.template() {
let mut block_context = BlockContext::new();
if let Some(path) = value.context_path() {
*block_context.base_path_mut() = path.to_vec();
}
rc.push_block(block_context);
let arguments: Vec<AMQPArgument> = serde_json::from_value(value.value().clone())
.map_err(|_| {
RenderError::new(
"Param is not a Vec<AMQPArgument> for helper \"each_argument\"",
)
})?;
let len = arguments.len();
let array_path = value.context_path();
for (index, argument) in arguments.iter().enumerate() {
if let Some(ref mut block) = rc.block_mut() {
let (path, is_value) = match *argument {
AMQPArgument::Value(_) => ("Value".to_owned(), true),
AMQPArgument::Flags(_) => ("Flags".to_owned(), false),
};
block.set_local_var("@index".to_string(), to_json(&index));
block.set_local_var("@last".to_string(), to_json(index == len - 1));
block.set_local_var("@argument_is_value".to_string(), to_json(&is_value));
if let Some(ref p) = array_path {
if index == 0 {
let mut path = Vec::with_capacity(p.len() + 1);
path.extend_from_slice(p);
path.push(index.to_string());
*block.base_path_mut() = path;
} else if let Some(ptr) = block.base_path_mut().last_mut() {
*ptr = index.to_string();
}
}
if let Some(block_param) = h.block_param() {
let mut params = BlockParams::new();
params.add_path(block_param, vec![path])?;
block.set_block_params(params);
}
}
t.render(r, ctx, rc, out)?;
}
rc.pop_block();
}
Ok(())
}
}
pub struct AMQPValueRefHelper;
impl HelperDef for AMQPValueRefHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars<'_>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
let arg = h
.param(0)
.ok_or_else(|| RenderError::new("First param not found for helper \"amqp_value\""))?;
let param: AMQPValue = serde_json::from_value(arg.value().clone())
.map_err(|_| RenderError::new("Param is not an AMQPValue for helper \"amqp_value\""))?;
let value = match param {
AMQPValue::Boolean(v) => serde_json::to_value(v)?,
AMQPValue::ShortShortInt(v) => serde_json::to_value(v)?,
AMQPValue::ShortShortUInt(v) => serde_json::to_value(v)?,
AMQPValue::ShortInt(v) => serde_json::to_value(v)?,
AMQPValue::ShortUInt(v) => serde_json::to_value(v)?,
AMQPValue::LongInt(v) => serde_json::to_value(v)?,
AMQPValue::LongUInt(v) => serde_json::to_value(v)?,
AMQPValue::LongLongInt(v) => serde_json::to_value(v)?,
AMQPValue::Float(v) => serde_json::to_value(v)?,
AMQPValue::Double(v) => serde_json::to_value(v)?,
AMQPValue::DecimalValue(v) => serde_json::to_value(v)?,
AMQPValue::ShortString(v) => serde_json::to_value(format!("\"{}\"", v))?,
AMQPValue::LongString(v) => serde_json::to_value(format!("\"{}\"", v))?,
AMQPValue::FieldArray(v) => serde_json::to_value(v)?,
AMQPValue::Timestamp(v) => serde_json::to_value(v)?,
AMQPValue::FieldTable(v) => serde_json::to_value(v)?,
AMQPValue::ByteArray(v) => serde_json::to_value(v)?,
AMQPValue::Void => JsonValue::Null,
};
Ok(Some(ScopedJson::Derived(value)))
}
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::BTreeMap;
pub const TEMPLATE: &'static str = r#"
{{protocol.name}} - {{protocol.major_version}}.{{protocol.minor_version}}.{{protocol.revision}}
{{protocol.copyright}}
port {{protocol.port}}
{{#each protocol.domains ~}}
{{@key}}: {{this}}
{{/each ~}}
{{#each protocol.constants as |constant| ~}}
{{constant.name}} = {{constant.value}}
{{/each ~}}
{{#each protocol.classes as |class| ~}}
{{class.id}} - {{class.name}}
{{#each class.properties as |property| ~}}
{{property.name}}: {{property.type}}
{{/each ~}}
{{#each class.methods as |method| ~}}
{{method.id}} - {{method.name}}
synchronous: {{method.synchronous}}
{{#each_argument method.arguments as |argument| ~}}
{{#if @argument_is_value ~}}
{{argument.name}}({{argument.domain}}): {{argument.type}}
{{else}}
{{#each argument.flags as |flag| ~}}
{{flag.name}}: {{flag.default_value}}
{{/each ~}}
{{/if ~}}
{{/each_argument ~}}
{{/each ~}}
{{/each ~}}
"#;
fn specs() -> AMQProtocolDefinition {
let mut domains = BTreeMap::default();
domains.insert("domain1".to_string(), AMQPType::LongString);
AMQProtocolDefinition {
name: "AMQP".to_string(),
major_version: 0,
minor_version: 9,
revision: 1,
port: 5672,
copyright: "Copyright 1\nCopyright 2".to_string(),
domains,
constants: vec![AMQPConstant {
name: "constant1".to_string(),
amqp_type: AMQPType::ShortUInt,
value: 128,
}],
soft_errors: Vec::default(),
hard_errors: Vec::default(),
classes: vec![AMQPClass {
id: 42,
methods: vec![AMQPMethod {
id: 64,
arguments: vec![
AMQPArgument::Value(AMQPValueArgument {
amqp_type: AMQPType::LongString,
name: "argument1".to_string(),
default_value: Some(AMQPValue::LongString("value1".into())),
domain: Some("domain1".to_string()),
force_default: false,
}),
AMQPArgument::Flags(AMQPFlagsArgument {
ignore_flags: false,
flags: vec![
AMQPFlagArgument {
name: "flag1".to_string(),
default_value: true,
force_default: false,
},
AMQPFlagArgument {
name: "flag2".to_string(),
default_value: false,
force_default: false,
},
],
}),
],
name: "method1".to_string(),
synchronous: true,
content: false,
metadata: Value::default(),
is_reply: false,
ignore_args: false,
c2s: true,
s2c: true,
}],
name: "class1".to_string(),
properties: vec![AMQPProperty {
amqp_type: AMQPType::LongString,
name: "property1".to_string(),
}],
metadata: Value::default(),
}],
}
}
#[test]
fn main_template() {
let mut data = HashMap::new();
let mut codegen = CodeGenerator::default().register_amqp_helpers();
data.insert("protocol".to_string(), specs());
assert!(codegen
.register_template_string("main", TEMPLATE.to_string())
.is_ok());
assert_eq!(
codegen.render("main", &data).unwrap(),
r#"
AMQP - 0.9.1
Copyright 1
Copyright 2
port 5672
domain1: LongString
constant1 = 128
42 - class1
property1: LongString
64 - method1
synchronous: true
argument1(domain1): LongString
flag1: true
flag2: false
"#
);
}
}