cfndsl 0.17.5 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +4 -4
  4. data/.travis.yml +6 -1
  5. data/CHANGELOG.md +32 -34
  6. data/README.md +68 -21
  7. data/Rakefile +25 -3
  8. data/TODO.md +18 -0
  9. data/UPGRADING.md +22 -0
  10. data/cfndsl.gemspec +3 -3
  11. data/exe/cfndsl +5 -0
  12. data/lib/cfndsl.rb +2 -116
  13. data/lib/cfndsl/aws/cloud_formation_template.rb +8 -1
  14. data/lib/cfndsl/aws/patches/000_sam.spec.json +574 -0
  15. data/lib/cfndsl/aws/patches/100_sam.spec_DeploymentPreference_patch.json +64 -0
  16. data/lib/cfndsl/aws/patches/500_Cognito_IdentityPoolRoleAttachment_patches.json +25 -0
  17. data/lib/cfndsl/aws/patches/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +20 -0
  18. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Event_Events_patch.json +16 -0
  19. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Location_Version_patch.json +16 -0
  20. data/lib/cfndsl/aws/patches/500_SSM_AssociationName_patch.json +16 -0
  21. data/lib/cfndsl/aws/patches/500_VPCEndpoint_patch.json +17 -0
  22. data/lib/cfndsl/aws/patches/510_ElasticSearch_Domain_patches.json +15 -0
  23. data/lib/cfndsl/aws/patches/600_RefKinds_patch.json +3654 -0
  24. data/lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json +20 -0
  25. data/lib/cfndsl/aws/patches/800_List_types_patch.json +115 -0
  26. data/lib/cfndsl/aws/resource_specification.json +35809 -11627
  27. data/lib/cfndsl/aws/types.rb +3 -3
  28. data/lib/cfndsl/cfnlego.rb +34 -0
  29. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.erb +0 -0
  30. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.rb +0 -0
  31. data/lib/{cfnlego → cfndsl/cfnlego}/resource.rb +3 -8
  32. data/lib/cfndsl/cloudformation.rb +107 -0
  33. data/lib/cfndsl/conditions.rb +11 -1
  34. data/lib/cfndsl/creation_policy.rb +1 -1
  35. data/lib/cfndsl/deep_merge.rb +4 -0
  36. data/lib/cfndsl/external_parameters.rb +4 -13
  37. data/lib/cfndsl/globals.rb +48 -9
  38. data/lib/cfndsl/jsonable.rb +22 -60
  39. data/lib/cfndsl/mappings.rb +1 -1
  40. data/lib/cfndsl/module.rb +16 -5
  41. data/lib/cfndsl/orchestration_template.rb +185 -83
  42. data/lib/cfndsl/outputs.rb +5 -1
  43. data/lib/cfndsl/parameters.rb +1 -1
  44. data/lib/cfndsl/plurals.rb +12 -1
  45. data/lib/cfndsl/properties.rb +1 -1
  46. data/lib/cfndsl/rake_task.rb +206 -12
  47. data/lib/cfndsl/ref_check.rb +19 -11
  48. data/lib/cfndsl/resources.rb +6 -19
  49. data/lib/cfndsl/rules.rb +1 -1
  50. data/lib/cfndsl/runner.rb +143 -0
  51. data/lib/cfndsl/specification.rb +80 -95
  52. data/lib/cfndsl/types.rb +205 -91
  53. data/lib/cfndsl/update_policy.rb +1 -1
  54. data/lib/cfndsl/version.rb +1 -1
  55. data/sample/autoscale.rb +0 -1
  56. data/sample/autoscale2.rb +0 -1
  57. data/sample/config_service.rb +2 -2
  58. data/sample/t1.rb +1 -1
  59. data/sample/vpc_example.rb +1 -1
  60. data/sample/vpc_with_vpn_example.rb +1 -1
  61. data/spec/aws/list_type_patches_spec.rb +35 -0
  62. data/spec/aws/nested_arrays_spec.rb +155 -3
  63. data/spec/aws/serverless_spec.rb +0 -2
  64. data/spec/cfndsl_spec.rb +94 -78
  65. data/spec/cli_spec.rb +16 -54
  66. data/spec/cloud_formation_template_spec.rb +233 -0
  67. data/spec/condition_spec.rb +24 -0
  68. data/spec/direct_ruby_spec.rb +19 -0
  69. data/spec/external_parameters_spec.rb +2 -15
  70. data/spec/fixtures/condition-assertion.json +1 -0
  71. data/spec/fixtures/test.rb +2 -1
  72. data/spec/generate_spec.rb +4 -2
  73. data/spec/resources_spec.rb +0 -7
  74. data/spec/spec_helper.rb +2 -7
  75. data/spec/support/shared_examples/orchestration_template.rb +15 -2
  76. data/spec/types_definition_spec.rb +3 -6
  77. metadata +52 -23
  78. data/bin/cfndsl +0 -160
  79. data/lib/cfndsl/errors.rb +0 -31
  80. data/lib/cfndsl/os/heat_template.rb +0 -18
  81. data/lib/cfndsl/os/types.rb +0 -14
  82. data/lib/cfndsl/os/types.yaml +0 -2423
  83. data/lib/cfndsl/patches.rb +0 -226
  84. data/lib/cfnlego.rb +0 -44
  85. data/spec/fixtures/heattest.rb +0 -24
  86. data/spec/heat_template_spec.rb +0 -7
@@ -1,107 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+ require 'hana'
5
+ require_relative 'globals'
6
+
3
7
  module CfnDsl
4
- # Helper module for bridging the gap between a static types file included in the repo
5
- # and dynamically generating the types directly from the AWS specification
6
- module Specification
7
- # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
8
- def self.extract_resources(spec)
9
- spec.each_with_object({}) do |(resource_name, resource_info), resources|
10
- properties = resource_info['Properties'].each_with_object({}) do |(property_name, property_info), extracted|
11
- # some json incorrectly labelled as Type -> Json instead of PrimitiveType
12
- # also, AWS now has the concept of Map which cfndsl had never defined
13
- if property_info['Type'] == 'Map' || property_info['Type'] == 'Json'
14
- property_type = 'Json'
15
- elsif property_info['PrimitiveType']
16
- property_type = property_info['PrimitiveType']
17
- elsif property_info['PrimitiveItemType']
18
- property_type = Array(property_info['PrimitiveItemType'])
19
- elsif property_info['ItemType']
20
- # Tag is a reused type, but not quite primitive
21
- # and not all resources use the general form
22
- property_type = if property_info['ItemType'] == 'Tag'
23
- ['Tag']
24
- else
25
- Array(resource_name.split('::').join + property_info['ItemType'])
26
- end
27
- elsif property_info['Type']
28
- # Special types (defined below) are joined with their parent
29
- # resource name for uniqueness and connection
30
- property_type = resource_name.split('::').join + property_info['Type']
31
- else
32
- warn "could not extract type from #{resource_name}"
33
- end
34
- extracted[property_name] = property_type
35
- extracted
36
- end
37
- resources[resource_name] = { 'Properties' => properties }
38
- resources
39
- 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
40
15
  end
41
- # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
42
-
43
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
44
- def self.extract_types(spec)
45
- primitive_types = {
46
- 'String' => 'String',
47
- 'Boolean' => 'Boolean',
48
- 'Json' => 'Json',
49
- 'Integer' => 'Integer',
50
- 'Number' => 'Number',
51
- 'Double' => 'Double',
52
- 'Timestamp' => 'Timestamp',
53
- 'Map' => 'Map',
54
- 'Long' => 'Long'
55
- }
56
- spec.each_with_object(primitive_types) do |(property_name, property_info), types|
57
- # In order to name things uniquely and allow for connections
58
- # we extract the resource name from the property
59
- # AWS::IAM::User.Policy becomes AWSIAMUserPolicy
60
- root_resource = property_name.match(/(.*)\./)
61
- root_resource_name = root_resource ? root_resource[1].gsub(/::/, '') : property_name
62
- property_name = property_name.gsub(/::|\./, '')
63
-
64
- if property_info.key?('PrimitiveType')
65
- properties = property_info['PrimitiveType']
66
- elsif property_info.key?('Type')
67
- properties = property_info['Type']
68
- elsif property_info.key?('Properties')
69
- properties = property_info['Properties'].each_with_object({}) do |(nested_prop_name, nested_prop_info), extracted|
70
- if nested_prop_info['Type'] == 'Map' || nested_prop_info['Type'] == 'Json'
71
- # The Map type and the incorrectly labelled Json type
72
- nested_prop_type = 'Json'
73
- elsif nested_prop_info['PrimitiveType']
74
- nested_prop_type = nested_prop_info['PrimitiveType']
75
- elsif nested_prop_info['PrimitiveItemType']
76
- nested_prop_type = Array(nested_prop_info['PrimitiveItemType'])
77
- elsif nested_prop_info['ItemType']
78
- nested_prop_type = Array(root_resource_name + nested_prop_info['ItemType'])
79
- elsif nested_prop_info['Type']
80
- nested_prop_type = root_resource_name + nested_prop_info['Type']
81
- else
82
- warn "could not extract type from #{property_name}"
83
- end
84
- extracted[nested_prop_name] = nested_prop_type
85
- extracted
86
- end
87
- end
88
- types[property_name] = properties
89
- types
90
- end
16
+
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')
39
+ end
40
+
41
+ def default_fixed_version
42
+ @default_fixed_version ||= version.bump
43
+ end
44
+
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
91
57
  end
92
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
93
58
 
94
- def self.determine_spec_file
95
- return CfnDsl.specification_file if File.exist? CfnDsl.specification_file
59
+ def merge_spec(spec_parsed, _from_file)
60
+ return unless patch_required?(spec_parsed)
96
61
 
97
- File.expand_path('aws/resource_specification.json', __dir__)
62
+ spec['ResourceTypes'].merge!(spec_parsed['ResourceTypes'])
63
+ spec['PropertyTypes'].merge!(spec_parsed['PropertyTypes'])
98
64
  end
99
65
 
100
- def self.extract_from_resource_spec!
101
- spec_file = JSON.parse File.read(determine_spec_file)
102
- resources = extract_resources spec_file['ResourceTypes'].merge(Patches.resources)
103
- types = extract_types spec_file['PropertyTypes'].merge(Patches.types)
104
- { 'Resources' => resources, 'Types' => types }
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
105
89
  end
106
90
  end
107
91
  end
92
+ # rubocop:enable
data/lib/cfndsl/types.rb CHANGED
@@ -1,22 +1,127 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require 'cfndsl/jsonable'
5
- require 'cfndsl/plurals'
6
- require 'cfndsl/names'
7
- require 'cfndsl/types'
4
+ require_relative 'plurals'
5
+ require_relative 'names'
6
+ require_relative 'specification'
8
7
 
9
8
  module CfnDsl
10
9
  # Types helper
11
10
  # rubocop:disable Metrics/ModuleLength
12
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
+
13
122
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
14
123
  def self.included(type_def)
15
- types_list = if type_def::TYPE_PREFIX == 'aws'
16
- Specification.extract_from_resource_spec!
17
- else
18
- YAML.safe_load(File.open("#{File.dirname(__FILE__)}/#{type_def::TYPE_PREFIX}/types.yaml"))
19
- end
124
+ types_list = extract_from_resource_spec
20
125
  type_def.const_set('Types_Internal', types_list)
21
126
  # Do a little sanity checking - all of the types referenced in Resources
22
127
  # should be represented in Types
@@ -26,10 +131,10 @@ module CfnDsl
26
131
  thing.each_value do |type|
27
132
  if type.is_a?(Array)
28
133
  type.each do |inner_type|
29
- 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)
30
135
  end
31
136
  else
32
- puts "unknown type #{type}" unless types_list['Types'].key?(type)
137
+ warn "unknown type #{type}" unless types_list['Types'].key?(type)
33
138
  end
34
139
  end
35
140
  end
@@ -39,7 +144,7 @@ module CfnDsl
39
144
  types_list['Types'].values do |type|
40
145
  if type.respond_to?(:values)
41
146
  type.each_value do |tv|
42
- puts "unknown type #{tv}" unless types_list['Types'].key?(tv)
147
+ warn "unknown type #{tv}" unless types_list['Types'].key?(tv)
43
148
  end
44
149
  end
45
150
  end
@@ -62,92 +167,101 @@ module CfnDsl
62
167
  next unless typeval.respond_to?(:each_pair)
63
168
 
64
169
  typeval.each_pair do |attr_name, attr_type|
170
+ attr_method = attr_name
171
+ variable = "@#{attr_name}".to_sym
172
+ klass = nil
173
+
65
174
  if attr_type.is_a?(Array)
66
175
  klass = type_def.const_get(attr_type[0])
67
- variable = "@#{attr_name}".to_sym
68
-
69
- method = CfnDsl::Plurals.singularize(attr_name)
70
- methods = attr_name
71
- all_methods = CfnDsl.method_names(method) + CfnDsl.method_names(methods)
72
- type.class_eval do
73
- all_methods.each do |method_name|
74
- define_method(method_name) do |value = nil, *rest, &block|
75
- existing = instance_variable_get(variable)
76
- # For no-op invocations, get out now
77
- return existing if value.nil? && rest.empty? && !block
78
-
79
- # We are going to modify the value in some
80
- # way, make sure that we have an array to mess
81
- # with if we start with nothing
82
- existing ||= instance_variable_set(variable, [])
83
-
84
- # special case for just a block, no args
85
- if value.nil? && rest.empty? && block
86
- val = klass.new
87
- existing.push val
88
- val.instance_eval(&block)
89
- return existing
90
- end
91
-
92
- # Glue all of our parameters together into
93
- # a giant array - flattening one level deep, if needed
94
- array_params = []
95
- if value.is_a?(Array)
96
- value.each { |x| array_params.push x }
97
- else
98
- array_params.push value
99
- end
100
- unless rest.empty?
101
- rest.each do |v|
102
- if v.is_a?(Array)
103
- array_params += rest
104
- else
105
- array_params.push v
106
- end
107
- end
108
- end
109
-
110
- # Here, if we were given multiple arguments either
111
- # as method [a,b,c], method(a,b,c), or even
112
- # method( a, [b], c) we end up with
113
- # array_params = [a,b,c]
114
- #
115
- # array_params will have at least one item
116
- # unless the user did something like pass in
117
- # a bunch of empty arrays.
118
- if block
119
- array_params.each do |array_params_value|
120
- value = klass.new
121
- existing.push value
122
- value.instance_eval(&block(array_params_value)) if block
123
- end
124
- else
125
- # List of parameters with no block -
126
- # hope that the user knows what he is
127
- # doing and stuff them into our existing
128
- # array
129
- array_params.each do |v|
130
- existing.push v
131
- end
132
- end
133
- return existing
134
- end
135
- 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)
136
181
  end
182
+
183
+ define_array_method(klass, singular_method, type, variable) if singular_method != attr_method
184
+
137
185
  else
138
186
  klass = type_def.const_get(attr_type)
139
- variable = "@#{attr_name}".to_sym
140
-
141
- type.class_eval do
142
- CfnDsl.method_names(attr_name) do |inner_method|
143
- define_method(inner_method) do |value = nil, *_rest, &block|
144
- value ||= klass.new
145
- instance_variable_set(variable, value)
146
- value.instance_eval(&block) if block
147
- value
148
- 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
149
262
  end
150
263
  end
264
+ return existing
151
265
  end
152
266
  end
153
267
  end