Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update lookup table to handle arbitrary types #75

Merged
merged 1 commit into from
Oct 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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