cardtapp-cloudformation-ruby-dsl 0.0.1.pre.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/OWNERS +3 -0
- data/PULL_REQUEST_TEMPLATE.md +30 -0
- data/README.md +128 -0
- data/Rakefile +9 -0
- data/bin/cfntemplate-to-ruby +347 -0
- data/cloudformation-ruby-dsl.gemspec +46 -0
- data/docs/Contributing.md +22 -0
- data/docs/Releasing.md +20 -0
- data/examples/cloudformation-ruby-script.rb +274 -0
- data/examples/maps/domains.txt +4 -0
- data/examples/maps/map.json +9 -0
- data/examples/maps/map.rb +5 -0
- data/examples/maps/map.yaml +5 -0
- data/examples/maps/more_maps/map1.json +8 -0
- data/examples/maps/more_maps/map2.json +8 -0
- data/examples/maps/more_maps/map3.json +8 -0
- data/examples/maps/table.txt +5 -0
- data/examples/maps/vpc.txt +25 -0
- data/examples/simple_template.rb +21 -0
- data/examples/userdata.sh +4 -0
- data/initial_contributions.md +5 -0
- data/lib/cloudformation-ruby-dsl/cfntemplate.rb +731 -0
- data/lib/cloudformation-ruby-dsl/dsl.rb +380 -0
- data/lib/cloudformation-ruby-dsl/spotprice.rb +50 -0
- data/lib/cloudformation-ruby-dsl/table.rb +123 -0
- data/lib/cloudformation-ruby-dsl/version.rb +21 -0
- data/lib/cloudformation-ruby-dsl.rb +2 -0
- data/spec/spec_helper.rb +161 -0
- data/spec/validation_spec.rb +26 -0
- metadata +234 -0
@@ -0,0 +1,380 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
############################ Utility functions
|
4
|
+
|
5
|
+
# Formats a template as JSON
|
6
|
+
def generate_template(template)
|
7
|
+
generate_json template, !template.nopretty
|
8
|
+
end
|
9
|
+
|
10
|
+
def generate_json(data, pretty = true)
|
11
|
+
# Raw formatting
|
12
|
+
return JSON.generate(data) unless pretty
|
13
|
+
|
14
|
+
# Pretty formatting
|
15
|
+
JSON.pretty_generate(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
############################# Generic DSL
|
20
|
+
|
21
|
+
class JsonObjectDSL
|
22
|
+
def initialize(&block)
|
23
|
+
@dict = {}
|
24
|
+
instance_eval &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def value(values)
|
28
|
+
@dict.update(values)
|
29
|
+
end
|
30
|
+
|
31
|
+
def default(key, value)
|
32
|
+
@dict[key] ||= value
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_json(*args)
|
36
|
+
@dict.to_json(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def print()
|
40
|
+
puts JSON.pretty_generate(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
############################# CloudFormation DSL
|
45
|
+
|
46
|
+
# Main entry point
|
47
|
+
def raw_template(options = {}, &block)
|
48
|
+
TemplateDSL.new(options, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_region
|
52
|
+
ENV['EC2_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
|
53
|
+
end
|
54
|
+
|
55
|
+
class Parameter < String
|
56
|
+
attr_accessor :default, :use_previous_value
|
57
|
+
|
58
|
+
def initialize string
|
59
|
+
super string.to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Core interpreter for the DSL
|
64
|
+
class TemplateDSL < JsonObjectDSL
|
65
|
+
attr_reader :parameters,
|
66
|
+
:parameter_cli,
|
67
|
+
:aws_region,
|
68
|
+
:nopretty,
|
69
|
+
:stack_name,
|
70
|
+
:aws_profile,
|
71
|
+
:s3_bucket
|
72
|
+
|
73
|
+
def initialize(options)
|
74
|
+
@parameters = options.fetch(:parameters, {})
|
75
|
+
@interactive = options.fetch(:interactive, false)
|
76
|
+
@stack_name = options[:stack_name]
|
77
|
+
@aws_region = options.fetch(:region, default_region)
|
78
|
+
@aws_profile = options[:profile]
|
79
|
+
@nopretty = options.fetch(:nopretty, false)
|
80
|
+
@s3_bucket = options.fetch(:s3_bucket, nil)
|
81
|
+
super()
|
82
|
+
end
|
83
|
+
|
84
|
+
def exec!()
|
85
|
+
cfn(self)
|
86
|
+
end
|
87
|
+
|
88
|
+
def parameter(name, options)
|
89
|
+
default(:Parameters, {})[name] = options
|
90
|
+
|
91
|
+
if @interactive
|
92
|
+
@parameters[name] ||= Parameter.new(_get_parameter_from_cli(name, options))
|
93
|
+
else
|
94
|
+
@parameters[name] ||= Parameter.new('')
|
95
|
+
end
|
96
|
+
|
97
|
+
# set various param options
|
98
|
+
@parameters[name].default = options[:Default]
|
99
|
+
@parameters[name].use_previous_value = options[:UsePreviousValue]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Find parameters where the specified attribute is true then remove the attribute from the cfn template.
|
103
|
+
def excise_parameter_attributes!(attributes)
|
104
|
+
marked_parameters = {}
|
105
|
+
@dict.fetch(:Parameters, {}).each do |param, options|
|
106
|
+
attributes.each do |attribute|
|
107
|
+
marked_parameters[attribute] ||= []
|
108
|
+
if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
|
109
|
+
marked_parameters[attribute] << param
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
marked_parameters
|
114
|
+
end
|
115
|
+
|
116
|
+
def mapping(name, options)
|
117
|
+
# if options is a string and a valid file then the script will process the external file.
|
118
|
+
default(:Mappings, {})[name] = \
|
119
|
+
if options.is_a?(Hash); options
|
120
|
+
elsif options.is_a?(String); load_from_file(options)['Mappings'][name]
|
121
|
+
else; raise("Options for mapping #{name} is neither a string or a hash. Error!")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_from_file(filename)
|
126
|
+
file = File.open(filename)
|
127
|
+
|
128
|
+
begin
|
129
|
+
# Figure out what the file extension is and process accordingly.
|
130
|
+
contents = case File.extname(filename)
|
131
|
+
when ".rb"; eval(file.read, nil, filename)
|
132
|
+
when ".json"; JSON.load(file)
|
133
|
+
when ".yaml"; YAML::load(file)
|
134
|
+
else; raise("Do not recognize extension of #{filename}.")
|
135
|
+
end
|
136
|
+
ensure
|
137
|
+
file.close
|
138
|
+
end
|
139
|
+
contents
|
140
|
+
end
|
141
|
+
|
142
|
+
# Find tags where the specified attribute is true then remove this attribute.
|
143
|
+
def get_tag_attribute(tags, attribute)
|
144
|
+
marked_tags = []
|
145
|
+
tags.each do |tag, options|
|
146
|
+
if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
|
147
|
+
marked_tags << tag
|
148
|
+
end
|
149
|
+
end
|
150
|
+
marked_tags
|
151
|
+
end
|
152
|
+
|
153
|
+
def excise_tags!
|
154
|
+
tags = @dict.fetch(:Tags, {})
|
155
|
+
@dict.delete(:Tags)
|
156
|
+
tags
|
157
|
+
end
|
158
|
+
|
159
|
+
def tag(tag, *args)
|
160
|
+
if (tag.is_a?(String) || tag.is_a?(Symbol)) && !args.empty?
|
161
|
+
default(:Tags, {})[tag.to_s] = args[0]
|
162
|
+
# For backward-compatibility, transform `tag_name=>value` format to `tag_name, :Value=>value, :Immutable=>true`
|
163
|
+
# Tags declared this way remain immutable and won't be updated.
|
164
|
+
elsif tag.is_a?(Hash) && tag.size == 1 && args.empty?
|
165
|
+
$stderr.puts "WARNING: #{tag} tag declaration format is deprecated and will be removed in a future version. Please use resource-like style instead."
|
166
|
+
tag.each do |name, value|
|
167
|
+
default(:Tags, {})[name.to_s] = {:Value => value, :Immutable => true}
|
168
|
+
end
|
169
|
+
else
|
170
|
+
$stderr.puts "Error: #{tag} tag validation error. Please verify tag's declaration format."
|
171
|
+
exit(false)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def metadata(name, options) default(:Metadata, {})[name] = options end
|
176
|
+
|
177
|
+
def condition(name, options) default(:Conditions, {})[name] = options end
|
178
|
+
|
179
|
+
def resource(name, options) default(:Resources, {})[name] = options end
|
180
|
+
|
181
|
+
def output(name, options) default(:Outputs, {})[name] = options end
|
182
|
+
|
183
|
+
def find_in_map(map, key, name)
|
184
|
+
# Eagerly evaluate mappings when all keys are known at template expansion time
|
185
|
+
if map.is_a?(String) && key.is_a?(String) && name.is_a?(String)
|
186
|
+
# We don't know whether the map was built with string keys or symbol keys. Try both.
|
187
|
+
def get(map, key) map[key] || map.fetch(key.to_sym) end
|
188
|
+
get(get(@dict.fetch(:Mappings).fetch(map), key), name)
|
189
|
+
else
|
190
|
+
{ :'Fn::FindInMap' => [ map, key, name ] }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
def _get_parameter_from_cli(name, options)
|
196
|
+
# basic request
|
197
|
+
param_request = "Parameter '#{name}' (#{options[:Type]})"
|
198
|
+
|
199
|
+
# add description to request
|
200
|
+
if options.has_key?(:Description)
|
201
|
+
param_request += "\nDescription: #{options[:Description]}"
|
202
|
+
end
|
203
|
+
|
204
|
+
# add validation to the request
|
205
|
+
|
206
|
+
# allowed pattern
|
207
|
+
if options.has_key?(:AllowedPattern)
|
208
|
+
param_request += "\nAllowed Pattern: /#{options[:AllowedPattern]}/"
|
209
|
+
end
|
210
|
+
|
211
|
+
# allowed values
|
212
|
+
if options.has_key?(:AllowedValues)
|
213
|
+
param_request += "\nAllowed Values: #{options[:AllowedValues].join(', ')}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# min/max length
|
217
|
+
if options.has_key?(:MinLength) or options.has_key?(:MaxLength)
|
218
|
+
min_length = "-infinity"
|
219
|
+
max_length = "+infinity"
|
220
|
+
if options.has_key?(:MinLength)
|
221
|
+
min_length = options[:MinLength]
|
222
|
+
end
|
223
|
+
if options.has_key?(:MaxLength)
|
224
|
+
max_length = options[:MaxLength]
|
225
|
+
end
|
226
|
+
param_request += "\nValid Length: #{min_length} < string < #{max_length}"
|
227
|
+
end
|
228
|
+
|
229
|
+
# min/max value
|
230
|
+
if options.has_key?(:MinValue) or options.has_key?(:MaxValue)
|
231
|
+
min_value = "-infinity"
|
232
|
+
max_value = "+infinity"
|
233
|
+
if options.has_key?(:MinValue)
|
234
|
+
min_value = options[:MinValue]
|
235
|
+
end
|
236
|
+
if options.has_key?(:MaxValue)
|
237
|
+
max_value = options[:MaxValue]
|
238
|
+
end
|
239
|
+
param_request += "\nValid Number: #{min_value} < number < #{max_value}"
|
240
|
+
end
|
241
|
+
|
242
|
+
# add default to request
|
243
|
+
if options.has_key?(:Default) and !options[:Default].nil?
|
244
|
+
param_request += "\nLeave value empty for default: #{options[:Default]}"
|
245
|
+
end
|
246
|
+
|
247
|
+
param_request += "\nValue: "
|
248
|
+
|
249
|
+
# request the param
|
250
|
+
$stdout.puts "===================="
|
251
|
+
$stdout.print param_request
|
252
|
+
input = $stdin.gets.chomp
|
253
|
+
|
254
|
+
if input.nil? or input.empty?
|
255
|
+
options[:Default]
|
256
|
+
else
|
257
|
+
input
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def base64(value) { :'Fn::Base64' => value } end
|
263
|
+
|
264
|
+
def find_in_map(map, key, name) { :'Fn::FindInMap' => [ map, key, name ] } end
|
265
|
+
|
266
|
+
def get_att(resource, attribute) { :'Fn::GetAtt' => [ resource, attribute ] } end
|
267
|
+
|
268
|
+
def get_azs(region = '') { :'Fn::GetAZs' => region } end
|
269
|
+
|
270
|
+
def import_value(value) { :'Fn::ImportValue' => value } end
|
271
|
+
|
272
|
+
# There are two valid forms of Fn::Sub, with a map and without.
|
273
|
+
def sub(sub_string, var_map = {})
|
274
|
+
if var_map.empty?
|
275
|
+
return { :'Fn::Sub' => sub_string }
|
276
|
+
else
|
277
|
+
return { :'Fn::Sub' => [sub_string, var_map] }
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def join(delim, *list)
|
282
|
+
case list.length
|
283
|
+
when 0 then ''
|
284
|
+
else join_list(delim,list)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Variant of join that matches the native CFN syntax.
|
289
|
+
def join_list(delim, list) { :'Fn::Join' => [ delim, list ] } end
|
290
|
+
|
291
|
+
def equal(one, two) { :'Fn::Equals' => [one, two] } end
|
292
|
+
|
293
|
+
def fn_not(condition) { :'Fn::Not' => [condition] } end
|
294
|
+
|
295
|
+
def fn_or(*condition_list)
|
296
|
+
case condition_list.length
|
297
|
+
when 0..1 then raise "fn_or needs at least 2 items."
|
298
|
+
when 2..10 then { :'Fn::Or' => condition_list }
|
299
|
+
else raise "fn_or needs a list of 2-10 items that evaluate to true/false."
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def fn_and(*condition_list)
|
304
|
+
case condition_list.length
|
305
|
+
when 0..1 then raise "fn_and needs at least 2 items."
|
306
|
+
when 2..10 then { :'Fn::And' => condition_list }
|
307
|
+
else raise "fn_and needs a list of 2-10 items that evaluate to true/false."
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def fn_if(cond, if_true, if_false) { :'Fn::If' => [cond, if_true, if_false] } end
|
312
|
+
|
313
|
+
def not_equal(one, two) fn_not(equal(one,two)) end
|
314
|
+
|
315
|
+
def select(index, list) { :'Fn::Select' => [ index, list ] } end
|
316
|
+
|
317
|
+
def split(delimiter, source_str) { :'Fn::Split' => [delimiter, source_str] } end
|
318
|
+
|
319
|
+
def ref(name) { :Ref => name } end
|
320
|
+
|
321
|
+
def aws_account_id() ref("AWS::AccountId") end
|
322
|
+
|
323
|
+
def aws_notification_arns() ref("AWS::NotificationARNs") end
|
324
|
+
|
325
|
+
def aws_no_value() ref("AWS::NoValue") end
|
326
|
+
|
327
|
+
def aws_stack_id() ref("AWS::StackId") end
|
328
|
+
|
329
|
+
def aws_stack_name() ref("AWS::StackName") end
|
330
|
+
|
331
|
+
# deprecated, for backward compatibility
|
332
|
+
def no_value()
|
333
|
+
warn_deprecated('no_value()', 'aws_no_value()')
|
334
|
+
aws_no_value()
|
335
|
+
end
|
336
|
+
|
337
|
+
# Read the specified file and return its value as a string literal
|
338
|
+
def file(filename) File.read(File.absolute_path(filename, File.dirname($PROGRAM_NAME))) end
|
339
|
+
|
340
|
+
# Interpolates a string like "NAME={{ref('Service')}}" and returns a CloudFormation "Fn::Join"
|
341
|
+
# operation to collect the results. Anything between {{ and }} is interpreted as a Ruby expression
|
342
|
+
# and eval'd. This is especially useful with Ruby "here" documents.
|
343
|
+
# Local variables may also be exposed to the string via the `locals` hash.
|
344
|
+
def interpolate(string, locals={})
|
345
|
+
list = []
|
346
|
+
while string.length > 0
|
347
|
+
head, match, string = string.partition(/\{\{.*?\}\}/)
|
348
|
+
list << head if head.length > 0
|
349
|
+
list << eval(match[2..-3], nil, 'interpolated string') if match.length > 0
|
350
|
+
end
|
351
|
+
|
352
|
+
# Split out strings in an array by newline, for visibility
|
353
|
+
list = list.flat_map {|value| value.is_a?(String) ? value.lines.to_a : value }
|
354
|
+
join('', *list)
|
355
|
+
end
|
356
|
+
|
357
|
+
def join_interpolate(delim, string)
|
358
|
+
$stderr.puts "join_interpolate(delim,string) has been deprecated; use interpolate(string) instead"
|
359
|
+
interpolate(string)
|
360
|
+
end
|
361
|
+
|
362
|
+
# This class is used by erb templates so they can access the parameters passed
|
363
|
+
class Namespace
|
364
|
+
attr_accessor :params
|
365
|
+
def initialize(hash)
|
366
|
+
@params = hash
|
367
|
+
end
|
368
|
+
def get_binding
|
369
|
+
binding
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Combines the provided ERB template with optional parameters
|
374
|
+
def erb_template(filename, params = {})
|
375
|
+
ERB.new(file(filename), nil, '-').result(Namespace.new(params).get_binding)
|
376
|
+
end
|
377
|
+
|
378
|
+
def warn_deprecated(old, new)
|
379
|
+
$stderr.puts "Warning: '#{old}' has been deprecated. Please update your template to use '#{new}' instead."
|
380
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
# This list of prices was sourced from the on-demand prices current as of 12/3/2012.
|
16
|
+
# We expect the actual price we pay per instance to be roughly 1/10 the prices below.
|
17
|
+
SPOT_PRICES_BY_INSTANCE_TYPE = {
|
18
|
+
"m1.small" => 0.065,
|
19
|
+
"m1.medium" => 0.130,
|
20
|
+
"m1.large" => 0.260,
|
21
|
+
"m1.xlarge" => 0.520,
|
22
|
+
"m3.xlarge" => 0.580,
|
23
|
+
"m3.2xlarge" => 1.160,
|
24
|
+
"t1.micro" => 0.020,
|
25
|
+
"m2.xlarge" => 0.450,
|
26
|
+
"m2.2xlarge" => 0.900,
|
27
|
+
"m2.4xlarge" => 1.800,
|
28
|
+
"c1.medium" => 0.165,
|
29
|
+
"c1.xlarge" => 0.660,
|
30
|
+
"cc1.4xlarge" => 1.300,
|
31
|
+
"cc2.8xlarge" => 2.400,
|
32
|
+
"cg1.4xlarge" => 2.100,
|
33
|
+
"hi1.4xlarge" => 3.100,
|
34
|
+
"hs1.8xlarge" => 4.600,
|
35
|
+
"cr1.8xlarge" => 4.000,
|
36
|
+
}
|
37
|
+
|
38
|
+
def spot_price(spot_price_string, instance_type)
|
39
|
+
case spot_price_string
|
40
|
+
when 'false', '' then aws_no_value()
|
41
|
+
when 'true' then spot_price_for_instance_type(instance_type)
|
42
|
+
else spot_price_string
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def spot_price_for_instance_type(instance_type)
|
47
|
+
# Add 10% to ensure that we have a small buffer against current spot prices increasing
|
48
|
+
# to the on-demand prices, which theoretically could happen often.
|
49
|
+
SPOT_PRICES_BY_INSTANCE_TYPE[instance_type] * 1.10
|
50
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'detabulator'
|
16
|
+
|
17
|
+
class Table
|
18
|
+
def self.load(filename)
|
19
|
+
self.new File.read filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(table_as_text)
|
23
|
+
raw_header, *raw_data = Detabulator.new.detabulate table_as_text
|
24
|
+
@header = raw_header.map(&:to_sym)
|
25
|
+
@records = raw_data.map { |row| Hash[@header.zip(row)] }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns
|
29
|
+
# the single distinct value from those rows for the specified key.
|
30
|
+
def get(key, predicate)
|
31
|
+
distinct_values(filter(predicate), key, false)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Select the headers as list. Argument(s) will be excluded from output.
|
35
|
+
def get_header(*exclude)
|
36
|
+
@header.reject{ |key| key if exclude.include?(key) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns
|
40
|
+
# all distinct values from those rows for the specified key.
|
41
|
+
def get_list(key, predicate)
|
42
|
+
distinct_values(filter(predicate), key, true)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns a
|
46
|
+
# hash of hashes, where the key for the top-level hash is the key paramter and the second-level hash keys are
|
47
|
+
# those in the keys paramter. This is useful when you want multiple column values for a given row.
|
48
|
+
def get_multihash(key, predicate, *keys)
|
49
|
+
build_nested_hash(filter(predicate), key, keys)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns a
|
53
|
+
# set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
|
54
|
+
# except for the last key in the specified keys which is used to determine the value of the leaf-level map.
|
55
|
+
# In the simple case where keys is a list of 2 elements, this returns a map from key[0] to key[1].
|
56
|
+
def get_map(predicate, *keys)
|
57
|
+
build_nested_map(filter(predicate), keys, false)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns a
|
61
|
+
# set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
|
62
|
+
# except for the last key in the specified keys which is used to determine the list of values in the
|
63
|
+
# leaf-level map. In the simple case where keys is a list of 2 elements, this returns a map from key[0]
|
64
|
+
# to a list of values for key[1].
|
65
|
+
def get_multimap(predicate, *keys)
|
66
|
+
build_nested_map(filter(predicate), keys, true)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Return the subset of records that match the predicate for all keys in the predicate.
|
72
|
+
# The predicate is expected to be a map of key/value or key/[value,...] pairs.
|
73
|
+
def filter(predicate)
|
74
|
+
def matches(predicate_value, record_value)
|
75
|
+
if predicate_value.is_a?(Array); predicate_value.include?(record_value)
|
76
|
+
else; predicate_value == record_value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@records.select { |record| predicate.all? { |key, value| matches(value, record[key]) } }
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_nested_hash(records, key, keys)
|
84
|
+
hash = {}
|
85
|
+
records.each do |record|
|
86
|
+
hash[record[key]] = {}
|
87
|
+
keys.each do |hash_key|
|
88
|
+
hash[record[key]][hash_key] = record[hash_key]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
return hash
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_nested_map(records, path, multi)
|
95
|
+
key, *rest = path
|
96
|
+
if rest.empty?
|
97
|
+
# Build the leaf level of the data structure
|
98
|
+
distinct_values(records, key, multi)
|
99
|
+
else
|
100
|
+
# Return a hash keyed by the distinct values of the first key and values are the result of a
|
101
|
+
# recursive invocation of arrange() with the rest of the keys
|
102
|
+
result = {}
|
103
|
+
records.group_by do |record|
|
104
|
+
record[key]
|
105
|
+
end.map do |value, group|
|
106
|
+
result[value] = build_nested_map(group, rest, multi)
|
107
|
+
end
|
108
|
+
result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def distinct_values(records, key, multi)
|
113
|
+
values = records.map { |record| record[key] }.uniq
|
114
|
+
if multi
|
115
|
+
# In a multimap the leaf level is a list of string values
|
116
|
+
values
|
117
|
+
else
|
118
|
+
# In a non-multimap the leaf level is a single string value
|
119
|
+
raise "Multiple distinct values for the same key '#{key}': #{records.inspect}" if values.length > 1
|
120
|
+
values[0]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module Cfn
|
16
|
+
module Ruby
|
17
|
+
module Dsl
|
18
|
+
VERSION = "0.0.1-pre1"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|