diff --git a/.github/codecov.yml b/.github/codecov.yml index 87de6419..11288cf4 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -35,7 +35,6 @@ component_management: name: Other paths: - src/errors/** - - src/integrations/** - src/primitives/** - src/specification/** - src/*.rs diff --git a/.github/workflows/pr-actions.yml b/.github/workflows/pr-actions.yml index d189d7a1..22865320 100644 --- a/.github/workflows/pr-actions.yml +++ b/.github/workflows/pr-actions.yml @@ -46,8 +46,6 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - directory: target/coverage + files: target/codecov.json fail_ci_if_error: true - gcov: true - gcov_ignore: "src/main.rs,src/**/tests.rs,tests/**" token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 8dad4944..bc0549da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,10 +35,3 @@ serde_with = "2.0.0" codegen-units = 1 lto = true opt-level = 3 - -[profile.coverage] -inherits = "test" -codegen-units = 1 -incremental = false -opt-level = 0 -panic = "abort" diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 800a33b5..31a907d0 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -32,24 +32,4 @@ impl fmt::Display for TransmuteError { impl Error for TransmuteError {} #[cfg(test)] -mod tests { - use serde::de::Error; - - use super::*; - - #[test] - fn test_transmute_error() { - let error = TransmuteError::new("Test error message"); - - assert_eq!(error.details, "Test error message"); - assert_eq!(error.to_string(), "TransmuteError: Test error message"); - } - - #[test] - fn test_transmute_error_from() { - let yaml_error = serde_yaml::Error::custom("YAML parsing error"); - let transmute_error: TransmuteError = yaml_error.into(); - - assert_eq!(transmute_error.details, "YAML parsing error"); - } -} +mod tests; diff --git a/src/errors/tests.rs b/src/errors/tests.rs new file mode 100644 index 00000000..4de95be6 --- /dev/null +++ b/src/errors/tests.rs @@ -0,0 +1,19 @@ +use serde::de::Error; + +use super::*; + +#[test] +fn test_transmute_error() { + let error = TransmuteError::new("Test error message"); + + assert_eq!(error.details, "Test error message"); + assert_eq!(error.to_string(), "TransmuteError: Test error message"); +} + +#[test] +fn test_transmute_error_from() { + let yaml_error = serde_yaml::Error::custom("YAML parsing error"); + let transmute_error: TransmuteError = yaml_error.into(); + + assert_eq!(transmute_error.details, "YAML parsing error"); +} diff --git a/src/integrations/iam.rs b/src/integrations/iam.rs deleted file mode 100644 index 0b9791d2..00000000 --- a/src/integrations/iam.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct IamResolver {} diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs deleted file mode 100644 index 3d15d97c..00000000 --- a/src/integrations/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod iam; diff --git a/src/ir/conditions.rs b/src/ir/conditions/mod.rs similarity index 77% rename from src/ir/conditions.rs rename to src/ir/conditions/mod.rs index dc760b2f..98ba0773 100644 --- a/src/ir/conditions.rs +++ b/src/ir/conditions/mod.rs @@ -189,62 +189,4 @@ impl ConditionValue { } #[cfg(test)] -mod tests { - use indexmap::IndexMap; - - use crate::ir::conditions::{determine_order, ConditionIr}; - use crate::ir::reference::{Origin, PseudoParameter, Reference}; - use crate::parser::condition::{ConditionFunction, ConditionValue}; - - #[test] - fn test_eq_translation() { - let condition_structure = ConditionFunction::Equals( - ConditionValue::String("us-west-2".into()), - ConditionValue::Ref("AWS::Region".into()), - ); - - let condition_ir = condition_structure.into_ir(); - assert_eq!( - ConditionIr::Equals( - Box::new(ConditionIr::Str("us-west-2".into())), - Box::new(ConditionIr::Ref(Reference::new( - "AWS::Region", - Origin::PseudoParameter(PseudoParameter::Region) - ))) - ), - condition_ir - ); - } - - #[test] - fn test_sorting() { - let a = ConditionFunction::Equals( - ConditionValue::Ref("Foo".into()), - ConditionValue::Ref("Bar".into()), - ); - - let b = ConditionFunction::Not(ConditionValue::Condition("A".into())); - - let hash = IndexMap::from([("A".into(), a), ("B".into(), b)]); - let ordered = determine_order(&hash); - - assert_eq!(ordered, vec!["A", "B"]); - } - - #[test] - fn test_condition_translation() { - let condition_structure: ConditionValue = ConditionValue::Condition("other".into()); - let condition_ir = condition_structure.into_ir(); - assert_eq!( - (ConditionIr::Ref(Reference::new("other", Origin::Condition))), - condition_ir - ); - } - - fn test_simple() { - assert_eq!( - ConditionIr::Str("hi".into()), - ConditionValue::String("hi".into()).into_ir() - ); - } -} +mod tests; diff --git a/src/ir/conditions/tests.rs b/src/ir/conditions/tests.rs new file mode 100644 index 00000000..9a7f6c18 --- /dev/null +++ b/src/ir/conditions/tests.rs @@ -0,0 +1,58 @@ +use indexmap::IndexMap; + +use crate::ir::conditions::{determine_order, ConditionIr}; +use crate::ir::reference::{Origin, PseudoParameter, Reference}; +use crate::parser::condition::{ConditionFunction, ConditionValue}; + +#[test] +fn test_eq_translation() { + let condition_structure = ConditionFunction::Equals( + ConditionValue::String("us-west-2".into()), + ConditionValue::Ref("AWS::Region".into()), + ); + + let condition_ir = condition_structure.into_ir(); + assert_eq!( + ConditionIr::Equals( + Box::new(ConditionIr::Str("us-west-2".into())), + Box::new(ConditionIr::Ref(Reference::new( + "AWS::Region", + Origin::PseudoParameter(PseudoParameter::Region) + ))) + ), + condition_ir + ); +} + +#[test] +fn test_sorting() { + let a = ConditionFunction::Equals( + ConditionValue::Ref("Foo".into()), + ConditionValue::Ref("Bar".into()), + ); + + let b = ConditionFunction::Not(ConditionValue::Condition("A".into())); + + let hash = IndexMap::from([("A".into(), a), ("B".into(), b)]); + let ordered = determine_order(&hash); + + assert_eq!(ordered, vec!["A", "B"]); +} + +#[test] +fn test_condition_translation() { + let condition_structure: ConditionValue = ConditionValue::Condition("other".into()); + let condition_ir = condition_structure.into_ir(); + assert_eq!( + (ConditionIr::Ref(Reference::new("other", Origin::Condition))), + condition_ir + ); +} + +#[test] +fn test_simple() { + assert_eq!( + ConditionIr::Str("hi".into()), + ConditionValue::String("hi".into()).into_ir() + ); +} diff --git a/src/ir/constructor.rs b/src/ir/constructor.rs deleted file mode 100644 index f2a67c17..00000000 --- a/src/ir/constructor.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::parser::parameters::Parameter; -use indexmap::IndexMap; -use voca_rs::case::camel_case; - -pub struct Constructor { - pub inputs: Vec, -} - -impl Constructor { - pub(super) fn from(parse_tree: IndexMap) -> Self { - Self { - inputs: parse_tree - .into_iter() - .map(|(name, param)| ConstructorParameter { - name: camel_case(&name), - description: param.description, - constructor_type: param.parameter_type.to_string(), - default_value: param.default, - }) - .collect(), - } - } -} - -pub struct ConstructorParameter { - pub name: String, - pub description: Option, - pub constructor_type: String, - pub default_value: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::parser; - use parser::parameters::ParameterType; - - #[test] - fn test_constructor_from() { - let mut parse_tree = IndexMap::new(); - parse_tree.insert( - "param1".to_string(), - Parameter { - allowed_values: None, - default: Some("default1".to_string()), - description: Some("description1".to_string()), - parameter_type: ParameterType::String, - }, - ); - parse_tree.insert( - "param2".to_string(), - Parameter { - allowed_values: None, - default: None, - description: Some("description2".to_string()), - parameter_type: ParameterType::Number, - }, - ); - - let constructor = Constructor::from(parse_tree); - - assert_eq!(constructor.inputs.len(), 2); - - assert_eq!(constructor.inputs[0].name, "param1"); - assert_eq!( - constructor.inputs[0].description, - Some("description1".to_string()) - ); - assert_eq!(constructor.inputs[0].constructor_type, "String"); - assert_eq!( - constructor.inputs[0].default_value, - Some("default1".to_string()) - ); - - assert_eq!(constructor.inputs[1].name, "param2"); - assert_eq!( - constructor.inputs[1].description, - Some("description2".to_string()) - ); - assert_eq!(constructor.inputs[1].constructor_type, "Number"); - assert_eq!(constructor.inputs[1].default_value, None); - } - - #[test] - fn test_constructor_parameter() { - let param = ConstructorParameter { - name: "Param1".to_string(), - description: Some("description1".to_string()), - constructor_type: "String".to_string(), - default_value: Some("default1".to_string()), - }; - - assert_eq!(param.name, "Param1"); - assert_eq!(param.description, Some("description1".to_string())); - assert_eq!(param.constructor_type, "String"); - assert_eq!(param.default_value, Some("default1".to_string())); - } -} diff --git a/src/ir/constructor/mod.rs b/src/ir/constructor/mod.rs new file mode 100644 index 00000000..e1b9c89c --- /dev/null +++ b/src/ir/constructor/mod.rs @@ -0,0 +1,33 @@ +use crate::parser::parameters::Parameter; +use indexmap::IndexMap; +use voca_rs::case::camel_case; + +pub struct Constructor { + pub inputs: Vec, +} + +impl Constructor { + pub(super) fn from(parse_tree: IndexMap) -> Self { + Self { + inputs: parse_tree + .into_iter() + .map(|(name, param)| ConstructorParameter { + name: camel_case(&name), + description: param.description, + constructor_type: param.parameter_type.to_string(), + default_value: param.default, + }) + .collect(), + } + } +} + +pub struct ConstructorParameter { + pub name: String, + pub description: Option, + pub constructor_type: String, + pub default_value: Option, +} + +#[cfg(test)] +mod tests; diff --git a/src/ir/constructor/tests.rs b/src/ir/constructor/tests.rs new file mode 100644 index 00000000..276790e3 --- /dev/null +++ b/src/ir/constructor/tests.rs @@ -0,0 +1,65 @@ +use super::*; + +use crate::parser; +use parser::parameters::ParameterType; + +#[test] +fn test_constructor_from() { + let mut parse_tree = IndexMap::new(); + parse_tree.insert( + "param1".to_string(), + Parameter { + allowed_values: None, + default: Some("default1".to_string()), + description: Some("description1".to_string()), + parameter_type: ParameterType::String, + }, + ); + parse_tree.insert( + "param2".to_string(), + Parameter { + allowed_values: None, + default: None, + description: Some("description2".to_string()), + parameter_type: ParameterType::Number, + }, + ); + + let constructor = Constructor::from(parse_tree); + + assert_eq!(constructor.inputs.len(), 2); + + assert_eq!(constructor.inputs[0].name, "param1"); + assert_eq!( + constructor.inputs[0].description, + Some("description1".to_string()) + ); + assert_eq!(constructor.inputs[0].constructor_type, "String"); + assert_eq!( + constructor.inputs[0].default_value, + Some("default1".to_string()) + ); + + assert_eq!(constructor.inputs[1].name, "param2"); + assert_eq!( + constructor.inputs[1].description, + Some("description2".to_string()) + ); + assert_eq!(constructor.inputs[1].constructor_type, "Number"); + assert_eq!(constructor.inputs[1].default_value, None); +} + +#[test] +fn test_constructor_parameter() { + let param = ConstructorParameter { + name: "Param1".to_string(), + description: Some("description1".to_string()), + constructor_type: "String".to_string(), + default_value: Some("default1".to_string()), + }; + + assert_eq!(param.name, "Param1"); + assert_eq!(param.description, Some("description1".to_string())); + assert_eq!(param.constructor_type, "String"); + assert_eq!(param.default_value, Some("default1".to_string())); +} diff --git a/src/ir/dependencies.rs b/src/ir/dependencies/mod.rs similarity index 100% rename from src/ir/dependencies.rs rename to src/ir/dependencies/mod.rs diff --git a/src/ir/importer.rs b/src/ir/importer/mod.rs similarity index 100% rename from src/ir/importer.rs rename to src/ir/importer/mod.rs diff --git a/src/ir/mappings.rs b/src/ir/mappings.rs deleted file mode 100644 index 6fa9b851..00000000 --- a/src/ir/mappings.rs +++ /dev/null @@ -1,119 +0,0 @@ -use indexmap::IndexMap; - -use crate::ir::mappings::OutputType::{Complex, Consistent}; -use crate::parser::lookup_table::{MappingInnerValue, MappingTable}; - -pub struct MappingInstruction { - pub name: String, - pub map: IndexMap>, -} - -/// 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)] -pub enum OutputType { - Consistent(MappingInnerValue), - Complex, -} - -impl MappingInstruction { - pub(super) fn from( - parse_tree: IndexMap, - ) -> Vec { - parse_tree - .into_iter() - .map(|(name, MappingTable { mappings: map, .. })| MappingInstruction { name, map }) - .collect() - } - - pub fn output_type(&self) -> OutputType { - let value = self.map.values().next().unwrap(); - 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()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - macro_rules! map { - ($($key:expr => $value:expr),+) => { - { - let mut m = ::indexmap::IndexMap::::default(); - $( - m.insert($key.into(), $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); - } -} diff --git a/src/ir/mappings/mod.rs b/src/ir/mappings/mod.rs new file mode 100644 index 00000000..ece9b001 --- /dev/null +++ b/src/ir/mappings/mod.rs @@ -0,0 +1,52 @@ +use indexmap::IndexMap; + +use crate::ir::mappings::OutputType::{Complex, Consistent}; +use crate::parser::lookup_table::{MappingInnerValue, MappingTable}; + +pub struct MappingInstruction { + pub name: String, + pub map: IndexMap>, +} + +/// 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)] +pub enum OutputType { + Consistent(MappingInnerValue), + Complex, +} + +impl MappingInstruction { + pub(super) fn from( + parse_tree: IndexMap, + ) -> Vec { + parse_tree + .into_iter() + .map(|(name, MappingTable { mappings: map, .. })| MappingInstruction { name, map }) + .collect() + } + + pub fn output_type(&self) -> OutputType { + let value = self.map.values().next().unwrap(); + 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()) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ir/mappings/tests.rs b/src/ir/mappings/tests.rs new file mode 100644 index 00000000..2010c68d --- /dev/null +++ b/src/ir/mappings/tests.rs @@ -0,0 +1,66 @@ +use super::*; +macro_rules! map { + ($($key:expr => $value:expr),+) => { + { + let mut m = ::indexmap::IndexMap::::default(); + $( + m.insert($key.into(), $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); +} diff --git a/src/ir/outputs.rs b/src/ir/outputs/mod.rs similarity index 96% rename from src/ir/outputs.rs rename to src/ir/outputs/mod.rs index 3fb2a377..f2cfe985 100644 --- a/src/ir/outputs.rs +++ b/src/ir/outputs/mod.rs @@ -7,6 +7,7 @@ use crate::TransmuteError; use super::ReferenceOrigins; +#[derive(Debug, PartialEq)] pub struct OutputInstruction { pub name: String, pub export: Option, @@ -49,3 +50,6 @@ impl OutputInstruction { Ok(list) } } + +#[cfg(test)] +mod tests; diff --git a/src/ir/outputs/tests.rs b/src/ir/outputs/tests.rs new file mode 100644 index 00000000..4b60a2d0 --- /dev/null +++ b/src/ir/outputs/tests.rs @@ -0,0 +1,23 @@ +use crate::CloudformationParseTree; + +use super::*; + +#[test] +pub fn none() { + assert_eq!( + OutputInstruction::from( + IndexMap::new(), + &ReferenceOrigins::new(&CloudformationParseTree { + description: None, + transforms: vec![], + conditions: IndexMap::default(), + mappings: IndexMap::default(), + outputs: IndexMap::default(), + parameters: IndexMap::default(), + resources: IndexMap::default() + }) + ) + .unwrap(), + vec![] + ); +} diff --git a/src/ir/reference.rs b/src/ir/reference/mod.rs similarity index 100% rename from src/ir/reference.rs rename to src/ir/reference/mod.rs diff --git a/src/ir/resources.rs b/src/ir/resources/mod.rs similarity index 91% rename from src/ir/resources.rs rename to src/ir/resources/mod.rs index 5b676b99..d340d063 100644 --- a/src/ir/resources.rs +++ b/src/ir/resources/mod.rs @@ -623,81 +623,4 @@ fn find_dependencies( } #[cfg(test)] -mod tests { - use std::collections::HashSet; - - use indexmap::IndexMap; - - use crate::ir::reference::{Origin, Reference}; - use crate::ir::resources::{order, ResourceInstruction, ResourceIr}; - - #[test] - fn test_ir_ordering() { - let ir_instruction = ResourceInstruction { - name: "A".to_string(), - condition: None, - metadata: None, - deletion_policy: None, - update_policy: None, - dependencies: Vec::new(), - resource_type: "".to_string(), - referrers: HashSet::default(), - properties: IndexMap::default(), - }; - - let later = ResourceInstruction { - name: "B".to_string(), - condition: None, - dependencies: Vec::new(), - metadata: None, - deletion_policy: None, - update_policy: None, - resource_type: "".to_string(), - referrers: HashSet::default(), - properties: create_property( - "something", - ResourceIr::Ref(Reference::new( - "A", - Origin::LogicalId { conditional: false }, - )), - ), - }; - - let misordered = vec![later.clone(), ir_instruction.clone()]; - - let actual = order(misordered); - assert_eq!(actual, vec![ir_instruction, later]); - } - - #[test] - fn test_ref_links() { - let mut ir_instruction = ResourceInstruction { - name: "A".to_string(), - condition: None, - metadata: None, - deletion_policy: None, - update_policy: None, - dependencies: vec!["foo".to_string()], - resource_type: "".to_string(), - referrers: HashSet::default(), - properties: create_property( - "something", - ResourceIr::Ref(Reference::new( - "bar", - Origin::LogicalId { conditional: false }, - )), - ), - }; - - ir_instruction.generate_references(); - - assert_eq!( - ir_instruction.referrers, - HashSet::from(["foo".into(), "bar".into()]) - ); - } - - fn create_property(name: &str, resource: ResourceIr) -> IndexMap { - IndexMap::from([(name.into(), resource)]) - } -} +mod tests; diff --git a/src/ir/resources/tests.rs b/src/ir/resources/tests.rs new file mode 100644 index 00000000..301687cd --- /dev/null +++ b/src/ir/resources/tests.rs @@ -0,0 +1,77 @@ +use std::collections::HashSet; + +use indexmap::IndexMap; + +use crate::ir::reference::{Origin, Reference}; +use crate::ir::resources::{order, ResourceInstruction, ResourceIr}; + +#[test] +fn test_ir_ordering() { + let ir_instruction = ResourceInstruction { + name: "A".to_string(), + condition: None, + metadata: None, + deletion_policy: None, + update_policy: None, + dependencies: Vec::new(), + resource_type: "".to_string(), + referrers: HashSet::default(), + properties: IndexMap::default(), + }; + + let later = ResourceInstruction { + name: "B".to_string(), + condition: None, + dependencies: Vec::new(), + metadata: None, + deletion_policy: None, + update_policy: None, + resource_type: "".to_string(), + referrers: HashSet::default(), + properties: create_property( + "something", + ResourceIr::Ref(Reference::new( + "A", + Origin::LogicalId { conditional: false }, + )), + ), + }; + + let misordered = vec![later.clone(), ir_instruction.clone()]; + + let actual = order(misordered); + assert_eq!(actual, vec![ir_instruction, later]); +} + +#[test] +fn test_ref_links() { + let mut ir_instruction = ResourceInstruction { + name: "A".to_string(), + condition: None, + metadata: None, + deletion_policy: None, + update_policy: None, + dependencies: vec!["foo".to_string()], + resource_type: "".to_string(), + referrers: HashSet::default(), + properties: create_property( + "something", + ResourceIr::Ref(Reference::new( + "bar", + Origin::LogicalId { conditional: false }, + )), + ), + }; + + ir_instruction.generate_references(); + + assert_eq!( + ir_instruction.referrers, + HashSet::from(["foo".into(), "bar".into()]) + ); +} + +#[inline] +fn create_property(name: &str, resource: ResourceIr) -> IndexMap { + IndexMap::from([(name.into(), resource)]) +} diff --git a/src/ir/sub.rs b/src/ir/sub.rs deleted file mode 100644 index 2a8d7f28..00000000 --- a/src/ir/sub.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::TransmuteError; -use nom::branch::alt; -use nom::bytes::complete::{tag, take, take_until}; -use nom::combinator::{map, rest}; -use nom::error::{Error, ErrorKind}; -use nom::multi::many1; -use nom::sequence::delimited; -use nom::Err; -use nom::IResult; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SubValue { - String(String), - Variable(String), -} - -pub fn sub_parse_tree(str: &str) -> Result, TransmuteError> { - let mut full_resolver = many1(inner_resolver); - - match full_resolver(str) { - Ok((remaining, built_subs)) => { - let mut subs = built_subs; - if !remaining.is_empty() { - subs.push(SubValue::String(remaining.to_string())) - } - Ok(subs) - } - - Err(err) => match err { - Err::Incomplete(_) => Err(TransmuteError::new("Should never enter this state")), - Err::Error(e) => Err(TransmuteError::new(e.code.description())), - Err::Failure(e) => Err(TransmuteError::new(e.code.description())), - }, - } -} - -/// inner_resolver will do one of the following: -/// * take until you see a ${ which is the start of the variable bits. -/// * take something like ${ ... } -/// TODO -- there are some Sub strings that will escape the $, that are not captured yet. -/// Will need to rewrite the parse tree to handle character escapes. -fn inner_resolver(str: &str) -> IResult<&str, SubValue> { - // Due to the caller being many1, we will need to create out own EOF error to - // stop the call pattern. - if str.is_empty() { - return IResult::Err(Err::Error(Error::new(str, ErrorKind::Eof))); - } - - let ir = alt(( - map( - delimited(tag("${!"), take_until("}"), take(1usize)), - |var: &str| SubValue::String(format!("${{{var}}}")), - ), - // Attempt to find ${...} and eat those tokens. - map( - delimited(tag("${"), take_until("}"), take(1usize)), - |var: &str| SubValue::Variable(var.to_string()), - ), - // Attempt to eat anything before ${ - map(take_until("${"), |static_str: &str| { - SubValue::String(static_str.to_string()) - }), - // Anything else is probably "the remaining tokens", consume the remaining tokens as no - // other values were found. - map(rest, |static_str: &str| { - SubValue::String(static_str.to_string()) - }), - ))(str); - - let (remaining, res) = ir?; - IResult::Ok((remaining, res)) -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn substitute_arn() -> Result<(), TransmuteError> { - let prefix = String::from("arn:"); - let var = String::from("some_value"); - let postfix = String::from(":constant"); - // for those who don't want to read: arn:${some_value}:constant - let v = sub_parse_tree(format!("{prefix}${{{var}}}{postfix}").as_str())?; - assert_eq!( - v, - vec![ - SubValue::String(prefix), - SubValue::Variable(var), - SubValue::String(postfix) - ] - ); - - Ok(()) - } - - #[test] - fn error_on_missing_brackets() { - let v = sub_parse_tree("arn:${variable"); - assert!(v.is_err()); - } - - #[test] - fn empty_variable() -> Result<(), TransmuteError> { - let v = sub_parse_tree("arn:${}")?; - assert_eq!( - v, - vec![ - SubValue::String("arn:".to_string()), - SubValue::Variable(String::new()) - ] - ); - - Ok(()) - } - - #[test] - fn test_suffix_substitution() -> Result<(), TransmuteError> { - let v = sub_parse_tree("${Tag}-Concatenated")?; - assert_eq!( - v, - vec![ - SubValue::Variable("Tag".to_string()), - SubValue::String(String::from("-Concatenated")) - ] - ); - - Ok(()) - } - - #[test] - fn test_no_substitution() -> Result<(), TransmuteError> { - let v = sub_parse_tree("NoSubstitution")?; - assert_eq!(v, vec![SubValue::String(String::from("NoSubstitution"))]); - - Ok(()) - } - - #[test] - fn test_quotes() -> Result<(), TransmuteError> { - let v = sub_parse_tree("echo \"${lol}\"")?; - assert_eq!( - v, - vec![ - SubValue::String(String::from("echo \"")), - SubValue::Variable(String::from("lol")), - SubValue::String(String::from("\"")), - ] - ); - - Ok(()) - } - - // As quoted in the sub docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html - // To write a dollar sign and curly braces (${}) literally, add an exclamation point (!) after the open curly brace, such as ${!Literal}. CloudFormation resolves this text as ${Literal}. - #[test] - fn test_literal() -> Result<(), TransmuteError> { - let v = sub_parse_tree("echo ${!lol}")?; - assert_eq!( - v, - vec![ - SubValue::String(String::from("echo ")), - SubValue::String(String::from("${lol}")) - ] - ); - - Ok(()) - } -} diff --git a/src/ir/sub/mod.rs b/src/ir/sub/mod.rs new file mode 100644 index 00000000..8e6806b3 --- /dev/null +++ b/src/ir/sub/mod.rs @@ -0,0 +1,75 @@ +use crate::TransmuteError; +use nom::branch::alt; +use nom::bytes::complete::{tag, take, take_until}; +use nom::combinator::{map, rest}; +use nom::error::{Error, ErrorKind}; +use nom::multi::many1; +use nom::sequence::delimited; +use nom::Err; +use nom::IResult; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SubValue { + String(String), + Variable(String), +} + +pub fn sub_parse_tree(str: &str) -> Result, TransmuteError> { + let mut full_resolver = many1(inner_resolver); + + match full_resolver(str) { + Ok((remaining, built_subs)) => { + let mut subs = built_subs; + if !remaining.is_empty() { + subs.push(SubValue::String(remaining.to_string())) + } + Ok(subs) + } + + Err(err) => match err { + Err::Incomplete(_) => Err(TransmuteError::new("Should never enter this state")), + Err::Error(e) => Err(TransmuteError::new(e.code.description())), + Err::Failure(e) => Err(TransmuteError::new(e.code.description())), + }, + } +} + +/// inner_resolver will do one of the following: +/// * take until you see a ${ which is the start of the variable bits. +/// * take something like ${ ... } +/// TODO -- there are some Sub strings that will escape the $, that are not captured yet. +/// Will need to rewrite the parse tree to handle character escapes. +fn inner_resolver(str: &str) -> IResult<&str, SubValue> { + // Due to the caller being many1, we will need to create out own EOF error to + // stop the call pattern. + if str.is_empty() { + return IResult::Err(Err::Error(Error::new(str, ErrorKind::Eof))); + } + + let ir = alt(( + map( + delimited(tag("${!"), take_until("}"), take(1usize)), + |var: &str| SubValue::String(format!("${{{var}}}")), + ), + // Attempt to find ${...} and eat those tokens. + map( + delimited(tag("${"), take_until("}"), take(1usize)), + |var: &str| SubValue::Variable(var.to_string()), + ), + // Attempt to eat anything before ${ + map(take_until("${"), |static_str: &str| { + SubValue::String(static_str.to_string()) + }), + // Anything else is probably "the remaining tokens", consume the remaining tokens as no + // other values were found. + map(rest, |static_str: &str| { + SubValue::String(static_str.to_string()) + }), + ))(str); + + let (remaining, res) = ir?; + IResult::Ok((remaining, res)) +} + +#[cfg(test)] +mod tests; diff --git a/src/ir/sub/tests.rs b/src/ir/sub/tests.rs new file mode 100644 index 00000000..45879149 --- /dev/null +++ b/src/ir/sub/tests.rs @@ -0,0 +1,93 @@ +use super::*; + +#[test] +fn substitute_arn() -> Result<(), TransmuteError> { + let prefix = String::from("arn:"); + let var = String::from("some_value"); + let postfix = String::from(":constant"); + // for those who don't want to read: arn:${some_value}:constant + let v = sub_parse_tree(format!("{prefix}${{{var}}}{postfix}").as_str())?; + assert_eq!( + v, + vec![ + SubValue::String(prefix), + SubValue::Variable(var), + SubValue::String(postfix) + ] + ); + + Ok(()) +} + +#[test] +fn error_on_missing_brackets() { + let v = sub_parse_tree("arn:${variable"); + assert!(v.is_err()); +} + +#[test] +fn empty_variable() -> Result<(), TransmuteError> { + let v = sub_parse_tree("arn:${}")?; + assert_eq!( + v, + vec![ + SubValue::String("arn:".to_string()), + SubValue::Variable(String::new()) + ] + ); + + Ok(()) +} + +#[test] +fn test_suffix_substitution() -> Result<(), TransmuteError> { + let v = sub_parse_tree("${Tag}-Concatenated")?; + assert_eq!( + v, + vec![ + SubValue::Variable("Tag".to_string()), + SubValue::String(String::from("-Concatenated")) + ] + ); + + Ok(()) +} + +#[test] +fn test_no_substitution() -> Result<(), TransmuteError> { + let v = sub_parse_tree("NoSubstitution")?; + assert_eq!(v, vec![SubValue::String(String::from("NoSubstitution"))]); + + Ok(()) +} + +#[test] +fn test_quotes() -> Result<(), TransmuteError> { + let v = sub_parse_tree("echo \"${lol}\"")?; + assert_eq!( + v, + vec![ + SubValue::String(String::from("echo \"")), + SubValue::Variable(String::from("lol")), + SubValue::String(String::from("\"")), + ] + ); + + Ok(()) +} + +// As quoted in the sub docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html +// To write a dollar sign and curly braces (${}) literally, add an exclamation point (!) after the open curly brace, such as ${!Literal}. CloudFormation resolves this text as ${Literal}. +#[test] +fn test_literal() -> Result<(), TransmuteError> { + let v = sub_parse_tree("echo ${!lol}")?; + assert_eq!( + v, + vec![ + SubValue::String(String::from("echo ")), + SubValue::String(String::from("${lol}")) + ] + ); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 039f4015..2c2d463e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,11 @@ -#![allow(dead_code)] - use indexmap::IndexMap; use parser::condition::ConditionFunction; use parser::lookup_table::MappingTable; use parser::output::Output; use parser::parameters::Parameter; -use parser::resource::{ResourceAttributes, ResourceValue}; +use parser::resource::ResourceAttributes; pub mod errors; -pub mod integrations; pub mod ir; pub mod parser; pub mod primitives; @@ -18,21 +15,6 @@ pub mod synthesizer; #[doc(inline)] pub use errors::*; -pub trait CustomIntegration { - fn is_type(resource_type: &str) -> bool; - fn synthesize(rv: &ResourceValue) -> String; -} - -pub struct Import { - package: String, -} - -pub struct CdkBuilder { - // Each cfn resource we use, is in a different package. Each resource will add imports to this - // list. - imports: Vec, -} - #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "PascalCase")] pub struct CloudformationParseTree { diff --git a/src/parser/condition.rs b/src/parser/condition/mod.rs similarity index 52% rename from src/parser/condition.rs rename to src/parser/condition/mod.rs index 68e7ec72..f33e06cf 100644 --- a/src/parser/condition.rs +++ b/src/parser/condition/mod.rs @@ -238,250 +238,4 @@ impl Singleton { } #[cfg(test)] -mod test { - use super::*; - - #[test] - fn function_and() { - let expected = Box::new(ConditionFunction::And(vec![ - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - ])); - - assert_eq!( - expected, - serde_yaml::from_str("!And [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::And: [true, 'false']").unwrap(), - ); - } - - #[test] - fn function_or() { - let expected = Box::new(ConditionFunction::Or(vec![ - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - ])); - - assert_eq!( - expected, - serde_yaml::from_str("!Or [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::Or: [true, 'false']").unwrap(), - ); - } - - #[test] - fn function_equals() { - let expected = Box::new(ConditionFunction::Equals( - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - )); - - assert_eq!( - expected, - serde_yaml::from_str("!Equals [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::Equals: [true, 'false']").unwrap(), - ); - } - - #[test] - fn function_if() { - let expected = Box::new(ConditionFunction::If { - condition_name: "condition".into(), - if_true: ConditionValue::String("true".into()), - if_false: ConditionValue::String("false".into()), - }); - - assert_eq!( - expected, - serde_yaml::from_str("!If [condition, true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::If: [condition, true, 'false']").unwrap(), - ); - } - - #[test] - fn function_not() { - let expected = Box::new(ConditionFunction::Not(ConditionValue::String( - "true".into(), - ))); - - assert_eq!(expected, serde_yaml::from_str("!Not [true]").unwrap()); - assert_eq!(expected, serde_yaml::from_str("Fn::Not: [true]").unwrap()); - - assert_eq!(expected, serde_yaml::from_str("!Not true").unwrap()); - assert_eq!(expected, serde_yaml::from_str("Fn::Not: true").unwrap()); - } - - #[test] - fn condition_function_and() { - let expected = ConditionValue::Function(Box::new(ConditionFunction::And(vec![ - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - ]))); - - assert_eq!( - expected, - serde_yaml::from_str("!And [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::And: [true, 'false']").unwrap(), - ); - } - - #[test] - fn condition_function_or() { - let expected = ConditionValue::Function(Box::new(ConditionFunction::Or(vec![ - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - ]))); - - assert_eq!( - expected, - serde_yaml::from_str("!Or [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::Or: [true, 'false']").unwrap(), - ); - } - - #[test] - fn condition_function_equals() { - let expected = ConditionValue::Function(Box::new(ConditionFunction::Equals( - ConditionValue::String("true".into()), - ConditionValue::String("false".into()), - ))); - - assert_eq!( - expected, - serde_yaml::from_str("!Equals [true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::Equals: [true, 'false']").unwrap(), - ); - } - - #[test] - fn condition_function_if() { - let expected = ConditionValue::Function(Box::new(ConditionFunction::If { - condition_name: "condition".into(), - if_true: ConditionValue::String("true".into()), - if_false: ConditionValue::String("false".into()), - })); - - assert_eq!( - expected, - serde_yaml::from_str("!If [condition, true, 'false']").unwrap(), - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::If: [condition, true, 'false']").unwrap(), - ); - } - - #[test] - fn condition_function_not() { - let expected = ConditionValue::Function(Box::new(ConditionFunction::Not( - ConditionValue::String("true".into()), - ))); - - assert_eq!(expected, serde_yaml::from_str("!Not [true]").unwrap()); - assert_eq!(expected, serde_yaml::from_str("Fn::Not: [true]").unwrap()); - - assert_eq!(expected, serde_yaml::from_str("!Not true").unwrap()); - assert_eq!(expected, serde_yaml::from_str("Fn::Not: true").unwrap()); - } - - #[test] - fn condition_find_in_map() { - let expected = ConditionValue::FindInMap( - Box::new(ConditionValue::String("Map".into())), - Box::new(ConditionValue::String("TLK".into())), - Box::new(ConditionValue::String("SLK".into())), - ); - assert_eq!( - expected, - serde_yaml::from_str("!FindInMap [Map, TLK, SLK]").unwrap() - ); - assert_eq!( - expected, - serde_yaml::from_str("Fn::FindInMap: [Map, TLK, SLK]").unwrap() - ); - } - - #[test] - fn condition_str_bool() { - let expected = ConditionValue::String("true".into()); - assert_eq!(expected, serde_yaml::from_str("true").unwrap()); - } - #[test] - fn condition_str_float() { - let expected = ConditionValue::String("3.1415".into()); - assert_eq!(expected, serde_yaml::from_str("3.1415").unwrap()); - } - #[test] - fn condition_str_ilong() { - let expected = ConditionValue::String("-184467440737095516150".into()); - assert_eq!( - expected, - serde_yaml::from_str("-184467440737095516150").unwrap() - ); - } - #[test] - fn condition_str_int() { - let expected = ConditionValue::String("-1337".into()); - assert_eq!(expected, serde_yaml::from_str("-1337").unwrap()); - } - #[test] - fn condition_str_uint() { - let expected = ConditionValue::String("1337".into()); - assert_eq!(expected, serde_yaml::from_str("1337").unwrap()); - } - #[test] - fn condition_str_ulong() { - let expected = ConditionValue::String("184467440737095516150".into()); - assert_eq!( - expected, - serde_yaml::from_str("184467440737095516150").unwrap() - ); - } - - #[test] - fn condition_str_string() { - let expected = ConditionValue::String("Hello, world!".into()); - assert_eq!(expected, serde_yaml::from_str("'Hello, world!'").unwrap()); - } - - #[test] - fn condition_ref() { - let expected = ConditionValue::Ref("LogicalID".into()); - assert_eq!(expected, serde_yaml::from_str("!Ref LogicalID").unwrap()); - assert_eq!(expected, serde_yaml::from_str("Ref: LogicalID").unwrap()); - } - - #[test] - fn condition_condition() { - let expected = ConditionValue::Condition("LogicalID".into()); - assert_eq!( - expected, - serde_yaml::from_str("!Condition LogicalID").unwrap() - ); - assert_eq!( - expected, - serde_yaml::from_str("Condition: LogicalID").unwrap() - ); - } -} +mod tests; diff --git a/src/parser/condition/tests.rs b/src/parser/condition/tests.rs new file mode 100644 index 00000000..545dfa91 --- /dev/null +++ b/src/parser/condition/tests.rs @@ -0,0 +1,245 @@ +use super::*; + +#[test] +fn function_and() { + let expected = Box::new(ConditionFunction::And(vec![ + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + ])); + + assert_eq!( + expected, + serde_yaml::from_str("!And [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::And: [true, 'false']").unwrap(), + ); +} + +#[test] +fn function_or() { + let expected = Box::new(ConditionFunction::Or(vec![ + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + ])); + + assert_eq!( + expected, + serde_yaml::from_str("!Or [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::Or: [true, 'false']").unwrap(), + ); +} + +#[test] +fn function_equals() { + let expected = Box::new(ConditionFunction::Equals( + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + )); + + assert_eq!( + expected, + serde_yaml::from_str("!Equals [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::Equals: [true, 'false']").unwrap(), + ); +} + +#[test] +fn function_if() { + let expected = Box::new(ConditionFunction::If { + condition_name: "condition".into(), + if_true: ConditionValue::String("true".into()), + if_false: ConditionValue::String("false".into()), + }); + + assert_eq!( + expected, + serde_yaml::from_str("!If [condition, true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::If: [condition, true, 'false']").unwrap(), + ); +} + +#[test] +fn function_not() { + let expected = Box::new(ConditionFunction::Not(ConditionValue::String( + "true".into(), + ))); + + assert_eq!(expected, serde_yaml::from_str("!Not [true]").unwrap()); + assert_eq!(expected, serde_yaml::from_str("Fn::Not: [true]").unwrap()); + + assert_eq!(expected, serde_yaml::from_str("!Not true").unwrap()); + assert_eq!(expected, serde_yaml::from_str("Fn::Not: true").unwrap()); +} + +#[test] +fn condition_function_and() { + let expected = ConditionValue::Function(Box::new(ConditionFunction::And(vec![ + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + ]))); + + assert_eq!( + expected, + serde_yaml::from_str("!And [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::And: [true, 'false']").unwrap(), + ); +} + +#[test] +fn condition_function_or() { + let expected = ConditionValue::Function(Box::new(ConditionFunction::Or(vec![ + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + ]))); + + assert_eq!( + expected, + serde_yaml::from_str("!Or [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::Or: [true, 'false']").unwrap(), + ); +} + +#[test] +fn condition_function_equals() { + let expected = ConditionValue::Function(Box::new(ConditionFunction::Equals( + ConditionValue::String("true".into()), + ConditionValue::String("false".into()), + ))); + + assert_eq!( + expected, + serde_yaml::from_str("!Equals [true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::Equals: [true, 'false']").unwrap(), + ); +} + +#[test] +fn condition_function_if() { + let expected = ConditionValue::Function(Box::new(ConditionFunction::If { + condition_name: "condition".into(), + if_true: ConditionValue::String("true".into()), + if_false: ConditionValue::String("false".into()), + })); + + assert_eq!( + expected, + serde_yaml::from_str("!If [condition, true, 'false']").unwrap(), + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::If: [condition, true, 'false']").unwrap(), + ); +} + +#[test] +fn condition_function_not() { + let expected = ConditionValue::Function(Box::new(ConditionFunction::Not( + ConditionValue::String("true".into()), + ))); + + assert_eq!(expected, serde_yaml::from_str("!Not [true]").unwrap()); + assert_eq!(expected, serde_yaml::from_str("Fn::Not: [true]").unwrap()); + + assert_eq!(expected, serde_yaml::from_str("!Not true").unwrap()); + assert_eq!(expected, serde_yaml::from_str("Fn::Not: true").unwrap()); +} + +#[test] +fn condition_find_in_map() { + let expected = ConditionValue::FindInMap( + Box::new(ConditionValue::String("Map".into())), + Box::new(ConditionValue::String("TLK".into())), + Box::new(ConditionValue::String("SLK".into())), + ); + assert_eq!( + expected, + serde_yaml::from_str("!FindInMap [Map, TLK, SLK]").unwrap() + ); + assert_eq!( + expected, + serde_yaml::from_str("Fn::FindInMap: [Map, TLK, SLK]").unwrap() + ); +} + +#[test] +fn condition_str_bool() { + let expected = ConditionValue::String("true".into()); + assert_eq!(expected, serde_yaml::from_str("true").unwrap()); +} +#[test] +fn condition_str_float() { + let expected = ConditionValue::String("3.1415".into()); + assert_eq!(expected, serde_yaml::from_str("3.1415").unwrap()); +} +#[test] +fn condition_str_ilong() { + let expected = ConditionValue::String("-184467440737095516150".into()); + assert_eq!( + expected, + serde_yaml::from_str("-184467440737095516150").unwrap() + ); +} +#[test] +fn condition_str_int() { + let expected = ConditionValue::String("-1337".into()); + assert_eq!(expected, serde_yaml::from_str("-1337").unwrap()); +} +#[test] +fn condition_str_uint() { + let expected = ConditionValue::String("1337".into()); + assert_eq!(expected, serde_yaml::from_str("1337").unwrap()); +} +#[test] +fn condition_str_ulong() { + let expected = ConditionValue::String("184467440737095516150".into()); + assert_eq!( + expected, + serde_yaml::from_str("184467440737095516150").unwrap() + ); +} + +#[test] +fn condition_str_string() { + let expected = ConditionValue::String("Hello, world!".into()); + assert_eq!(expected, serde_yaml::from_str("'Hello, world!'").unwrap()); +} + +#[test] +fn condition_ref() { + let expected = ConditionValue::Ref("LogicalID".into()); + assert_eq!(expected, serde_yaml::from_str("!Ref LogicalID").unwrap()); + assert_eq!(expected, serde_yaml::from_str("Ref: LogicalID").unwrap()); +} + +#[test] +fn condition_condition() { + let expected = ConditionValue::Condition("LogicalID".into()); + assert_eq!( + expected, + serde_yaml::from_str("!Condition LogicalID").unwrap() + ); + assert_eq!( + expected, + serde_yaml::from_str("Condition: LogicalID").unwrap() + ); +} diff --git a/src/parser/intrinsics.rs b/src/parser/intrinsics/mod.rs similarity index 100% rename from src/parser/intrinsics.rs rename to src/parser/intrinsics/mod.rs diff --git a/src/parser/lookup_table.rs b/src/parser/lookup_table/mod.rs similarity index 100% rename from src/parser/lookup_table.rs rename to src/parser/lookup_table/mod.rs diff --git a/src/parser/output.rs b/src/parser/output/mod.rs similarity index 100% rename from src/parser/output.rs rename to src/parser/output/mod.rs diff --git a/src/parser/parameters.rs b/src/parser/parameters/mod.rs similarity index 65% rename from src/parser/parameters.rs rename to src/parser/parameters/mod.rs index 2bbaaf61..2f666789 100644 --- a/src/parser/parameters.rs +++ b/src/parser/parameters/mod.rs @@ -34,21 +34,4 @@ impl fmt::Display for ParameterType { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parameter_type_display() { - assert_eq!(ParameterType::String.to_string(), "String"); - assert_eq!(ParameterType::Number.to_string(), "Number"); - assert_eq!(ParameterType::ListOfNumbers.to_string(), "List"); - assert_eq!( - ParameterType::CommaDelimitedList.to_string(), - "CommaDelimitedList" - ); - assert_eq!( - ParameterType::Other("CustomType".to_string()).to_string(), - "CustomType" - ); - } -} +mod tests; diff --git a/src/parser/parameters/tests.rs b/src/parser/parameters/tests.rs new file mode 100644 index 00000000..1d3ca3a5 --- /dev/null +++ b/src/parser/parameters/tests.rs @@ -0,0 +1,16 @@ +use super::*; + +#[test] +fn test_parameter_type_display() { + assert_eq!(ParameterType::String.to_string(), "String"); + assert_eq!(ParameterType::Number.to_string(), "Number"); + assert_eq!(ParameterType::ListOfNumbers.to_string(), "List"); + assert_eq!( + ParameterType::CommaDelimitedList.to_string(), + "CommaDelimitedList" + ); + assert_eq!( + ParameterType::Other("CustomType".to_string()).to_string(), + "CustomType" + ); +} diff --git a/src/parser/resource.rs b/src/parser/resource.rs deleted file mode 100644 index e3787bea..00000000 --- a/src/parser/resource.rs +++ /dev/null @@ -1,559 +0,0 @@ -use crate::primitives::WrapperF64; -use indexmap::map::Entry; -use indexmap::IndexMap; -use serde::de::Error; -use std::convert::TryInto; -use std::fmt; - -pub use super::intrinsics::IntrinsicFunction; - -#[derive(Clone, Debug, PartialEq)] -pub enum ResourceValue { - Null, - Bool(bool), - Number(i64), - Double(WrapperF64), - String(String), - Array(Vec), - Object(IndexMap), - - IntrinsicFunction(Box), -} - -impl From<&str> for ResourceValue { - fn from(s: &str) -> Self { - ResourceValue::String(s.to_owned()) - } -} - -impl From for ResourceValue { - fn from(i: IntrinsicFunction) -> Self { - match i { - IntrinsicFunction::Ref(ref_name) if ref_name == "AWS::NoValue" => ResourceValue::Null, - i => ResourceValue::IntrinsicFunction(Box::new(i)), - } - } -} - -impl<'de> serde::de::Deserialize<'de> for ResourceValue { - fn deserialize>(deserializer: D) -> Result { - struct ResourceValueVisitor; - impl<'de> serde::de::Visitor<'de> for ResourceValueVisitor { - type Value = ResourceValue; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a CloudFormation resource value") - } - - fn visit_bool(self, val: bool) -> Result { - Ok(Self::Value::Bool(val)) - } - - fn visit_enum>( - self, - data: A, - ) -> Result { - IntrinsicFunction::from_enum(data).map(Into::into) - } - - fn visit_f64(self, val: f64) -> Result { - Ok(Self::Value::Double(val.into())) - } - - fn visit_i64(self, val: i64) -> Result { - Ok(Self::Value::Number(val)) - } - - fn visit_i128(self, val: i128) -> Result { - if let Ok(val) = val.try_into() { - Ok(Self::Value::Number(val)) - } else { - Ok(Self::Value::Double(val.into())) - } - } - - fn visit_map>( - self, - mut data: A, - ) -> Result { - let mut map = IndexMap::with_capacity(data.size_hint().unwrap_or_default()); - while let Some(key) = data.next_key::()? { - if let Some(intrinsic) = IntrinsicFunction::from_singleton_map(&key, &mut data)? - { - if let Some(extraneous) = data.next_key()? { - return Err(A::Error::unknown_field(extraneous, &[])); - } - return Ok(intrinsic.into()); - } - match map.entry(key) { - Entry::Vacant(entry) => { - entry.insert(data.next_value()?); - } - Entry::Occupied(entry) => { - return Err(A::Error::custom(&format!( - "duplicate object key {key:?}", - key = entry.key() - ))) - } - } - } - Ok(Self::Value::Object(map)) - } - - fn visit_seq>( - self, - mut data: A, - ) -> Result { - let mut vec = Vec::with_capacity(data.size_hint().unwrap_or_default()); - while let Some(elem) = data.next_element()? { - vec.push(elem); - } - Ok(Self::Value::Array(vec)) - } - - fn visit_str(self, val: &str) -> Result { - Ok(Self::Value::String(val.into())) - } - - fn visit_u64(self, val: u64) -> Result { - if let Ok(val) = val.try_into() { - Ok(Self::Value::Number(val)) - } else { - Ok(Self::Value::Double(val.into())) - } - } - - fn visit_u128(self, val: u128) -> Result { - if let Ok(val) = val.try_into() { - Ok(Self::Value::Number(val)) - } else { - Ok(Self::Value::Double(val.into())) - } - } - - fn visit_unit(self) -> Result { - Ok(Self::Value::Null) - } - } - - deserializer.deserialize_any(ResourceValueVisitor) - } -} - -#[derive(Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct ResourceAttributes { - #[serde(rename = "Type")] - pub resource_type: String, - - pub condition: Option, - - pub metadata: Option, - - #[serde(default)] - pub depends_on: Vec, - - pub update_policy: Option, - - pub deletion_policy: Option, - - #[serde(default)] - pub properties: IndexMap, -} - -#[derive(Clone, Copy, Debug, PartialEq, serde_enum_str::Deserialize_enum_str)] -pub enum DeletionPolicy { - Delete, - Retain, - Snapshot, -} - -impl fmt::Display for DeletionPolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Delete => write!(f, "DELETE"), - Self::Retain => write!(f, "RETAIN"), - Self::Snapshot => write!(f, "SNAPSHOT"), - } - } -} - -#[cfg(test)] -mod test { - use serde_yaml::Value; - - use super::*; - - // Bring in the json! macro - include!("../../tests/json.rs"); - - #[test] - fn intrinsic_base64() { - const BASE64_TEXT: &str = "dGVzdAo="; - assert_eq!( - ResourceValue::from_value(json!({ "Fn::Base64": BASE64_TEXT })).unwrap(), - IntrinsicFunction::Base64(ResourceValue::String(BASE64_TEXT.to_string())).into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Base64 {BASE64_TEXT:?}")).unwrap() - ) - .unwrap(), - IntrinsicFunction::Base64(ResourceValue::String(BASE64_TEXT.to_string())).into(), - ); - } - - #[test] - fn intrinsic_cidr() { - const IP_BLOCK: &str = "10.0.0.0"; - const COUNT: i64 = 3; - const CIDR_BITS: i64 = 8; - - assert_eq!( - ResourceValue::from_value(json!({"Fn::Cidr": [IP_BLOCK, COUNT, CIDR_BITS] })).unwrap(), - IntrinsicFunction::Cidr { - ip_block: ResourceValue::String(IP_BLOCK.to_string()), - count: ResourceValue::Number(COUNT), - cidr_bits: ResourceValue::Number(CIDR_BITS) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Cidr [{IP_BLOCK:?}, {COUNT}, {CIDR_BITS}]")) - .unwrap() - ) - .unwrap(), - IntrinsicFunction::Cidr { - ip_block: ResourceValue::String(IP_BLOCK.to_string()), - count: ResourceValue::Number(COUNT), - cidr_bits: ResourceValue::Number(CIDR_BITS) - } - .into(), - ); - - assert_eq!( - ResourceValue::from_value( - json!({"Fn::Cidr": [IP_BLOCK, COUNT.to_string(), CIDR_BITS.to_string()] }) - ) - .unwrap(), - IntrinsicFunction::Cidr { - ip_block: ResourceValue::String(IP_BLOCK.to_string()), - count: ResourceValue::String(COUNT.to_string()), - cidr_bits: ResourceValue::String(CIDR_BITS.to_string()) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!( - "!Cidr [{IP_BLOCK:?}, {:?}, {:?}]", - COUNT.to_string(), - CIDR_BITS.to_string() - )) - .unwrap() - ) - .unwrap(), - IntrinsicFunction::Cidr { - ip_block: ResourceValue::String(IP_BLOCK.to_string()), - count: ResourceValue::String(COUNT.to_string()), - cidr_bits: ResourceValue::String(CIDR_BITS.to_string()) - } - .into(), - ); - } - - #[test] - fn intrinsic_find_in_map() { - const MAP_NAME: &str = "MapName"; - const FIRST_KEY: &str = "FirstKey"; - const SECOND_KEY: &str = "SecondKey"; - assert_eq!( - ResourceValue::from_value(json!({"Fn::FindInMap": [MAP_NAME, FIRST_KEY, SECOND_KEY]})) - .unwrap(), - IntrinsicFunction::FindInMap { - map_name: MAP_NAME.to_string(), - top_level_key: ResourceValue::String(FIRST_KEY.to_string()), - second_level_key: ResourceValue::String(SECOND_KEY.to_string()) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!( - "!FindInMap [{MAP_NAME}, {FIRST_KEY}, {SECOND_KEY}]" - )) - .unwrap() - ) - .unwrap(), - IntrinsicFunction::FindInMap { - map_name: MAP_NAME.to_string(), - top_level_key: ResourceValue::String(FIRST_KEY.to_string()), - second_level_key: ResourceValue::String(SECOND_KEY.to_string()) - } - .into(), - ); - } - - #[test] - fn intrinsic_get_att() { - const LOGICAL_NAME: &str = "MapName"; - const ATTRIBUTE_NAME: &str = "FirstKey"; - assert_eq!( - ResourceValue::from_value(json!({"Fn::GetAtt": [LOGICAL_NAME, ATTRIBUTE_NAME]})) - .unwrap(), - IntrinsicFunction::GetAtt { - logical_name: LOGICAL_NAME.into(), - attribute_name: ATTRIBUTE_NAME.into(), - } - .into(), - ); - // TODO: Confirm the below actually works in CloudFormation (it's not documented!) - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!GetAtt [{LOGICAL_NAME}, {ATTRIBUTE_NAME}]")) - .unwrap() - ) - .unwrap(), - IntrinsicFunction::GetAtt { - logical_name: LOGICAL_NAME.into(), - attribute_name: ATTRIBUTE_NAME.into(), - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!GetAtt {LOGICAL_NAME}.{ATTRIBUTE_NAME}")).unwrap() - ) - .unwrap(), - IntrinsicFunction::GetAtt { - logical_name: LOGICAL_NAME.into(), - attribute_name: ATTRIBUTE_NAME.into(), - } - .into(), - ); - } - - #[test] - fn intrinsic_get_azs() { - const REGION: &str = "test-dummy-1337"; - assert_eq!( - ResourceValue::from_value(json!({ "Fn::GetAZs": REGION })).unwrap(), - IntrinsicFunction::GetAZs(ResourceValue::String(REGION.to_string())).into(), - ); - assert_eq!( - ResourceValue::from_value(serde_yaml::from_str(&format!("!GetAZs {REGION}")).unwrap()) - .unwrap(), - IntrinsicFunction::GetAZs(ResourceValue::String(REGION.to_string())).into(), - ); - } - - #[test] - fn intrinsic_import_value() { - const SHARED_VALUE: &str = "SharedValue.ToImport"; - assert_eq!( - ResourceValue::from_value(json!({ "Fn::ImportValue": SHARED_VALUE })).unwrap(), - IntrinsicFunction::ImportValue(SHARED_VALUE.into()).into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!ImportValue {SHARED_VALUE}")).unwrap() - ) - .unwrap(), - IntrinsicFunction::ImportValue(SHARED_VALUE.into()).into(), - ); - } - - #[test] - fn intrinsic_join() { - const DELIMITER: &str = "/"; - const VALUES: [&str; 3] = ["a", "b", "c"]; - - assert_eq!( - ResourceValue::from_value(json!({"Fn::Join": [DELIMITER, VALUES]})).unwrap(), - IntrinsicFunction::Join { - sep: DELIMITER.into(), - list: ResourceValue::Array( - VALUES - .iter() - .map(|v| ResourceValue::String(v.to_string())) - .collect() - ) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Join [{DELIMITER}, {VALUES:?}]",)).unwrap() - ) - .unwrap(), - IntrinsicFunction::Join { - sep: DELIMITER.into(), - list: ResourceValue::Array( - VALUES - .iter() - .map(|v| ResourceValue::String(v.to_string())) - .collect() - ) - } - .into(), - ); - } - - #[test] - fn intrinsic_select() { - const INDEX: i64 = 1337; - const VALUES: [&str; 3] = ["a", "b", "c"]; - - assert_eq!( - ResourceValue::from_value(json!({"Fn::Select": [INDEX, VALUES]})).unwrap(), - IntrinsicFunction::Select { - index: ResourceValue::Number(INDEX), - list: ResourceValue::Array( - VALUES - .iter() - .map(|v| ResourceValue::String(v.to_string())) - .collect() - ) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Select [{INDEX}, {VALUES:?}]",)).unwrap() - ) - .unwrap(), - IntrinsicFunction::Select { - index: ResourceValue::Number(INDEX), - list: ResourceValue::Array( - VALUES - .iter() - .map(|v| ResourceValue::String(v.to_string())) - .collect() - ) - } - .into(), - ); - } - - #[test] - fn intrinsic_split() { - const DELIMITER: &str = "/"; - const VALUE: &str = "a/b/c"; - - assert_eq!( - ResourceValue::from_value(json!({"Fn::Split": [DELIMITER, VALUE]})).unwrap(), - IntrinsicFunction::Split { - sep: DELIMITER.into(), - string: ResourceValue::String(VALUE.to_string()) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Split [{DELIMITER}, {VALUE}]",)).unwrap() - ) - .unwrap(), - IntrinsicFunction::Split { - sep: DELIMITER.into(), - string: ResourceValue::String(VALUE.to_string()) - } - .into(), - ); - } - - #[test] - fn intrinsic_sub() { - const STRING: &str = "String ${AWS::Region} with ${CUSTOM_VARIABLE}"; - const CUSTOM: i64 = 1337; - - assert_eq!( - ResourceValue::from_value(json!({ "Fn::Sub": STRING })).unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: None - } - .into(), - ); - assert_eq!( - ResourceValue::from_value(json!({ "Fn::Sub": [STRING] })).unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: None - } - .into(), - ); - assert_eq!( - ResourceValue::from_value(serde_yaml::from_str(&format!("!Sub {STRING}")).unwrap()) - .unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: None - } - .into(), - ); - assert_eq!( - ResourceValue::from_value(serde_yaml::from_str(&format!("!Sub [{STRING:?}]")).unwrap()) - .unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: None - } - .into(), - ); - - assert_eq!( - ResourceValue::from_value(json!({ "Fn::Sub": [STRING, {"CUSTOM_VARIABLE": CUSTOM}] })) - .unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: Some(ResourceValue::Object(IndexMap::from([( - "CUSTOM_VARIABLE".to_string(), - ResourceValue::Number(CUSTOM) - )]))) - } - .into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!( - "!Sub [{STRING:?}, {{ CUSTOM_VARIABLE: {CUSTOM} }}]" - )) - .unwrap() - ) - .unwrap(), - IntrinsicFunction::Sub { - string: STRING.into(), - replaces: Some(ResourceValue::Object(IndexMap::from([( - "CUSTOM_VARIABLE".to_string(), - ResourceValue::Number(CUSTOM) - )]))), - } - .into(), - ); - } - - #[test] - fn intrinsic_ref() { - const LOGICAL_NAME: &str = "LogicalName"; - - assert_eq!( - ResourceValue::from_value(json!({ "Ref": LOGICAL_NAME })).unwrap(), - IntrinsicFunction::Ref(LOGICAL_NAME.to_string()).into(), - ); - assert_eq!( - ResourceValue::from_value( - serde_yaml::from_str(&format!("!Ref {LOGICAL_NAME}")).unwrap() - ) - .unwrap(), - IntrinsicFunction::Ref(LOGICAL_NAME.to_string()).into(), - ); - } - - impl ResourceValue { - #[inline(always)] - fn from_value(value: Value) -> Result { - serde_yaml::from_value(value) - } - } -} diff --git a/src/parser/resource/mod.rs b/src/parser/resource/mod.rs new file mode 100644 index 00000000..4b74a7ef --- /dev/null +++ b/src/parser/resource/mod.rs @@ -0,0 +1,186 @@ +use crate::primitives::WrapperF64; +use indexmap::map::Entry; +use indexmap::IndexMap; +use serde::de::Error; +use std::convert::TryInto; +use std::fmt; + +pub use super::intrinsics::IntrinsicFunction; + +#[derive(Clone, Debug, PartialEq)] +pub enum ResourceValue { + Null, + Bool(bool), + Number(i64), + Double(WrapperF64), + String(String), + Array(Vec), + Object(IndexMap), + + IntrinsicFunction(Box), +} + +impl From for ResourceValue { + fn from(i: IntrinsicFunction) -> Self { + match i { + IntrinsicFunction::Ref(ref_name) if ref_name == "AWS::NoValue" => ResourceValue::Null, + i => ResourceValue::IntrinsicFunction(Box::new(i)), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for ResourceValue { + fn deserialize>(deserializer: D) -> Result { + struct ResourceValueVisitor; + impl<'de> serde::de::Visitor<'de> for ResourceValueVisitor { + type Value = ResourceValue; + + #[inline] + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a CloudFormation resource value") + } + + #[inline] + fn visit_bool(self, val: bool) -> Result { + Ok(Self::Value::Bool(val)) + } + + #[inline] + fn visit_enum>( + self, + data: A, + ) -> Result { + IntrinsicFunction::from_enum(data).map(Into::into) + } + + #[inline] + fn visit_f64(self, val: f64) -> Result { + Ok(Self::Value::Double(val.into())) + } + + #[inline] + fn visit_i64(self, val: i64) -> Result { + Ok(Self::Value::Number(val)) + } + + #[cold] + fn visit_i128(self, val: i128) -> Result { + if let Ok(val) = val.try_into() { + Ok(Self::Value::Number(val)) + } else { + Ok(Self::Value::Double(val.into())) + } + } + + fn visit_map>( + self, + mut data: A, + ) -> Result { + let mut map = IndexMap::with_capacity(data.size_hint().unwrap_or_default()); + while let Some(key) = data.next_key::()? { + if let Some(intrinsic) = IntrinsicFunction::from_singleton_map(&key, &mut data)? + { + if let Some(extraneous) = data.next_key()? { + return Err(A::Error::unknown_field(extraneous, &[])); + } + return Ok(intrinsic.into()); + } + match map.entry(key) { + Entry::Vacant(entry) => { + entry.insert(data.next_value()?); + } + Entry::Occupied(entry) => { + return Err(A::Error::custom(&format!( + "duplicate object key {key:?}", + key = entry.key() + ))) + } + } + } + Ok(Self::Value::Object(map)) + } + + fn visit_seq>( + self, + mut data: A, + ) -> Result { + let mut vec = Vec::with_capacity(data.size_hint().unwrap_or_default()); + while let Some(elem) = data.next_element()? { + vec.push(elem); + } + Ok(Self::Value::Array(vec)) + } + + #[inline] + fn visit_str(self, val: &str) -> Result { + Ok(Self::Value::String(val.into())) + } + + #[inline] + fn visit_u64(self, val: u64) -> Result { + if let Ok(val) = val.try_into() { + Ok(Self::Value::Number(val)) + } else { + Ok(Self::Value::Double(val.into())) + } + } + + #[cold] + fn visit_u128(self, val: u128) -> Result { + if let Ok(val) = val.try_into() { + Ok(Self::Value::Number(val)) + } else { + Ok(Self::Value::Double(val.into())) + } + } + + #[inline] + fn visit_unit(self) -> Result { + Ok(Self::Value::Null) + } + } + + deserializer.deserialize_any(ResourceValueVisitor) + } +} + +#[derive(Debug, PartialEq, serde::Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ResourceAttributes { + #[serde(rename = "Type")] + pub resource_type: String, + + pub condition: Option, + + pub metadata: Option, + + #[serde(default)] + pub depends_on: Vec, + + pub update_policy: Option, + + pub deletion_policy: Option, + + #[serde(default)] + pub properties: IndexMap, +} + +#[derive(Clone, Copy, Debug, PartialEq, serde_enum_str::Deserialize_enum_str)] +pub enum DeletionPolicy { + Delete, + Retain, + Snapshot, +} + +impl fmt::Display for DeletionPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Delete => write!(f, "DELETE"), + Self::Retain => write!(f, "RETAIN"), + Self::Snapshot => write!(f, "SNAPSHOT"), + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/parser/resource/tests.rs b/src/parser/resource/tests.rs new file mode 100644 index 00000000..3d2687d1 --- /dev/null +++ b/src/parser/resource/tests.rs @@ -0,0 +1,371 @@ +use serde_yaml::Value; + +use super::*; + +// Bring in the json! macro +include!("../../../tests/json.rs"); + +#[test] +fn intrinsic_base64() { + const BASE64_TEXT: &str = "dGVzdAo="; + assert_eq!( + ResourceValue::from_value(json!({ "Fn::Base64": BASE64_TEXT })).unwrap(), + IntrinsicFunction::Base64(ResourceValue::String(BASE64_TEXT.to_string())).into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!Base64 {BASE64_TEXT:?}")).unwrap() + ) + .unwrap(), + IntrinsicFunction::Base64(ResourceValue::String(BASE64_TEXT.to_string())).into(), + ); +} + +#[test] +fn intrinsic_cidr() { + const IP_BLOCK: &str = "10.0.0.0"; + const COUNT: i64 = 3; + const CIDR_BITS: i64 = 8; + + assert_eq!( + ResourceValue::from_value(json!({"Fn::Cidr": [IP_BLOCK, COUNT, CIDR_BITS] })).unwrap(), + IntrinsicFunction::Cidr { + ip_block: ResourceValue::String(IP_BLOCK.to_string()), + count: ResourceValue::Number(COUNT), + cidr_bits: ResourceValue::Number(CIDR_BITS) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!Cidr [{IP_BLOCK:?}, {COUNT}, {CIDR_BITS}]")).unwrap() + ) + .unwrap(), + IntrinsicFunction::Cidr { + ip_block: ResourceValue::String(IP_BLOCK.to_string()), + count: ResourceValue::Number(COUNT), + cidr_bits: ResourceValue::Number(CIDR_BITS) + } + .into(), + ); + + assert_eq!( + ResourceValue::from_value( + json!({"Fn::Cidr": [IP_BLOCK, COUNT.to_string(), CIDR_BITS.to_string()] }) + ) + .unwrap(), + IntrinsicFunction::Cidr { + ip_block: ResourceValue::String(IP_BLOCK.to_string()), + count: ResourceValue::String(COUNT.to_string()), + cidr_bits: ResourceValue::String(CIDR_BITS.to_string()) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!( + "!Cidr [{IP_BLOCK:?}, {:?}, {:?}]", + COUNT.to_string(), + CIDR_BITS.to_string() + )) + .unwrap() + ) + .unwrap(), + IntrinsicFunction::Cidr { + ip_block: ResourceValue::String(IP_BLOCK.to_string()), + count: ResourceValue::String(COUNT.to_string()), + cidr_bits: ResourceValue::String(CIDR_BITS.to_string()) + } + .into(), + ); +} + +#[test] +fn intrinsic_find_in_map() { + const MAP_NAME: &str = "MapName"; + const FIRST_KEY: &str = "FirstKey"; + const SECOND_KEY: &str = "SecondKey"; + assert_eq!( + ResourceValue::from_value(json!({"Fn::FindInMap": [MAP_NAME, FIRST_KEY, SECOND_KEY]})) + .unwrap(), + IntrinsicFunction::FindInMap { + map_name: MAP_NAME.to_string(), + top_level_key: ResourceValue::String(FIRST_KEY.to_string()), + second_level_key: ResourceValue::String(SECOND_KEY.to_string()) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!( + "!FindInMap [{MAP_NAME}, {FIRST_KEY}, {SECOND_KEY}]" + )) + .unwrap() + ) + .unwrap(), + IntrinsicFunction::FindInMap { + map_name: MAP_NAME.to_string(), + top_level_key: ResourceValue::String(FIRST_KEY.to_string()), + second_level_key: ResourceValue::String(SECOND_KEY.to_string()) + } + .into(), + ); +} + +#[test] +fn intrinsic_get_att() { + const LOGICAL_NAME: &str = "MapName"; + const ATTRIBUTE_NAME: &str = "FirstKey"; + assert_eq!( + ResourceValue::from_value(json!({"Fn::GetAtt": [LOGICAL_NAME, ATTRIBUTE_NAME]})).unwrap(), + IntrinsicFunction::GetAtt { + logical_name: LOGICAL_NAME.into(), + attribute_name: ATTRIBUTE_NAME.into(), + } + .into(), + ); + // TODO: Confirm the below actually works in CloudFormation (it's not documented!) + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!GetAtt [{LOGICAL_NAME}, {ATTRIBUTE_NAME}]")).unwrap() + ) + .unwrap(), + IntrinsicFunction::GetAtt { + logical_name: LOGICAL_NAME.into(), + attribute_name: ATTRIBUTE_NAME.into(), + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!GetAtt {LOGICAL_NAME}.{ATTRIBUTE_NAME}")).unwrap() + ) + .unwrap(), + IntrinsicFunction::GetAtt { + logical_name: LOGICAL_NAME.into(), + attribute_name: ATTRIBUTE_NAME.into(), + } + .into(), + ); +} + +#[test] +fn intrinsic_get_azs() { + const REGION: &str = "test-dummy-1337"; + assert_eq!( + ResourceValue::from_value(json!({ "Fn::GetAZs": REGION })).unwrap(), + IntrinsicFunction::GetAZs(ResourceValue::String(REGION.to_string())).into(), + ); + assert_eq!( + ResourceValue::from_value(serde_yaml::from_str(&format!("!GetAZs {REGION}")).unwrap()) + .unwrap(), + IntrinsicFunction::GetAZs(ResourceValue::String(REGION.to_string())).into(), + ); +} + +#[test] +fn intrinsic_import_value() { + const SHARED_VALUE: &str = "SharedValue.ToImport"; + assert_eq!( + ResourceValue::from_value(json!({ "Fn::ImportValue": SHARED_VALUE })).unwrap(), + IntrinsicFunction::ImportValue(SHARED_VALUE.into()).into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!ImportValue {SHARED_VALUE}")).unwrap() + ) + .unwrap(), + IntrinsicFunction::ImportValue(SHARED_VALUE.into()).into(), + ); +} + +#[test] +fn intrinsic_join() { + const DELIMITER: &str = "/"; + const VALUES: [&str; 3] = ["a", "b", "c"]; + + assert_eq!( + ResourceValue::from_value(json!({"Fn::Join": [DELIMITER, VALUES]})).unwrap(), + IntrinsicFunction::Join { + sep: DELIMITER.into(), + list: ResourceValue::Array( + VALUES + .iter() + .map(|v| ResourceValue::String(v.to_string())) + .collect() + ) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!Join [{DELIMITER}, {VALUES:?}]",)).unwrap() + ) + .unwrap(), + IntrinsicFunction::Join { + sep: DELIMITER.into(), + list: ResourceValue::Array( + VALUES + .iter() + .map(|v| ResourceValue::String(v.to_string())) + .collect() + ) + } + .into(), + ); +} + +#[test] +fn intrinsic_select() { + const INDEX: i64 = 1337; + const VALUES: [&str; 3] = ["a", "b", "c"]; + + assert_eq!( + ResourceValue::from_value(json!({"Fn::Select": [INDEX, VALUES]})).unwrap(), + IntrinsicFunction::Select { + index: ResourceValue::Number(INDEX), + list: ResourceValue::Array( + VALUES + .iter() + .map(|v| ResourceValue::String(v.to_string())) + .collect() + ) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!Select [{INDEX}, {VALUES:?}]",)).unwrap() + ) + .unwrap(), + IntrinsicFunction::Select { + index: ResourceValue::Number(INDEX), + list: ResourceValue::Array( + VALUES + .iter() + .map(|v| ResourceValue::String(v.to_string())) + .collect() + ) + } + .into(), + ); +} + +#[test] +fn intrinsic_split() { + const DELIMITER: &str = "/"; + const VALUE: &str = "a/b/c"; + + assert_eq!( + ResourceValue::from_value(json!({"Fn::Split": [DELIMITER, VALUE]})).unwrap(), + IntrinsicFunction::Split { + sep: DELIMITER.into(), + string: ResourceValue::String(VALUE.to_string()) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!("!Split [{DELIMITER}, {VALUE}]",)).unwrap() + ) + .unwrap(), + IntrinsicFunction::Split { + sep: DELIMITER.into(), + string: ResourceValue::String(VALUE.to_string()) + } + .into(), + ); +} + +#[test] +fn intrinsic_sub() { + const STRING: &str = "String ${AWS::Region} with ${CUSTOM_VARIABLE}"; + const CUSTOM: i64 = 1337; + + assert_eq!( + ResourceValue::from_value(json!({ "Fn::Sub": STRING })).unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: None + } + .into(), + ); + assert_eq!( + ResourceValue::from_value(json!({ "Fn::Sub": [STRING] })).unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: None + } + .into(), + ); + assert_eq!( + ResourceValue::from_value(serde_yaml::from_str(&format!("!Sub {STRING}")).unwrap()) + .unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: None + } + .into(), + ); + assert_eq!( + ResourceValue::from_value(serde_yaml::from_str(&format!("!Sub [{STRING:?}]")).unwrap()) + .unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: None + } + .into(), + ); + + assert_eq!( + ResourceValue::from_value(json!({ "Fn::Sub": [STRING, {"CUSTOM_VARIABLE": CUSTOM}] })) + .unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: Some(ResourceValue::Object(IndexMap::from([( + "CUSTOM_VARIABLE".to_string(), + ResourceValue::Number(CUSTOM) + )]))) + } + .into(), + ); + assert_eq!( + ResourceValue::from_value( + serde_yaml::from_str(&format!( + "!Sub [{STRING:?}, {{ CUSTOM_VARIABLE: {CUSTOM} }}]" + )) + .unwrap() + ) + .unwrap(), + IntrinsicFunction::Sub { + string: STRING.into(), + replaces: Some(ResourceValue::Object(IndexMap::from([( + "CUSTOM_VARIABLE".to_string(), + ResourceValue::Number(CUSTOM) + )]))), + } + .into(), + ); +} + +#[test] +fn intrinsic_ref() { + const LOGICAL_NAME: &str = "LogicalName"; + + assert_eq!( + ResourceValue::from_value(json!({ "Ref": LOGICAL_NAME })).unwrap(), + IntrinsicFunction::Ref(LOGICAL_NAME.to_string()).into(), + ); + assert_eq!( + ResourceValue::from_value(serde_yaml::from_str(&format!("!Ref {LOGICAL_NAME}")).unwrap()) + .unwrap(), + IntrinsicFunction::Ref(LOGICAL_NAME.to_string()).into(), + ); +} + +impl ResourceValue { + #[inline(always)] + fn from_value(value: Value) -> Result { + serde_yaml::from_value(value) + } +} diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index 26660f1a..d1b65795 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -56,33 +56,4 @@ impl From for WrapperF64 { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_from_u64() { - let value: u64 = 42; - let wrapper: WrapperF64 = value.into(); - let expected: WrapperF64 = WrapperF64::new(42.0); - - assert_eq!(wrapper, expected); - } - - #[test] - fn test_from_i128() { - let value: i128 = -10; - let wrapper: WrapperF64 = value.into(); - let expected: WrapperF64 = WrapperF64::new(-10.0); - - assert_eq!(wrapper, expected); - } - - #[test] - fn test_from_u128() { - let value: u128 = 1000; - let wrapper: WrapperF64 = value.into(); - let expected: WrapperF64 = WrapperF64::new(1000.0); - - assert_eq!(wrapper, expected); - } -} +mod tests; diff --git a/src/primitives/tests.rs b/src/primitives/tests.rs new file mode 100644 index 00000000..ef91e187 --- /dev/null +++ b/src/primitives/tests.rs @@ -0,0 +1,28 @@ +use super::*; + +#[test] +fn test_from_u64() { + let value: u64 = 42; + let wrapper: WrapperF64 = value.into(); + let expected: WrapperF64 = WrapperF64::new(42.0); + + assert_eq!(wrapper, expected); +} + +#[test] +fn test_from_i128() { + let value: i128 = -10; + let wrapper: WrapperF64 = value.into(); + let expected: WrapperF64 = WrapperF64::new(-10.0); + + assert_eq!(wrapper, expected); +} + +#[test] +fn test_from_u128() { + let value: u128 = 1000; + let wrapper: WrapperF64 = value.into(); + let expected: WrapperF64 = WrapperF64::new(1000.0); + + assert_eq!(wrapper, expected); +} diff --git a/src/specification/mod.rs b/src/specification/mod.rs index ed947f09..5d85708f 100644 --- a/src/specification/mod.rs +++ b/src/specification/mod.rs @@ -33,10 +33,11 @@ impl Rule { #[derive(Clone, Debug, PartialEq)] pub enum Structure { Simple(CfnType), - Composite(String), + Composite(&'static str), } impl Default for Structure { + #[inline] fn default() -> Self { Self::Simple(CfnType::Json) } @@ -45,7 +46,7 @@ impl Default for Structure { /// CfnType is the primitives in the CloudFormation specification. /// They are when CFN just "doesn't care anymore" and doesn't do anything /// outside of parse-classification-errors. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CfnType { Boolean, Integer, @@ -65,18 +66,11 @@ pub enum TypeRule { } impl TypeRule { - fn to_primitive(self) -> Option { - match self { - Self::Primitive(cfn_type) => Some(cfn_type), - _ => None, - } - } - - fn to_structure(self) -> Structure { + const fn to_structure(self) -> Structure { match self { Self::List(item_type) => item_type.as_structure(), Self::Map(item_type) => item_type.as_structure(), - Self::PropertyType(property_type) => Structure::Composite(property_type.to_string()), + Self::PropertyType(property_type) => Structure::Composite(property_type), Self::Primitive(primitive) => Structure::Simple(primitive), } } @@ -107,16 +101,17 @@ impl PropertyRule { // // - If a "Type" exists, it is a Complex type. // - Otherwise, it is simple and will always have a "PrimitiveType" (different from "PrimitiveItemType") - pub fn get_structure(&self) -> Structure { + #[inline] + pub const fn get_structure(&self) -> Structure { self.type_rule.to_structure() } } impl ItemTypeRule { - fn as_structure(&self) -> Structure { + const fn as_structure(&self) -> Structure { match self { Self::Primitive(primitive) => Structure::Simple(*primitive), - Self::PropertyType(property_type) => Structure::Composite(property_type.to_string()), + Self::PropertyType(property_type) => Structure::Composite(property_type), } } } @@ -129,7 +124,7 @@ pub struct Specification { impl Specification { #[inline(always)] - fn new( + const fn new( property_types: phf::Map<&'static str, Rule>, resource_types: phf::Map<&'static str, phf::Map<&'static str, PropertyRule>>, ) -> Specification { @@ -146,17 +141,13 @@ impl Specification { pub fn full_property_name(complexity: &Structure, resource_type: &str) -> Option { match complexity { Structure::Simple(_) => Option::None, - Structure::Composite(x) => { - let mut full_rule_name = format!("{resource_type}.{x}"); + Structure::Composite("Tag") => { // Every type in CloudFormation has the form: {resource}.{resource_type} // e.g. AWS::Iam::Role.Policy . Tag's lookup name in the specification is "Tag". // no one can explain why. Thanks CFN. - if x == "Tag" { - full_rule_name = "Tag".to_string(); - } - - Option::Some(full_rule_name) + Option::Some("Tag".into()) } + Structure::Composite(x) => Option::Some(format!("{resource_type}.{x}")), } } @@ -178,6 +169,7 @@ impl Specification { } impl Default for Specification { + #[inline] fn default() -> Self { Self::new(spec::PROPERTY_TYPES, spec::RESOURCE_TYPES) } @@ -185,7 +177,7 @@ impl Default for Specification { // Internal enum to look specifically for CustomResources, as they have to be treated differently // in the CFN Spec. -#[derive(Debug, Clone)] +#[derive(Clone, Copy, Debug)] enum ResourceType { Normal, Custom, @@ -242,31 +234,5 @@ impl<'a> ResourceProperties<'a> { } } -#[test] -fn test_pull_json_spec() { - let specification = Specification::default(); - let policy = specification - .property_types - .get("AWS::IAM::Role.Policy") - .unwrap(); - let policy_properties = policy.as_properties().unwrap(); - - assert_eq!( - CfnType::Json, - policy_properties - .get("PolicyDocument") - .unwrap() - .type_rule - .to_primitive() - .unwrap() - ); - assert_eq!( - CfnType::String, - policy_properties - .get("PolicyName") - .unwrap() - .type_rule - .to_primitive() - .unwrap() - ); -} +#[cfg(test)] +mod tests; diff --git a/src/specification/tests.rs b/src/specification/tests.rs new file mode 100644 index 00000000..d07f7e0b --- /dev/null +++ b/src/specification/tests.rs @@ -0,0 +1,20 @@ +use super::*; + +#[test] +fn test_pull_json_spec() { + let specification = Specification::default(); + let policy = specification + .property_types + .get("AWS::IAM::Role.Policy") + .unwrap(); + let policy_properties = policy.as_properties().unwrap(); + + assert_eq!( + TypeRule::Primitive(CfnType::Json), + policy_properties.get("PolicyDocument").unwrap().type_rule + ); + assert_eq!( + TypeRule::Primitive(CfnType::String), + policy_properties.get("PolicyName").unwrap().type_rule + ); +} diff --git a/src/synthesizer/output.rs b/src/synthesizer/output/mod.rs similarity index 100% rename from src/synthesizer/output.rs rename to src/synthesizer/output/mod.rs diff --git a/src/synthesizer/typescript_synthesizer.rs b/src/synthesizer/typescript_synthesizer/mod.rs similarity index 98% rename from src/synthesizer/typescript_synthesizer.rs rename to src/synthesizer/typescript_synthesizer/mod.rs index a92c0e4a..28b8ba63 100644 --- a/src/synthesizer/typescript_synthesizer.rs +++ b/src/synthesizer/typescript_synthesizer/mod.rs @@ -18,6 +18,7 @@ pub struct TypescriptSynthesizer { } impl TypescriptSynthesizer { + #[cfg_attr(coverage_nightly, no_coverage)] #[deprecated(note = "Prefer using the Synthesizer API instead")] pub fn output(ir: CloudformationProgramIr) -> String { let mut output = Vec::new(); @@ -668,16 +669,4 @@ fn pretty_name(name: &str) -> String { } #[cfg(test)] -mod test { - use super::*; - - #[test] - fn pretty_name_fixes() { - assert_eq!("vpc", pretty_name("VPC")); - assert_eq!("objectAccess", pretty_name("GetObject")); - assert_eq!("equalTo", pretty_name("Equals")); - assert_eq!("providerArns", pretty_name("ProviderARNs")); - assert_eq!("targetAZs", pretty_name("TargetAZs")); - assert_eq!("diskSizeMBs", pretty_name("DiskSizeMBs")); - } -} +mod tests; diff --git a/src/synthesizer/typescript_synthesizer/tests.rs b/src/synthesizer/typescript_synthesizer/tests.rs new file mode 100644 index 00000000..74dd071a --- /dev/null +++ b/src/synthesizer/typescript_synthesizer/tests.rs @@ -0,0 +1,11 @@ +use super::*; + +#[test] +fn pretty_name_fixes() { + assert_eq!("vpc", pretty_name("VPC")); + assert_eq!("objectAccess", pretty_name("GetObject")); + assert_eq!("equalTo", pretty_name("Equals")); + assert_eq!("providerArns", pretty_name("ProviderARNs")); + assert_eq!("targetAZs", pretty_name("TargetAZs")); + assert_eq!("diskSizeMBs", pretty_name("DiskSizeMBs")); +} diff --git a/tasks/coverage.sh b/tasks/coverage.sh index fbfdde86..d5276adb 100755 --- a/tasks/coverage.sh +++ b/tasks/coverage.sh @@ -1,39 +1,25 @@ #/usr/bin/env bash set -euo pipefail -COVERAGE_ROOT="${PWD}/target/coverage" +# Install llvm-tools component if needed... +rustup component add llvm-tools -mkdir -p ${COVERAGE_ROOT}/profraw - -if ! command -v grcov >/dev/null; then - echo 'Installing grcov...' - cargo install grcov +# Install cargo-llvm-cov if it's not already there... +if ! command -v cargo-llvm-cov >/dev/null; then + echo 'Installing cargo-llvm-cov...' + cargo install cargo-llvm-cov fi -# We trap EXIT to collect coverage & clean-up profraw files... -function after_tests(){ - echo 'Generating coverage reports...' - grcov "${COVERAGE_ROOT}" \ - --binary-path "${COVERAGE_ROOT}" \ - --source-dir "${PWD}" \ - --output-types "html,lcov" \ - --branch \ - --ignore-not-existing \ - --keep-only "src/*" \ - --ignore "src/main.rs" \ - --output-path "${COVERAGE_ROOT}" \ - --commit-sha $(git rev-parse HEAD) \ - --service-name "noctilucent" - - # Rename `lcov` to a name that is aligned with what IDEs usually look for... - mv "${COVERAGE_ROOT}/lcov" "${COVERAGE_ROOT}/lcov.info" +cargo llvm-cov \ + --all-features \ + --ignore-filename-regex '^(tests/.*\.rs|.*/tests\.rs)$' \ + --no-fail-fast \ + --codecov --output-path target/codecov.json - echo 'Cleaning up...' - rm -rf "${COVERAGE_ROOT}/deps/*.gcda" -} -trap after_tests EXIT +cargo llvm-cov report \ + --hide-instantiations \ + --ignore-filename-regex '^(tests/.*\.rs|.*/tests\.rs)$' \ + --html --output-dir target/coverage -echo 'Running tests with coverage instrumentation...' -RUSTC_BOOTSTRAP=1 \ -RUSTFLAGS='-Zprofile -Clink-dead-code -Coverflow-checks=off' \ -cargo test --profile=coverage +cargo llvm-cov report \ + --ignore-filename-regex '^(tests/.*\.rs|.*/tests\.rs)$'