cfndsl 0.15.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +6 -6
  4. data/.travis.yml +10 -5
  5. data/CHANGELOG.md +721 -400
  6. data/Gemfile +2 -0
  7. data/README.md +85 -23
  8. data/Rakefile +28 -6
  9. data/TODO.md +18 -0
  10. data/UPGRADING.md +22 -0
  11. data/cfndsl.gemspec +17 -16
  12. data/exe/cfndsl +5 -0
  13. data/lib/cfndsl.rb +3 -113
  14. data/lib/cfndsl/aws/cloud_formation_template.rb +10 -1
  15. data/lib/cfndsl/aws/patches/000_CloudFormationResourceSpecification.json +51726 -0
  16. data/lib/cfndsl/aws/patches/000_sam.spec.json +1242 -0
  17. data/lib/cfndsl/aws/patches/100_sam.spec_DeploymentPreference_patch.json +64 -0
  18. data/lib/cfndsl/aws/patches/200_Scrutinies_patch.json +86 -0
  19. data/lib/cfndsl/aws/patches/500_Cognito_IdentityPoolRoleAttachment_patches.json +25 -0
  20. data/lib/cfndsl/aws/patches/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +20 -0
  21. data/lib/cfndsl/aws/patches/500_NetworkAclEntry_patch.json +16 -0
  22. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Event_Events_patch.json +16 -0
  23. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Location_Version_patch.json +16 -0
  24. data/lib/cfndsl/aws/patches/500_SSM_AssociationName_patch.json +16 -0
  25. data/lib/cfndsl/aws/patches/500_VPCEndpoint_patch.json +17 -0
  26. data/lib/cfndsl/aws/patches/510_ElasticSearch_Domain_patches.json +15 -0
  27. data/lib/cfndsl/aws/patches/520_ServiceDiscovery_InstanceAttributes_patch.json +16 -0
  28. data/lib/cfndsl/aws/patches/600_RefKinds_patch.json +3654 -0
  29. data/lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json +20 -0
  30. data/lib/cfndsl/aws/patches/800_List_types_patch.json +115 -0
  31. data/lib/cfndsl/aws/resource_specification.json +39955 -9293
  32. data/lib/cfndsl/aws/types.rb +5 -3
  33. data/lib/cfndsl/cfnlego.rb +34 -0
  34. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.erb +0 -0
  35. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.rb +3 -1
  36. data/lib/{cfnlego → cfndsl/cfnlego}/resource.rb +5 -8
  37. data/lib/cfndsl/cloudformation.rb +114 -0
  38. data/lib/cfndsl/conditions.rb +13 -1
  39. data/lib/cfndsl/creation_policy.rb +3 -1
  40. data/lib/cfndsl/deep_merge.rb +4 -0
  41. data/lib/cfndsl/external_parameters.rb +12 -13
  42. data/lib/cfndsl/globals.rb +51 -10
  43. data/lib/cfndsl/json_serialisable_object.rb +4 -2
  44. data/lib/cfndsl/jsonable.rb +51 -68
  45. data/lib/cfndsl/mappings.rb +3 -1
  46. data/lib/cfndsl/module.rb +18 -5
  47. data/lib/cfndsl/names.rb +2 -0
  48. data/lib/cfndsl/orchestration_template.rb +193 -73
  49. data/lib/cfndsl/outputs.rb +7 -1
  50. data/lib/cfndsl/parameters.rb +3 -1
  51. data/lib/cfndsl/plurals.rb +23 -10
  52. data/lib/cfndsl/properties.rb +3 -1
  53. data/lib/cfndsl/rake_task.rb +217 -15
  54. data/lib/cfndsl/ref_check.rb +21 -11
  55. data/lib/cfndsl/resources.rb +8 -19
  56. data/lib/cfndsl/rules.rb +46 -0
  57. data/lib/cfndsl/runner.rb +143 -0
  58. data/lib/cfndsl/specification.rb +82 -84
  59. data/lib/cfndsl/types.rb +212 -95
  60. data/lib/cfndsl/update_policy.rb +3 -1
  61. data/lib/cfndsl/version.rb +3 -1
  62. data/lib/deep_merge/core.rb +10 -6
  63. data/lib/deep_merge/deep_merge.rb +3 -1
  64. data/sample/autoscale.rb +1 -1
  65. data/sample/autoscale2.rb +4 -3
  66. data/sample/circular.rb +2 -0
  67. data/sample/codedeploy.rb +3 -1
  68. data/sample/config_service.rb +5 -3
  69. data/sample/ecs.rb +3 -1
  70. data/sample/export.rb +5 -3
  71. data/sample/iam_policies.rb +2 -0
  72. data/sample/import.rb +4 -2
  73. data/sample/lambda.rb +3 -1
  74. data/sample/s3.rb +2 -0
  75. data/sample/t1.rb +3 -1
  76. data/sample/vpc_example.rb +3 -1
  77. data/sample/vpc_with_vpn_example.rb +3 -1
  78. data/spec/aws/ec2_security_group_spec.rb +2 -0
  79. data/spec/aws/ecs_task_definition_spec.rb +2 -0
  80. data/spec/aws/iam_managed_policy_spec.rb +2 -0
  81. data/spec/aws/kms_alias_spec.rb +2 -0
  82. data/spec/aws/list_type_patches_spec.rb +35 -0
  83. data/spec/aws/logs_log_group_spec.rb +2 -0
  84. data/spec/aws/nested_arrays_spec.rb +194 -0
  85. data/spec/aws/rds_db_instance_spec.rb +2 -0
  86. data/spec/aws/serverless_spec.rb +2 -2
  87. data/spec/cfndsl_spec.rb +102 -63
  88. data/spec/cli_spec.rb +52 -49
  89. data/spec/cloud_formation_template_spec.rb +235 -0
  90. data/spec/condition_spec.rb +24 -0
  91. data/spec/deep_merge_spec.rb +2 -0
  92. data/spec/direct_ruby_spec.rb +19 -0
  93. data/spec/external_parameters_spec.rb +25 -20
  94. data/spec/fixtures/condition-assertion.json +1 -0
  95. data/spec/fixtures/params.json +1 -0
  96. data/spec/fixtures/params.yaml +2 -0
  97. data/spec/fixtures/params_struct1.yaml +4 -0
  98. data/spec/fixtures/params_struct2.yaml +4 -0
  99. data/spec/fixtures/rule-assertion.json +1 -0
  100. data/spec/fixtures/test.rb +12 -4
  101. data/spec/generate_spec.rb +4 -0
  102. data/spec/jsonable_spec.rb +2 -0
  103. data/spec/metadata_spec.rb +2 -0
  104. data/spec/names_spec.rb +2 -0
  105. data/spec/output_spec.rb +2 -0
  106. data/spec/plurals_spec.rb +2 -0
  107. data/spec/resource_name_spec.rb +21 -0
  108. data/spec/resources_spec.rb +2 -7
  109. data/spec/rule_spec.rb +17 -0
  110. data/spec/spec_helper.rb +4 -7
  111. data/spec/support/shared_examples/orchestration_template.rb +17 -2
  112. data/spec/transform_spec.rb +2 -0
  113. data/spec/types_definition_spec.rb +6 -7
  114. metadata +79 -25
  115. data/bin/cfndsl +0 -143
  116. data/lib/cfndsl/errors.rb +0 -29
  117. data/lib/cfndsl/os/heat_template.rb +0 -16
  118. data/lib/cfndsl/os/types.rb +0 -12
  119. data/lib/cfndsl/os/types.yaml +0 -2423
  120. data/lib/cfndsl/patches.rb +0 -98
  121. data/lib/cfnlego.rb +0 -42
  122. data/spec/fixtures/heattest.rb +0 -22
  123. data/spec/heat_template_spec.rb +0 -5
@@ -1,94 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'hana'
5
+ require_relative 'globals'
6
+
1
7
  module CfnDsl
2
- # Helper module for bridging the gap between a static types file included in the repo
3
- # and dynamically generating the types directly from the AWS specification
4
- module Specification
5
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
6
- def self.extract_resources(spec)
7
- spec.each_with_object({}) do |(resource_name, resource_info), resources|
8
- properties = resource_info['Properties'].each_with_object({}) do |(property_name, property_info), extracted|
9
- # some json incorrectly labelled as Type -> Json instead of PrimitiveType
10
- # also, AWS now has the concept of Map which cfndsl had never defined
11
- if property_info['Type'] == 'Map' || property_info['Type'] == 'Json'
12
- property_type = 'Json'
13
- elsif property_info['PrimitiveType']
14
- property_type = property_info['PrimitiveType']
15
- elsif property_info['PrimitiveItemType']
16
- property_type = Array(property_info['PrimitiveItemType'])
17
- elsif property_info['ItemType']
18
- # Tag is a reused type, but not quite primitive
19
- # and not all resources use the general form
20
- property_type = if property_info['ItemType'] == 'Tag'
21
- ['Tag']
22
- else
23
- Array(resource_name.split('::').join + property_info['ItemType'])
24
- end
25
- elsif property_info['Type']
26
- # Special types (defined below) are joined with their parent
27
- # resource name for uniqueness and connection
28
- property_type = resource_name.split('::').join + property_info['Type']
29
- else
30
- warn "could not extract type from #{resource_name}"
31
- end
32
- extracted[property_name] = property_type
33
- extracted
34
- end
35
- resources[resource_name] = { 'Properties' => properties }
36
- resources
37
- end
8
+ # Module for loading and patching a spec file
9
+ class Specification
10
+ def self.load_file(file: CfnDsl.specification_file, specs: CfnDsl.additional_specs, patches: CfnDsl.specification_patches, fail_patches: false)
11
+ specification = new(file)
12
+ specs&.each { |spec| specification.merge_spec(JSON.parse(File.read(spec)), spec) }
13
+ patches&.each { |patch| specification.patch_spec(JSON.parse(File.read(patch)), patch, fail_patches) }
14
+ specification
38
15
  end
39
16
 
40
- def self.extract_types(spec)
41
- primitive_types = {
42
- 'String' => 'String',
43
- 'Boolean' => 'Boolean',
44
- 'Json' => 'Json',
45
- 'Integer' => 'Integer',
46
- 'Number' => 'Number',
47
- 'Double' => 'Double',
48
- 'Timestamp' => 'Timestamp',
49
- 'Long' => 'Long'
50
- }
51
- spec.each_with_object(primitive_types) do |(property_name, property_info), types|
52
- # In order to name things uniquely and allow for connections
53
- # we extract the resource name from the property
54
- # AWS::IAM::User.Policy becomes AWSIAMUserPolicy
55
- root_resource = property_name.match(/(.*)\./)
56
- root_resource_name = root_resource ? root_resource[1].gsub(/::/, '') : property_name
57
- property_name = property_name.gsub(/::|\./, '')
58
- next unless property_info['Properties']
59
- properties = property_info['Properties'].each_with_object({}) do |(nested_prop_name, nested_prop_info), extracted|
60
- if nested_prop_info['Type'] == 'Map' || nested_prop_info['Type'] == 'Json'
61
- # The Map type and the incorrectly labelled Json type
62
- nested_prop_type = 'Json'
63
- elsif nested_prop_info['PrimitiveType']
64
- nested_prop_type = nested_prop_info['PrimitiveType']
65
- elsif nested_prop_info['PrimitiveItemType']
66
- nested_prop_type = Array(nested_prop_info['PrimitiveItemType'])
67
- elsif nested_prop_info['ItemType']
68
- nested_prop_type = root_resource_name + nested_prop_info['ItemType']
69
- elsif nested_prop_info['Type']
70
- nested_prop_type = root_resource_name + nested_prop_info['Type']
71
- else
72
- warn "could not extract type from #{property_name}"
73
- end
74
- extracted[nested_prop_name] = nested_prop_type
75
- extracted
76
- end
77
- types[property_name] = properties
78
- types
79
- end
17
+ def self.update_required?(version:, file: CfnDsl.specification_file)
18
+ version.to_s == 'latest' || !File.exist?(file) || load_file(file: file, specs: nil, patches: nil).update_required?(version)
19
+ end
20
+
21
+ attr_reader :file, :spec
22
+
23
+ def initialize(file)
24
+ @file = file
25
+ @spec = JSON.parse File.read(file)
26
+ end
27
+
28
+ def resources
29
+ spec['ResourceTypes']
30
+ end
31
+
32
+ def types
33
+ spec['PropertyTypes']
34
+ end
35
+
36
+ # @return [Gem::Version] semantic version of the spec file
37
+ def version
38
+ @version ||= Gem::Version.new(spec['ResourceSpecificationVersion'] || '0.0.0')
80
39
  end
81
40
 
82
- def self.determine_spec_file
83
- return CfnDsl.specification_file if File.exist? CfnDsl.specification_file
84
- File.expand_path('../aws/resource_specification.json', __FILE__)
41
+ def default_fixed_version
42
+ @default_fixed_version ||= version.bump
85
43
  end
86
44
 
87
- def self.extract_from_resource_spec!
88
- spec_file = JSON.parse File.read(determine_spec_file)
89
- resources = extract_resources spec_file['ResourceTypes'].merge(Patches.resources)
90
- types = extract_types spec_file['PropertyTypes'].merge(Patches.types)
91
- { 'Resources' => resources, 'Types' => types }
45
+ def default_broken_version
46
+ @default_broken_version ||= Gem::Version.new('0.0.0')
47
+ end
48
+
49
+ def update_required?(needed_version)
50
+ needed_version.to_s == 'latest' || version < Gem::Version.new(needed_version || '0.0.0')
51
+ end
52
+
53
+ def patch_required?(patch)
54
+ broken = patch.key?('broken') ? Gem::Version.new(patch['broken']) : default_broken_version
55
+ fixed = patch.key?('fixed') ? Gem::Version.new(patch['fixed']) : default_fixed_version
56
+ broken <= version && version < fixed
57
+ end
58
+
59
+ def merge_spec(spec_parsed, _from_file)
60
+ return unless patch_required?(spec_parsed)
61
+
62
+ spec['ResourceTypes'].merge!(spec_parsed['ResourceTypes'])
63
+ spec['PropertyTypes'].merge!(spec_parsed['PropertyTypes'])
64
+ end
65
+
66
+ def patch_spec(parsed_patch, from_file, fail_patches)
67
+ return unless patch_required?(parsed_patch)
68
+
69
+ parsed_patch.each_pair do |top_level_type, patches|
70
+ next unless %w[ResourceTypes PropertyTypes].include?(top_level_type)
71
+
72
+ patches.each_pair do |property_type_name, patch_details|
73
+ begin
74
+ applies_to = spec[top_level_type]
75
+ unless property_type_name == 'patch'
76
+ # Patch applies within a specific property type
77
+ applies_to = applies_to[property_type_name]
78
+ patch_details = patch_details['patch']
79
+ end
80
+
81
+ Hana::Patch.new(patch_details['operations']).apply(applies_to) if patch_required?(patch_details)
82
+ rescue Hana::Patch::MissingTargetException => e
83
+ raise "Failed specification patch #{top_level_type} #{property_type_name} from #{from_file}" if fail_patches
84
+
85
+ warn "Ignoring failed specification patch #{top_level_type} #{property_type_name} from #{from_file} - #{e.class.name}:#{e.message}"
86
+ end
87
+ end
88
+ end
92
89
  end
93
90
  end
94
91
  end
92
+ # rubocop:enable
data/lib/cfndsl/types.rb CHANGED
@@ -1,33 +1,140 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
- require 'cfndsl/jsonable'
3
- require 'cfndsl/plurals'
4
- require 'cfndsl/names'
5
- require 'cfndsl/types'
4
+ require_relative 'plurals'
5
+ require_relative 'names'
6
+ require_relative 'specification'
6
7
 
7
8
  module CfnDsl
8
9
  # Types helper
9
10
  # rubocop:disable Metrics/ModuleLength
10
11
  module Types
12
+ def self.extract_from_resource_spec(fail_patches: false)
13
+ spec = Specification.load_file(fail_patches: fail_patches)
14
+ resources = extract_resources spec.resources
15
+ types = extract_types spec.types
16
+ { 'Resources' => resources, 'Types' => types, 'Version' => spec.version, 'File' => spec.file }
17
+ end
18
+
19
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
20
+ def self.extract_resources(spec)
21
+ spec.each_with_object({}) do |(resource_name, resource_info), resources|
22
+ properties = resource_info['Properties'].each_with_object({}) do |(property_name, property_info), extracted|
23
+ # some json incorrectly labelled as Type -> Json instead of PrimitiveType
24
+ # also, AWS now has the concept of Map which cfndsl had never defined
25
+ if property_info['Type'] == 'Map' || property_info['Type'] == 'Json'
26
+ property_type = 'Json'
27
+ elsif property_info['PrimitiveType']
28
+ property_type = property_info['PrimitiveType']
29
+ elsif property_info['PrimitiveItemType']
30
+ property_type = Array(property_info['PrimitiveItemType'])
31
+ elsif property_info['PrimitiveTypes']
32
+ property_type = property_info['PrimitiveTypes'][0]
33
+ elsif property_info['ItemType']
34
+ # Tag is a reused type, but not quite primitive
35
+ # and not all resources use the general form
36
+ property_type = if property_info['ItemType'] == 'Tag'
37
+ ['Tag']
38
+ else
39
+ Array(resource_name.split('::').join + property_info['ItemType'])
40
+ end
41
+ elsif property_info['Type']
42
+ # Special types (defined below) are joined with their parent
43
+ # resource name for uniqueness and connection
44
+ property_type = resource_name.split('::').join + property_info['Type']
45
+ else
46
+ warn "could not extract resource type from #{resource_name}"
47
+ end
48
+ extracted[property_name] = property_type
49
+ extracted
50
+ end
51
+ resources[resource_name] = { 'Properties' => properties }
52
+ resources
53
+ end
54
+ end
55
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
56
+
57
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
58
+ def self.extract_types(spec)
59
+ primitive_types = {
60
+ 'String' => 'String',
61
+ 'Boolean' => 'Boolean',
62
+ 'Json' => 'Json',
63
+ 'S3Event' => 'S3Event',
64
+ 'Integer' => 'Integer',
65
+ 'Number' => 'Number',
66
+ 'Double' => 'Double',
67
+ 'Timestamp' => 'Timestamp',
68
+ 'Map' => 'Map',
69
+ 'Long' => 'Long'
70
+ }
71
+ spec.each_with_object(primitive_types) do |(property_name, property_info), types|
72
+ # In order to name things uniquely and allow for connections
73
+ # we extract the resource name from the property
74
+ # AWS::IAM::User.Policy becomes AWSIAMUserPolicy
75
+ root_resource = property_name.match(/(.*)\./)
76
+ root_resource_name = root_resource ? root_resource[1].gsub(/::/, '') : property_name
77
+ property_name = property_name.gsub(/::|\./, '')
78
+
79
+ if property_info.key?('PrimitiveType')
80
+ properties = property_info['PrimitiveType']
81
+ elsif property_info.key?('Type')
82
+ properties = property_info['Type']
83
+ elsif property_info.key?('Properties')
84
+ properties = property_info['Properties'].each_with_object({}) do |(nested_prop_name, nested_prop_info), extracted|
85
+ if nested_prop_info['Type'] == 'Map' || nested_prop_info['Type'] == 'Json'
86
+ # The Map type and the incorrectly labelled Json type
87
+ nested_prop_type = 'Json'
88
+ elsif nested_prop_info['PrimitiveType']
89
+ nested_prop_type = nested_prop_info['PrimitiveType']
90
+ elsif nested_prop_info['PrimitiveItemType']
91
+ nested_prop_type = Array(nested_prop_info['PrimitiveItemType'])
92
+ elsif nested_prop_info['PrimitiveItemTypes']
93
+ nested_prop_type = Array(nested_prop_info['PrimitiveItemTypes'])
94
+ elsif nested_prop_info['Types']
95
+ nested_prop_type = Array(nested_prop_info['Types'])
96
+ elsif nested_prop_info['ItemType']
97
+ # Tag is a reused type, but not quite primitive
98
+ # and not all resources use the general form
99
+ nested_prop_type =
100
+ if nested_prop_info['ItemType'] == 'Tag'
101
+ ['Tag']
102
+ else
103
+ Array(root_resource_name + nested_prop_info['ItemType'])
104
+ end
105
+
106
+ elsif nested_prop_info['Type']
107
+ nested_prop_type = root_resource_name + nested_prop_info['Type']
108
+ else
109
+ warn "could not extract property type from #{property_name}"
110
+ p nested_prop_info
111
+ end
112
+ extracted[nested_prop_name] = nested_prop_type
113
+ extracted
114
+ end
115
+ end
116
+ types[property_name] = properties
117
+ types
118
+ end
119
+ end
120
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
121
+
11
122
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
12
123
  def self.included(type_def)
13
- types_list = if type_def::TYPE_PREFIX == 'aws'
14
- Specification.extract_from_resource_spec!
15
- else
16
- YAML.safe_load(File.open("#{File.dirname(__FILE__)}/#{type_def::TYPE_PREFIX}/types.yaml"))
17
- end
124
+ types_list = extract_from_resource_spec
18
125
  type_def.const_set('Types_Internal', types_list)
19
126
  # Do a little sanity checking - all of the types referenced in Resources
20
127
  # should be represented in Types
21
- types_list['Resources'].keys.each do |resource_name|
128
+ types_list['Resources'].each_key do |resource_name|
22
129
  resource = types_list['Resources'][resource_name]
23
- resource.values.each do |thing|
24
- thing.values.each do |type|
130
+ resource.each_value do |thing|
131
+ thing.each_value do |type|
25
132
  if type.is_a?(Array)
26
133
  type.each do |inner_type|
27
- puts "unknown type #{inner_type}" unless types_list['Types'].key?(inner_type)
134
+ warn "unknown type #{inner_type}" unless types_list['Types'].key?(inner_type)
28
135
  end
29
136
  else
30
- puts "unknown type #{type}" unless types_list['Types'].key?(type)
137
+ warn "unknown type #{type}" unless types_list['Types'].key?(type)
31
138
  end
32
139
  end
33
140
  end
@@ -36,8 +143,8 @@ module CfnDsl
36
143
  # All of the type values should also be references
37
144
  types_list['Types'].values do |type|
38
145
  if type.respond_to?(:values)
39
- type.values.each do |tv|
40
- puts "unknown type #{tv}" unless types_list['Types'].key?(tv)
146
+ type.each_value do |tv|
147
+ warn "unknown type #{tv}" unless types_list['Types'].key?(tv)
41
148
  end
42
149
  end
43
150
  end
@@ -58,93 +165,103 @@ module CfnDsl
58
165
  classes.each_pair do |typename, type|
59
166
  typeval = types_list['Types'][typename]
60
167
  next unless typeval.respond_to?(:each_pair)
168
+
61
169
  typeval.each_pair do |attr_name, attr_type|
170
+ attr_method = attr_name
171
+ variable = "@#{attr_name}".to_sym
172
+ klass = nil
173
+
62
174
  if attr_type.is_a?(Array)
63
175
  klass = type_def.const_get(attr_type[0])
64
- variable = "@#{attr_name}".to_sym
65
-
66
- method = CfnDsl::Plurals.singularize(attr_name)
67
- methods = attr_name
68
- all_methods = CfnDsl.method_names(method) + CfnDsl.method_names(methods)
69
- type.class_eval do
70
- all_methods.each do |method_name|
71
- define_method(method_name) do |value = nil, *rest, &block|
72
- existing = instance_variable_get(variable)
73
- # For no-op invocations, get out now
74
- return existing if value.nil? && rest.empty? && !block
75
-
76
- # We are going to modify the value in some
77
- # way, make sure that we have an array to mess
78
- # with if we start with nothing
79
- existing = instance_variable_set(variable, []) unless existing
80
-
81
- # special case for just a block, no args
82
- if value.nil? && rest.empty? && block
83
- val = klass.new
84
- existing.push val
85
- value.instance_eval(&block(val))
86
- return existing
87
- end
88
-
89
- # Glue all of our parameters together into
90
- # a giant array - flattening one level deep, if needed
91
- array_params = []
92
- if value.is_a?(Array)
93
- value.each { |x| array_params.push x }
94
- else
95
- array_params.push value
96
- end
97
- unless rest.empty?
98
- rest.each do |v|
99
- if v.is_a?(Array)
100
- array_params += rest
101
- else
102
- array_params.push v
103
- end
104
- end
105
- end
106
-
107
- # Here, if we were given multiple arguments either
108
- # as method [a,b,c], method(a,b,c), or even
109
- # method( a, [b], c) we end up with
110
- # array_params = [a,b,c]
111
- #
112
- # array_params will have at least one item
113
- # unless the user did something like pass in
114
- # a bunch of empty arrays.
115
- if block
116
- array_params.each do |array_params_value|
117
- value = klass.new
118
- existing.push value
119
- value.instance_eval(&block(array_params_value)) if block
120
- end
121
- else
122
- # List of parameters with no block -
123
- # hope that the user knows what he is
124
- # doing and stuff them into our existing
125
- # array
126
- array_params.each do |_|
127
- existing.push value
128
- end
129
- end
130
- return existing
131
- end
132
- end
176
+ singular_method = CfnDsl::Plurals.singularize(attr_name)
177
+
178
+ if singular_method == attr_name
179
+ # see if plural is different to singular
180
+ attr_method = CfnDsl::Plurals.pluralize(attr_name)
133
181
  end
182
+
183
+ define_array_method(klass, singular_method, type, variable) if singular_method != attr_method
184
+
134
185
  else
135
186
  klass = type_def.const_get(attr_type)
136
- variable = "@#{attr_name}".to_sym
137
-
138
- type.class_eval do
139
- CfnDsl.method_names(attr_name) do |inner_method|
140
- define_method(inner_method) do |value = nil, *_rest, &block|
141
- value ||= klass.new
142
- instance_variable_set(variable, value)
143
- value.instance_eval(&block) if block
144
- value
145
- end
187
+ end
188
+
189
+ type.class_eval do
190
+ CfnDsl.method_names(attr_method) do |inner_method|
191
+ define_method(inner_method) do |value = nil, *_rest, &block|
192
+ value ||= klass.new
193
+ instance_variable_set(variable, value)
194
+ value.instance_eval(&block) if block
195
+ value
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def self.define_array_method(klass, singular_method, type, variable)
204
+ type.class_eval do
205
+ CfnDsl.method_names(singular_method).each do |method_name|
206
+ define_method(method_name) do |value = nil, *rest, fn_if: nil, &block|
207
+ existing = instance_variable_get(variable)
208
+ # For no-op invocations, get out now
209
+ return existing if value.nil? && rest.empty? && !block
210
+
211
+ # We are going to modify the value in some
212
+ # way, make sure that we have an array to mess
213
+ # with if we start with nothing
214
+ existing ||= instance_variable_set(variable, [])
215
+
216
+ # special case for just a block, no args
217
+ if value.nil? && rest.empty? && block
218
+ val = klass.new
219
+ existing.push(fn_if ? FnIf(fn_if, val, Ref('AWS::NoValue')) : val)
220
+ val.instance_eval(&block)
221
+ return existing
222
+ end
223
+
224
+ # Glue all of our parameters together into
225
+ # a giant array - flattening one level deep, if needed
226
+ array_params = []
227
+ if value.is_a?(Array)
228
+ array_params.concat(value)
229
+ else
230
+ array_params.push value
231
+ end
232
+
233
+ rest.each do |v|
234
+ if v.is_a?(Array)
235
+ array_params.concat(rest)
236
+ else
237
+ array_params.push v
238
+ end
239
+ end
240
+ # Here, if we were given multiple arguments either
241
+ # as method [a,b,c], method(a,b,c), or even
242
+ # method( a, [b], c) we end up with
243
+ # array_params = [a,b,c]
244
+ #
245
+ # array_params will have at least one item
246
+ # unless the user did something like pass in
247
+ # a bunch of empty arrays.
248
+ if block
249
+ array_params.each do |array_params_value|
250
+ value = klass.new
251
+ existing.push(fn_if ? FnIf(fn_if, value, Ref('AWS::NoValue')) : value)
252
+ # This line never worked before, the useful thing to do is pass the array value to the block
253
+ value.instance_exec(array_params_value, &block)
254
+ end
255
+ else
256
+ # List of parameters with no block -
257
+ # hope that the user knows what he is
258
+ # doing and stuff them into our existing
259
+ # array
260
+ array_params.each do |v|
261
+ existing.push v
146
262
  end
147
263
  end
264
+ return existing
148
265
  end
149
266
  end
150
267
  end