Skip to content

Commit

Permalink
Update lookup table to handle arbitrary types (#75)
Browse files Browse the repository at this point in the history
It turns out that cloudformation actually allows arbitrary types
in Fn::Mappings. Within that, it's also not uniform types as well.

This update handles the arbitrary types for non-array-based inputs
and will "opt out" for non-array-based complex structures with an
`any` mode, leaving it up to the user to resolve that problem in
typescript. Also, in order to reuse f64 types, moved WrapperF64
to a primitives folder that spans the gap of parser and ir.
  • Loading branch information
iph committed Oct 23, 2022
1 parent 65a2a9b commit e5f9e96
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 45 deletions.
100 changes: 97 additions & 3 deletions src/ir/mappings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::ir::mappings::OutputType::{Complex, Consistent};
use crate::parser::lookup_table::MappingInnerValue;
use crate::CloudformationParseTree;
use std::collections::HashMap;
Expand All @@ -7,11 +8,34 @@ pub struct MappingInstruction {
pub map: HashMap<String, HashMap<String, MappingInnerValue>>,
}

/// When printing out to a file, sometimes there are non ordinal types in mappings.
/// An example of this is something like:
/// {
/// "DisableScaleIn": true,
/// "ScaleInCooldown": 10
/// }
///
/// The above example has both a number and a bool. This is considered "Complex".
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutputType {
Consistent(MappingInnerValue),
Complex,
}

impl MappingInstruction {
pub fn find_first_type(&self) -> &MappingInnerValue {
pub fn output_type(&self) -> OutputType {
let value = self.map.values().next().unwrap();
let inner_value = value.values().next().unwrap();
inner_value
let first_inner_value = value.values().next().unwrap();

for _outer_map in self.map.values() {
for inner_value in value.values() {
if std::mem::discriminant(inner_value) != std::mem::discriminant(first_inner_value)
{
return Complex;
}
}
}
Consistent(first_inner_value.clone())
}
}
pub fn translate(parse_tree: &CloudformationParseTree) -> Vec<MappingInstruction> {
Expand All @@ -25,3 +49,73 @@ pub fn translate(parse_tree: &CloudformationParseTree) -> Vec<MappingInstruction
}
instructions
}

#[cfg(test)]
mod tests {
use super::*;
macro_rules! map(
{ $($key:expr => $value:expr),+ } => {
{
let mut m = ::std::collections::HashMap::new();
$(
m.insert($key.to_string(), $value);
)+
m
}
};
);

#[test]
fn test_mapping_consistent_string() {
let mapping = MappingInstruction {
name: "TableMappings".into(),
map: map! {
"Table" => map!{
"Key" => MappingInnerValue::String("Value".into()),
"Key2" => MappingInnerValue::String("Value2".into())
}
},
};

let actual_output = mapping.output_type();
let expected_output = OutputType::Consistent(MappingInnerValue::String("Value".into()));
// In the end, we only care if the output is Consistent(string), not the value that is used.
assert_eq!(
std::mem::discriminant(&expected_output),
std::mem::discriminant(&actual_output)
);
}

#[test]
fn test_mapping_consistent_bool() {
let mapping = MappingInstruction {
name: "TableMappings".into(),
map: map! {
"Table" => map!{
"DisableScaleIn" => MappingInnerValue::Bool(true)
}
},
};

let actual_output = mapping.output_type();
let expected_output = OutputType::Consistent(MappingInnerValue::Bool(true));
assert_eq!(expected_output, actual_output);
}

#[test]
fn test_mapping_complex() {
let mapping = MappingInstruction {
name: "TableMappings".into(),
map: map! {
"Table" => map!{
"DisableScaleIn" => MappingInnerValue::Bool(true),
"Cooldown" => MappingInnerValue::Number(10)
}
},
};

let actual_output = mapping.output_type();
let expected_output = OutputType::Complex;
assert_eq!(expected_output, actual_output);
}
}
3 changes: 2 additions & 1 deletion src/ir/resources.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::ir::reference::{Origin, Reference};
use crate::ir::sub::{sub_parse_tree, SubValue};
use crate::parser::resource::{ResourceValue, WrapperF64};
use crate::parser::resource::ResourceValue;
use crate::primitives::WrapperF64;
use crate::specification::{spec, Complexity, SimpleType, Specification};
use crate::{CloudformationParseTree, TransmuteError};
use std::collections::HashMap;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use serde_json::Value;
pub mod integrations;
pub mod ir;
pub mod parser;
pub mod primitives;
pub mod specification;
pub mod synthesizer;

Expand Down
22 changes: 19 additions & 3 deletions src/parser/lookup_table.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::primitives::WrapperF64;
use crate::TransmuteError;
use serde_json::{Map, Value};
use std::collections::HashMap;
Expand Down Expand Up @@ -49,12 +50,18 @@ impl MappingParseTree {

/**
* MappingInnerValue tracks the allowed value types in a Mapping as defined by CloudFormation in the
* link below. Right now that is either a String or List.
* link below. The values are allowed to only be a String or List:
*
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html#mappings-section-structure-syntax
*
* In reality, all values are allowed from the json specification. If we detect any other conflicting
* numbers, then the type becomes "Any" to allow for the strangeness.
*/
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MappingInnerValue {
Number(i64),
Float(WrapperF64),
Bool(bool),
String(String),
List(Vec<String>),
}
Expand All @@ -68,6 +75,9 @@ impl Display for MappingInnerValue {
list_val.iter().map(|val| format!("'{}'", val)).collect();
write!(f, "[{}]", quoted_list_values.join(","))
}
MappingInnerValue::Number(val) => write!(f, "{}", val),
MappingInnerValue::Float(val) => write!(f, "{}", val),
MappingInnerValue::Bool(val) => write!(f, "{}", val),
};
}
}
Expand Down Expand Up @@ -143,7 +153,13 @@ fn ensure_object<'a>(name: &str, obj: &'a Value) -> Result<&'a Map<String, Value
fn ensure_mapping_value_type(name: &str, obj: &Value) -> Result<MappingInnerValue, TransmuteError> {
match obj {
Value::String(x) => Ok(MappingInnerValue::String(x.to_string())),
Value::Number(x) => Ok(MappingInnerValue::String(x.to_string())),
Value::Number(x) => match x.is_f64() {
true => Ok(MappingInnerValue::Float(WrapperF64::new(
x.as_f64().unwrap(),
))),
false => Ok(MappingInnerValue::Number(x.as_i64().unwrap())),
},
Value::Bool(x) => Ok(MappingInnerValue::Bool(*x)),
Value::Array(x) => Ok(MappingInnerValue::List(convert_to_string_vector(x, name)?)),
_ => Err(TransmuteError {
details: format!(
Expand Down
34 changes: 3 additions & 31 deletions src/parser/resource.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::primitives::WrapperF64;
use crate::TransmuteError;
use numberkit::is_digit;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::{f64, fmt};

#[derive(Debug, Eq, PartialEq)]
pub enum ResourceValue {
Expand Down Expand Up @@ -31,32 +31,6 @@ pub enum ResourceValue {

impl ResourceValue {}

#[derive(Debug, Clone, Copy)]
pub struct WrapperF64 {
num: f64,
}

impl WrapperF64 {
pub fn new(num: f64) -> WrapperF64 {
WrapperF64 { num }
}
}

impl PartialEq for WrapperF64 {
fn eq(&self, other: &Self) -> bool {
// It's equal if the diff is very small
(self.num - other.num).abs() < 0.0000001
}
}

impl fmt::Display for WrapperF64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.num)
}
}

impl Eq for WrapperF64 {}

#[derive(Debug, Eq, PartialEq)]
pub struct ResourceParseTree {
pub name: String,
Expand Down Expand Up @@ -182,10 +156,8 @@ pub fn build_resources_recursively(
if is_digit(n.to_string(), false) {
return Ok(ResourceValue::Number(n.as_i64().unwrap()));
}
let val = WrapperF64 {
num: n.as_f64().unwrap(),
};
return Ok(ResourceValue::Double(val));
let v = WrapperF64::new(n.as_f64().unwrap());
return Ok(ResourceValue::Double(v));
}
Value::Array(arr) => {
let mut v = Vec::new();
Expand Down
34 changes: 34 additions & 0 deletions src/primitives/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Primitives are for things that can be outside the scope of parsing and IR and used heavily across both
* Generally, attempt to keep this section to a minimu
*
*/
use std::fmt;

/// WrapperF64 exists because compraisons and outputs into typescripts are annoying with the
/// default f64. Use this whenever referring to a floating point number in CFN standard.
#[derive(Debug, Clone, Copy)]
pub struct WrapperF64 {
num: f64,
}

impl WrapperF64 {
pub fn new(num: f64) -> WrapperF64 {
WrapperF64 { num }
}
}

impl PartialEq for WrapperF64 {
fn eq(&self, other: &Self) -> bool {
// It's equal if the diff is very small
(self.num - other.num).abs() < 0.0000001
}
}

impl fmt::Display for WrapperF64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.num)
}
}

impl Eq for WrapperF64 {}
15 changes: 11 additions & 4 deletions src/synthesizer/typescript_synthesizer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::ir::conditions::ConditionIr;
use crate::ir::mappings::MappingInstruction;
use crate::ir::mappings::{MappingInstruction, OutputType};
use crate::ir::resources::ResourceIr;
use crate::ir::CloudformationProgramIr;
use crate::parser::lookup_table::MappingInnerValue;
Expand Down Expand Up @@ -55,9 +55,16 @@ impl TypescriptSynthesizer {
append_with_newline(output, "\n\t\t// Mappings");

for mapping in ir.mappings.iter() {
let record_type = match mapping.find_first_type() {
MappingInnerValue::String(_) => "Record<string, Record<string, string>>",
MappingInnerValue::List(_) => "Record<string, Record<string, Array<string>>>",
let record_type = match mapping.output_type() {
OutputType::Consistent(inner_type) => match inner_type {
MappingInnerValue::Number(_) | MappingInnerValue::Float(_) => {
"Record<string, Record<string, number>>"
}
MappingInnerValue::Bool(_) => "Record<string, Record<string, bool>>",
MappingInnerValue::String(_) => "Record<string, Record<string, string>>",
MappingInnerValue::List(_) => "Record<string, Record<string, Array<string>>>",
},
OutputType::Complex => "Record<string, Record<string, any>>",
};

append_with_newline(
Expand Down
5 changes: 2 additions & 3 deletions tests/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use noctilucent::parser::resource::{
build_resources, ResourceParseTree, ResourceValue, WrapperF64,
};
use noctilucent::parser::resource::{build_resources, ResourceParseTree, ResourceValue};
use noctilucent::primitives::WrapperF64;
use serde_json::Value;

macro_rules! map(
Expand Down

0 comments on commit e5f9e96

Please sign in to comment.