Skip to content

Commit

Permalink
Update lookup table to handle arbitrary types
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 4f574e1
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 4f574e1

Please sign in to comment.