humidifier 4.1.1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,39 +4,58 @@ module Humidifier
4
4
  # Reads the specs/CloudFormationResourceSpecification.json file and load each
5
5
  # resource as a class
6
6
  module Loader
7
- # Handles searching the PropertyTypes specifications for a specific
8
- # resource type
9
- class PropertyTypes
10
- attr_reader :structs
7
+ class Compiler
8
+ attr_reader :specification, :property_types
11
9
 
12
- def initialize(structs)
13
- @structs = structs
14
- end
10
+ def initialize(specification)
11
+ @specification = specification
15
12
 
16
- # find the substructures necessary for the given resource key
17
- def search(key)
18
- results = structs.keys.grep(/#{key}/)
19
- shortened_names = results.map { |result| result.gsub("#{key}.", '') }
20
- shortened_names.zip(structs.values_at(*results)).to_h.merge(global)
13
+ # Set an initial value for each property types so that we can handle
14
+ # cycles in the specification
15
+ @property_types = specification["PropertyTypes"].to_h do |name, _|
16
+ [name, {}]
17
+ end
21
18
  end
22
19
 
23
- private
20
+ def compile
21
+ # Loop through every property type that's already defined and build up
22
+ # each of the properties into the list
23
+ property_types.each do |property_type_name, property_type|
24
+ prefix = property_type_name.split(".", 2).first
24
25
 
25
- def global
26
- @global ||= structs.reject { |key, _| key.match(/AWS/) }
27
- end
28
- end
26
+ subspec = specification["PropertyTypes"].fetch(property_type_name)
27
+ subspec.fetch("Properties") { {} }.each do |property_name, property|
28
+ property = build_property(prefix, property_name, property)
29
+ property_type[property.name] = property if property
30
+ end
31
+ end
32
+
33
+ # Loop through every resource type in the specification and define a
34
+ # class for each one dynamically.
35
+ specification["ResourceTypes"].each do |aws_name, resource_type|
36
+ _top, group, resource = aws_name.split("::")
37
+
38
+ properties = {}
39
+ resource_type["Properties"].each do |property_name, property|
40
+ property = build_property(aws_name, property_name, property)
41
+ properties[property.name] = property if property
42
+ end
29
43
 
30
- class << self
31
- # loop through the specs and register each class
32
- def load
33
- parsed = parse_spec
34
- return unless parsed
44
+ resource_class =
45
+ Class.new(Resource) do
46
+ self.aws_name = aws_name
47
+ self.props = properties
48
+ end
35
49
 
36
- types = PropertyTypes.new(parsed['PropertyTypes'])
37
- parsed['ResourceTypes'].each do |key, spec|
38
- match = key.match(/\A(\w+)::(\w+)::(\w+)\z/)
39
- register(match[1], match[2], match[3], spec, types.search(key))
50
+ group_module =
51
+ if Humidifier.const_defined?(group)
52
+ Humidifier.const_get(group)
53
+ else
54
+ Humidifier.const_set(group, Module.new)
55
+ end
56
+
57
+ Humidifier.registry[aws_name] =
58
+ group_module.const_set(resource, resource_class)
40
59
  end
41
60
 
42
61
  Humidifier.registry.freeze
@@ -44,35 +63,72 @@ module Humidifier
44
63
 
45
64
  private
46
65
 
47
- def build_class(aws_name, spec, substructs)
48
- Class.new(Resource) do
49
- self.aws_name = aws_name
50
- self.props =
51
- spec['Properties'].map do |(key, config)|
52
- prop = Props.from(key, config, substructs)
53
- [prop.name, prop]
54
- end.to_h
66
+ def build_primitive(type, name, spec)
67
+ case type
68
+ in "Boolean"
69
+ Props::BooleanProp.new(name, spec)
70
+ in "Double"
71
+ Props::DoubleProp.new(name, spec)
72
+ in "Integer" | "Long"
73
+ Props::IntegerProp.new(name, spec)
74
+ in "Json"
75
+ Props::JsonProp.new(name, spec)
76
+ in "String"
77
+ Props::StringProp.new(name, spec)
78
+ in "Timestamp"
79
+ Props::TimestampProp.new(name, spec)
55
80
  end
56
81
  end
57
82
 
58
- def parse_spec
59
- path = File.expand_path(File.join('..', '..', SPECIFICATION), __dir__)
60
- return unless File.file?(path)
61
-
62
- JSON.parse(File.read(path))
83
+ def build_property(prefix, name, spec)
84
+ case spec.transform_keys(&:to_sym)
85
+ in { PrimitiveType: type }
86
+ build_primitive(type, name, spec)
87
+ in { Type: "List", PrimitiveItemType: type }
88
+ Props::ListProp.new(name, spec, build_primitive(type, name, spec))
89
+ in { Type: "Map", PrimitiveItemType: type }
90
+ Props::MapProp.new(name, spec, build_primitive(type, name, spec))
91
+ in { Type: "List", ItemType: "List" }
92
+ # specifically calling this out since
93
+ # AWS::Rekognition::StreamProcessor.PolygonRegionsOfInterest has a
94
+ # nested list structure that otherwise breaks the compiler
95
+ Props::ListProp.new(name, spec, Props::ListProp.new(name, spec))
96
+ in { Type: "List", ItemType: item_type }
97
+ Props::ListProp.new(
98
+ name,
99
+ spec,
100
+ Props::StructureProp.new(name, spec,
101
+ property_type(prefix, item_type))
102
+ )
103
+ in { Type: "Map", ItemType: item_type }
104
+ Props::MapProp.new(
105
+ name,
106
+ spec,
107
+ Props::StructureProp.new(name, spec,
108
+ property_type(prefix, item_type))
109
+ )
110
+ in { Type: type }
111
+ Props::StructureProp.new(name, spec, property_type(prefix, type))
112
+ else # rubocop:disable Layout/IndentationWidth
113
+ # It's possible to hit this clause if the specification has a property
114
+ # that is not currently supported by CloudFormation. In this case,
115
+ # we're not going to create a property at all for it.
116
+ end
63
117
  end
64
118
 
65
- def register(top, group, resource, spec, substructs)
66
- aws_name = "#{top}::#{group}::#{resource}"
67
- resource_class = build_class(aws_name, spec, substructs)
68
-
69
- unless Humidifier.const_defined?(group)
70
- Humidifier.const_set(group, Module.new)
119
+ def property_type(prefix, type)
120
+ property_types.fetch("#{prefix}.#{type}") do
121
+ property_types.fetch(type)
71
122
  end
72
-
73
- Humidifier.const_get(group).const_set(resource, resource_class)
74
- Humidifier.registry[aws_name] = resource_class
75
123
  end
76
124
  end
125
+
126
+ # loop through the specs and register each class
127
+ def self.load
128
+ filepath = File.expand_path("../../#{SPECIFICATION}", __dir__)
129
+ return unless File.file?(filepath)
130
+
131
+ Compiler.new(JSON.parse(File.read(filepath))).compile
132
+ end
77
133
  end
78
134
  end
@@ -11,9 +11,9 @@ module Humidifier
11
11
  end
12
12
 
13
13
  def to_cf
14
- { 'Value' => Serializer.dump(value) }.tap do |cf|
15
- cf['Description'] = description if description
16
- cf['Export'] = { 'Name' => export_name } if export_name
14
+ { "Value" => Serializer.dump(value) }.tap do |cf|
15
+ cf["Description"] = description if description
16
+ cf["Export"] = { "Name" => export_name } if export_name
17
17
  end
18
18
  end
19
19
  end
@@ -15,12 +15,12 @@ module Humidifier
15
15
  instance_variable_set(:"@#{property}", opts[property])
16
16
  end
17
17
 
18
- @type = opts.fetch(:type, 'String')
18
+ @type = opts.fetch(:type, "String")
19
19
  end
20
20
 
21
21
  # CFN stack syntax
22
22
  def to_cf
23
- { 'Type' => type }.tap do |cf|
23
+ { "Type" => type }.tap do |cf|
24
24
  PROPERTIES.each do |name, prop|
25
25
  value = public_send(prop)
26
26
  cf[name] = Serializer.dump(value) if value
@@ -18,12 +18,12 @@ module Humidifier
18
18
 
19
19
  # the link to the AWS docs
20
20
  def documentation
21
- spec['Documentation']
21
+ spec["Documentation"]
22
22
  end
23
23
 
24
24
  # true if this property is required by the resource
25
25
  def required?
26
- spec['Required']
26
+ spec["Required"]
27
27
  end
28
28
 
29
29
  # CFN stack syntax
@@ -34,7 +34,7 @@ module Humidifier
34
34
  # the type of update that occurs when this property is updated on its
35
35
  # associated resource
36
36
  def update_type
37
- spec['UpdateType']
37
+ spec["UpdateType"]
38
38
  end
39
39
 
40
40
  def valid?(value)
@@ -55,34 +55,70 @@ module Humidifier
55
55
 
56
56
  class BooleanProp < Prop
57
57
  allow_type TrueClass, FalseClass
58
+
59
+ def pretty_print(q)
60
+ q.text("(#{name}=boolean)")
61
+ end
58
62
  end
59
63
 
60
64
  class DoubleProp < Prop
61
65
  allow_type Integer, Float
66
+
67
+ def pretty_print(q)
68
+ q.text("(#{name}=double)")
69
+ end
62
70
  end
63
71
 
64
72
  class IntegerProp < Prop
65
73
  allow_type Integer
74
+
75
+ def pretty_print(q)
76
+ q.text("(#{name}=integer)")
77
+ end
66
78
  end
67
79
 
68
80
  class JsonProp < Prop
69
81
  allow_type Hash
82
+
83
+ def pretty_print(q)
84
+ q.text("(#{name}=json)")
85
+ end
70
86
  end
71
87
 
72
88
  class StringProp < Prop
73
89
  allow_type String
90
+
91
+ def pretty_print(q)
92
+ q.text("(#{name}=string)")
93
+ end
74
94
  end
75
95
 
76
96
  class TimestampProp < Prop
77
97
  allow_type Time, Date
98
+
99
+ def pretty_print(q)
100
+ q.text("(#{name}=timestamp)")
101
+ end
78
102
  end
79
103
 
80
104
  class ListProp < Prop
81
105
  attr_reader :subprop
82
106
 
83
- def initialize(key, spec = {}, substructs = {})
107
+ def initialize(key, spec = {}, subprop = nil)
84
108
  super(key, spec)
85
- @subprop = Props.singular_from(key, spec, substructs)
109
+ @subprop = subprop
110
+ end
111
+
112
+ def pretty_print(q)
113
+ q.group do
114
+ q.text("(#{name}=list")
115
+ q.nest(2) do
116
+ q.breakable
117
+ q.pp(subprop)
118
+ end
119
+ q.breakable("")
120
+ q.text(")")
121
+ end
86
122
  end
87
123
 
88
124
  def to_cf(list)
@@ -106,9 +142,21 @@ module Humidifier
106
142
  class MapProp < Prop
107
143
  attr_reader :subprop
108
144
 
109
- def initialize(key, spec = {}, substructs = {})
145
+ def initialize(key, spec = {}, subprop = nil)
110
146
  super(key, spec)
111
- @subprop = Props.singular_from(key, spec, substructs)
147
+ @subprop = subprop
148
+ end
149
+
150
+ def pretty_print(q)
151
+ q.group do
152
+ q.text("(#{name}=map")
153
+ q.nest(2) do
154
+ q.breakable
155
+ q.pp(subprop)
156
+ end
157
+ q.breakable("")
158
+ q.text(")")
159
+ end
112
160
  end
113
161
 
114
162
  def to_cf(map)
@@ -116,9 +164,9 @@ module Humidifier
116
164
  if map.respond_to?(:to_cf)
117
165
  map.to_cf
118
166
  else
119
- map.map do |subkey, subvalue|
167
+ map.to_h do |subkey, subvalue|
120
168
  [subkey, subprop.to_cf(subvalue).last]
121
- end.to_h
169
+ end
122
170
  end
123
171
 
124
172
  [key, cf_value]
@@ -134,9 +182,21 @@ module Humidifier
134
182
  class StructureProp < Prop
135
183
  attr_reader :subprops
136
184
 
137
- def initialize(key, spec = {}, substructs = {})
185
+ def initialize(key, spec = {}, subprops = {})
138
186
  super(key, spec)
139
- @subprops = subprops_from(substructs, spec['ItemType'] || spec['Type'])
187
+ @subprops = subprops
188
+ end
189
+
190
+ def pretty_print(q)
191
+ q.group do
192
+ q.text("(#{name}=structure")
193
+ q.nest(2) do
194
+ q.breakable
195
+ q.seplist(subprops.values) { |subprop| q.pp(subprop) }
196
+ end
197
+ q.breakable("")
198
+ q.text(")")
199
+ end
140
200
  end
141
201
 
142
202
  def to_cf(struct)
@@ -144,9 +204,9 @@ module Humidifier
144
204
  if struct.respond_to?(:to_cf)
145
205
  struct.to_cf
146
206
  else
147
- struct.map do |subkey, subvalue|
207
+ struct.to_h do |subkey, subvalue|
148
208
  subprops[subkey.to_s].to_cf(subvalue)
149
- end.to_h
209
+ end
150
210
  end
151
211
 
152
212
  [key, cf_value]
@@ -158,49 +218,11 @@ module Humidifier
158
218
 
159
219
  private
160
220
 
161
- def subprops_from(substructs, type)
162
- subprop_names = substructs.fetch(type, {}).fetch('Properties', {})
163
-
164
- subprop_names.each_with_object({}) do |(key, config), subprops|
165
- subprops[key.underscore] =
166
- if config['ItemType'] == type
167
- self
168
- else
169
- Props.from(key, config, substructs)
170
- end
171
- end
172
- end
173
-
174
221
  def valid_struct?(struct)
175
222
  struct.all? do |key, value|
176
223
  subprops.key?(key.to_s) && subprops[key.to_s].valid?(value)
177
224
  end
178
225
  end
179
226
  end
180
-
181
- class << self
182
- # builds the appropriate prop object from the given spec line
183
- def from(key, spec, substructs = {})
184
- case spec['Type']
185
- when 'List' then ListProp.new(key, spec, substructs)
186
- when 'Map' then MapProp.new(key, spec, substructs)
187
- else singular_from(key, spec, substructs)
188
- end
189
- end
190
-
191
- # builds a prop that is not a List or Map type
192
- # PrimitiveType is one of Boolean, Double, Integer, Json, String, or
193
- # Timestamp
194
- def singular_from(key, spec, substructs)
195
- primitive = spec['PrimitiveItemType'] || spec['PrimitiveType']
196
-
197
- if primitive && !%w[List Map].include?(primitive)
198
- primitive = 'Integer' if primitive == 'Long'
199
- const_get(:"#{primitive}Prop").new(key, spec)
200
- else
201
- StructureProp.new(key, spec, substructs)
202
- end
203
- end
204
- end
205
227
  end
206
228
  end
@@ -11,7 +11,7 @@ module Humidifier
11
11
 
12
12
  # Builds CFN syntax
13
13
  def to_cf
14
- { 'Ref' => Serializer.dump(reference) }
14
+ { "Ref" => Serializer.dump(reference) }
15
15
  end
16
16
  end
17
17
  end
@@ -45,8 +45,8 @@ module Humidifier
45
45
  end
46
46
 
47
47
  common_attributes.merge!(
48
- 'Type' => self.class.aws_name,
49
- 'Properties' => props_cf.to_h
48
+ "Type" => self.class.aws_name,
49
+ "Properties" => props_cf.to_h
50
50
  )
51
51
  end
52
52
 
@@ -114,7 +114,7 @@ module Humidifier
114
114
 
115
115
  def validate_property(property)
116
116
  unless self.class.prop?(property)
117
- raise ArgumentError, 'Attempting to set invalid property for ' \
117
+ raise ArgumentError, "Attempting to set invalid property for " \
118
118
  "#{self.class.name}: #{property}"
119
119
  end
120
120
 
@@ -5,9 +5,9 @@ module Humidifier
5
5
  class Serializer
6
6
  class << self
7
7
  # dumps the given object out to CFN syntax recursively
8
- def dump(node) # rubocop:disable Metrics/CyclomaticComplexity
8
+ def dump(node)
9
9
  case node
10
- when Hash then node.map { |key, value| [key, dump(value)] }.to_h
10
+ when Hash then node.to_h { |key, value| [key, dump(value)] }
11
11
  when Array then node.map { |value| dump(value) }
12
12
  when Ref, Fn then dump(node.to_cf)
13
13
  when Date then node.iso8601
@@ -14,8 +14,8 @@ module Humidifier
14
14
  super(
15
15
  "Cannot use a template > #{MAX_TEMPLATE_URL_SIZE} bytes " \
16
16
  "(currently #{bytesize} bytes), consider using nested stacks " \
17
- '(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide' \
18
- '/aws-properties-stack.html)'
17
+ "(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide" \
18
+ "/aws-properties-stack.html)"
19
19
  )
20
20
  end
21
21
  end
@@ -37,7 +37,7 @@ module Humidifier
37
37
  end
38
38
 
39
39
  # The AWS region, can be set through the environment, defaults to us-east-1
40
- AWS_REGION = ENV['AWS_REGION'] || 'us-east-1'
40
+ AWS_REGION = ENV.fetch("AWS_REGION", "us-east-1")
41
41
 
42
42
  # Lists of objects linked to the stack
43
43
  ENUMERABLE_RESOURCES =
@@ -184,7 +184,7 @@ module Humidifier
184
184
  perform_and_wait(:update, opts)
185
185
  end
186
186
 
187
- def upload # rubocop:disable Metrics/AbcSize
187
+ def upload
188
188
  raise NoResourcesError.new(self, :upload) unless resources.any?
189
189
 
190
190
  bucket = Humidifier.config.s3_bucket
@@ -231,9 +231,9 @@ module Humidifier
231
231
  next if resources.empty?
232
232
 
233
233
  list[name] =
234
- resources.map do |resource_name, resource|
234
+ resources.to_h do |resource_name, resource|
235
235
  [resource_name, resource.to_cf]
236
- end.to_h
236
+ end
237
237
  end
238
238
  end
239
239
 
@@ -2,19 +2,19 @@
2
2
 
3
3
  module Humidifier
4
4
  class Upgrade
5
- PATH = -File.expand_path(File.join('..', '..', SPECIFICATION), __dir__)
6
- URL = 'https://docs.aws.amazon.com/AWSCloudFormation/latest' \
7
- '/UserGuide/cfn-resource-specification.html'
5
+ PATH = -File.expand_path(File.join("..", "..", SPECIFICATION), __dir__)
6
+ URL = "https://docs.aws.amazon.com/AWSCloudFormation/latest" \
7
+ "/UserGuide/cfn-resource-specification.html"
8
8
 
9
9
  def perform
10
- require 'net/http'
11
- require 'nokogiri'
10
+ require "net/http"
11
+ require "nokogiri"
12
12
 
13
13
  response = Net::HTTP.get_response(uri).body
14
14
  parsed = JSON.parse(response)
15
15
 
16
16
  File.write(PATH, JSON.pretty_generate(parsed))
17
- parsed['ResourceSpecificationVersion']
17
+ parsed["ResourceSpecificationVersion"]
18
18
  end
19
19
 
20
20
  def self.perform
@@ -28,11 +28,11 @@ module Humidifier
28
28
  end
29
29
 
30
30
  def uri
31
- Nokogiri::HTML(page).css('table tr').detect do |tr|
32
- name = tr.at_css('td:first-child p')
33
- next if !name || name.text.strip != 'US East (N. Virginia)'
31
+ Nokogiri::HTML(page).css("table tr").detect do |tr|
32
+ name = tr.at_css("td:first-child p")
33
+ next if !name || name.text.strip != "US East (N. Virginia)"
34
34
 
35
- break URI.parse(tr.at_css('td:nth-child(3) p a').attr('href'))
35
+ break URI.parse(tr.at_css("td:nth-child(3) p a").attr("href"))
36
36
  end
37
37
  end
38
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- VERSION = '4.1.1'
4
+ VERSION = "4.2.0"
5
5
  end
data/lib/humidifier.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'json'
5
- require 'pathname'
6
- require 'yaml'
3
+ require "date"
4
+ require "json"
5
+ require "pathname"
6
+ require "yaml"
7
7
 
8
- require 'aws-sdk-cloudformation'
9
- require 'aws-sdk-s3'
10
- require 'fast_underscore'
11
- require 'thor'
12
- require 'thor/hollaback'
8
+ require "aws-sdk-cloudformation"
9
+ require "aws-sdk-s3"
10
+ require "fast_underscore"
11
+ require "thor"
12
+ require "thor/hollaback"
13
13
 
14
14
  # Hook into the string extension and ensure it works for certain AWS acronyms
15
15
  String.prepend(
@@ -23,7 +23,7 @@ String.prepend(
23
23
  # container module for all gem classes
24
24
  module Humidifier
25
25
  # The file name of the specification for consistency.
26
- SPECIFICATION = 'CloudFormationResourceSpecification.json'
26
+ SPECIFICATION = "CloudFormationResourceSpecification.json"
27
27
 
28
28
  # A parent class for all Humidifier errors for easier rescuing.
29
29
  class Error < StandardError; end
@@ -61,30 +61,30 @@ module Humidifier
61
61
 
62
62
  # a frozen hash of the given names mapped to their underscored version
63
63
  def underscore(names)
64
- names.map { |name| [name, name.underscore.to_sym] }.to_h.freeze
64
+ names.to_h { |name| [name, name.underscore.to_sym] }.freeze
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
- require 'humidifier/fn'
70
- require 'humidifier/ref'
71
- require 'humidifier/props'
72
-
73
- require 'humidifier/cli'
74
- require 'humidifier/condition'
75
- require 'humidifier/directory'
76
- require 'humidifier/loader'
77
- require 'humidifier/mapping'
78
- require 'humidifier/output'
79
- require 'humidifier/parameter'
80
- require 'humidifier/resource'
81
- require 'humidifier/serializer'
82
- require 'humidifier/stack'
83
- require 'humidifier/upgrade'
84
- require 'humidifier/version'
85
-
86
- require 'humidifier/config'
87
- require 'humidifier/config/mapper'
88
- require 'humidifier/config/mapping'
69
+ require "humidifier/fn"
70
+ require "humidifier/ref"
71
+ require "humidifier/props"
72
+
73
+ require "humidifier/cli"
74
+ require "humidifier/condition"
75
+ require "humidifier/directory"
76
+ require "humidifier/loader"
77
+ require "humidifier/mapping"
78
+ require "humidifier/output"
79
+ require "humidifier/parameter"
80
+ require "humidifier/resource"
81
+ require "humidifier/serializer"
82
+ require "humidifier/stack"
83
+ require "humidifier/upgrade"
84
+ require "humidifier/version"
85
+
86
+ require "humidifier/config"
87
+ require "humidifier/config/mapper"
88
+ require "humidifier/config/mapping"
89
89
 
90
90
  Humidifier::Loader.load