right_develop 2.1.5 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. data/VERSION +1 -1
  2. data/bin/right_develop +4 -0
  3. data/lib/right_develop.rb +1 -0
  4. data/lib/right_develop/commands.rb +2 -1
  5. data/lib/right_develop/commands/server.rb +194 -0
  6. data/lib/right_develop/testing.rb +31 -0
  7. data/lib/right_develop/testing/clients.rb +36 -0
  8. data/lib/right_develop/testing/clients/rest.rb +34 -0
  9. data/lib/right_develop/testing/clients/rest/requests.rb +38 -0
  10. data/lib/right_develop/testing/clients/rest/requests/base.rb +305 -0
  11. data/lib/right_develop/testing/clients/rest/requests/playback.rb +293 -0
  12. data/lib/right_develop/testing/clients/rest/requests/record.rb +175 -0
  13. data/lib/right_develop/testing/recording.rb +33 -0
  14. data/lib/right_develop/testing/recording/config.rb +571 -0
  15. data/lib/right_develop/testing/recording/metadata.rb +805 -0
  16. data/lib/right_develop/testing/servers/might_api/.gitignore +3 -0
  17. data/lib/right_develop/testing/servers/might_api/Gemfile +6 -0
  18. data/lib/right_develop/testing/servers/might_api/Gemfile.lock +18 -0
  19. data/lib/right_develop/testing/servers/might_api/app/base.rb +323 -0
  20. data/lib/right_develop/testing/servers/might_api/app/echo.rb +73 -0
  21. data/lib/right_develop/testing/servers/might_api/app/playback.rb +46 -0
  22. data/lib/right_develop/testing/servers/might_api/app/record.rb +45 -0
  23. data/lib/right_develop/testing/servers/might_api/config.ru +8 -0
  24. data/lib/right_develop/testing/servers/might_api/config/init.rb +47 -0
  25. data/lib/right_develop/testing/servers/might_api/lib/config.rb +204 -0
  26. data/lib/right_develop/testing/servers/might_api/lib/logger.rb +68 -0
  27. data/right_develop.gemspec +30 -2
  28. metadata +84 -34
@@ -0,0 +1,33 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # ancestor
24
+ require 'right_develop/testing'
25
+
26
+ module RightDevelop
27
+ module Testing
28
+ module Recording
29
+ autoload :Config, 'right_develop/testing/recording/config'
30
+ autoload :Metadata, 'right_develop/testing/recording/metadata'
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,571 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # ancestor
24
+ require 'right_develop/testing/recording'
25
+ require 'extlib'
26
+ require 'json'
27
+ require 'logger'
28
+ require 'rack/utils'
29
+ require 'uri'
30
+ require 'yaml'
31
+
32
+ module RightDevelop::Testing::Recording
33
+
34
+ # Config file format.
35
+ class Config
36
+
37
+ # default relative directories.
38
+ FIXTURES_DIR_NAME = 'fixtures'.freeze
39
+ LOG_DIR_NAME = 'log'.freeze
40
+
41
+ # the empty key is used as a stop traversal signal because any literal
42
+ # value would be ambiguous.
43
+ STOP_TRAVERSAL_KEY = ''.freeze
44
+
45
+ VALID_MODES = ::Mash.new(
46
+ :echo => 'Echoes request back as response and validates route.',
47
+ :playback => 'Playback a session for one or more stubbed web services.',
48
+ :record => 'Record a session for one or more proxied web services.'
49
+ ).freeze
50
+
51
+ # keys allowed under the deep route configuration.
52
+ ALLOWED_KINDS = %w(request response)
53
+ ALLOWED_CONFIG_ACTIONS = %w(significant timeouts transform variables)
54
+ ALLOWED_TIMEOUTS = %w(open_timeout read_timeout)
55
+ ALLOWED_VARIABLE_TYPES = %w(body header query)
56
+
57
+ # metadata.
58
+ METADATA_CLASS = ::RightDevelop::Testing::Recording::Metadata
59
+
60
+ # patterns for fixture files.
61
+ FIXTURE_FILE_NAME_REGEX = /^([0-9A-Fa-f]{32}).yml$/
62
+
63
+ # typename to value expression for significant/requests/responses configurations.
64
+ TYPE_NAME_VALUE_REGEX = /^(body|header|query|verb)(?:\:([^=]+))?=(.*)$/
65
+
66
+ # exceptions.
67
+ class ConfigError < StandardError; end
68
+
69
+ def initialize(config_hash, options = nil)
70
+ # defaults.
71
+ current_dir = ::Dir.pwd
72
+ defaults = ::Mash.new(
73
+ 'mode' => :playback,
74
+ 'routes' => {},
75
+ 'fixtures_dir' => ::File.expand_path(FIXTURES_DIR_NAME, current_dir),
76
+ 'log_level' => :info,
77
+ 'log_dir' => ::File.expand_path(LOG_DIR_NAME, current_dir),
78
+ 'throttle' => 0,
79
+ )
80
+ unless config_hash.kind_of?(::Hash)
81
+ raise ConfigError, 'config_hash must be a hash'
82
+ end
83
+
84
+ # shallow merge of hash because the defaults are a shallow hash. deep mash
85
+ # of caller's config to deep clone and normalize keys.
86
+ config_hash = defaults.merge(deep_mash(config_hash))
87
+ if options
88
+ # another deep merge of any additional options.
89
+ ::RightSupport::Data::HashTools.deep_merge!(config_hash, options)
90
+ end
91
+
92
+ @config_hash = ::Mash.new
93
+ mode(config_hash['mode'])
94
+ routes(config_hash['routes'])
95
+ log_dir(config_hash['log_dir'])
96
+ log_level(config_hash['log_level'])
97
+ fixtures_dir(config_hash['fixtures_dir'])
98
+ throttle(config_hash['throttle'])
99
+ end
100
+
101
+ # @return [String] raw hash representing complete configuration
102
+ def to_hash
103
+ # unmash to hash
104
+ ::JSON.load(@config_hash.to_json)
105
+ end
106
+
107
+ # @return [String] location of fixtures used for record/playback
108
+ def fixtures_dir(value = nil)
109
+ @config_hash['fixtures_dir'] = value if value
110
+ @config_hash['fixtures_dir']
111
+ end
112
+
113
+ def mode(value = nil)
114
+ if value
115
+ value = value.to_s
116
+ if value.empty?
117
+ raise ConfigError, "#{MODE_ENV_VAR} must be set"
118
+ elsif VALID_MODES.has_key?(value)
119
+ @config_hash['mode'] = value.to_sym
120
+ else
121
+ raise ConfigError, "mode must be one of #{VALID_MODES.keys.sort.inspect}: #{value.inspect}"
122
+ end
123
+ end
124
+ @config_hash['mode']
125
+ end
126
+
127
+ def routes(value = nil)
128
+ if value
129
+ case value
130
+ when Hash
131
+ # normalize routes for efficient usage but keep them separate from
132
+ # user's config so that .to_hash returns something understandable and
133
+ # JSONizable/YAMLable.
134
+ @normalized_routes = value.inject(::Mash.new) do |r, (k, v)|
135
+ r[normalize_route_prefix(k)] = normalize_route_data(k, v)
136
+ r
137
+ end
138
+ @config_hash['routes'] = ::RightSupport::Data::HashTools.deep_clone(value)
139
+ else
140
+ raise ConfigError, 'routes must be a hash'
141
+ end
142
+ end
143
+ @normalized_routes
144
+ end
145
+
146
+ def log_level(value = nil)
147
+ if value
148
+ case value
149
+ when Integer
150
+ if value < ::Logger::DEBUG || value >= ::Logger::UNKNOWN
151
+ raise ConfigError, "log_level is out of range: #{value}"
152
+ end
153
+ @config_hash['log_level'] = value
154
+ when String, Symbol
155
+ @config_hash['log_level'] = ::Logger.const_get(value.to_s.upcase)
156
+ else
157
+ raise ConfigError, "log_level is unexpected type: #{log_level}"
158
+ end
159
+ end
160
+ @config_hash['log_level']
161
+ end
162
+
163
+ def log_dir(value = nil)
164
+ @config_hash['log_dir'] = value if value
165
+ @config_hash['log_dir']
166
+ end
167
+
168
+ def throttle(value = nil)
169
+ if value
170
+ value = Integer(value)
171
+ if value < 0 || value > 100
172
+ raise ConfigError, "throttle is out of range: #{value}"
173
+ end
174
+ @config_hash['throttle'] = value
175
+ end
176
+ @config_hash['throttle']
177
+ end
178
+
179
+ # Loads the config from given path or a relative location.
180
+ #
181
+ # @param [String] path to configuration
182
+ # @param [Hash] options to merge after loading config hash
183
+ #
184
+ # @return [Config] configuration object
185
+ #
186
+ # @raise [ArgumentError] on failure to load
187
+ def self.from_file(path, options = nil)
188
+ # load
189
+ unless ::File.file?(path)
190
+ raise ConfigError, "Missing expected configuration file: #{path.inspect}"
191
+ end
192
+ config_hash = deep_mash(::YAML.load_file(path))
193
+
194
+ # enumerate routes looking for any route-specific config data to load
195
+ # into the config hash from .yml files in subdirectories. this allows
196
+ # the user to spread configuration of specific requests/responses out in
197
+ # the file system instead of having a single monster config .yml
198
+ extension = '.yml'
199
+ (config_hash[:routes] || {}).each do |route_path, route_data|
200
+ if subdir = route_data[:subdir]
201
+ route_subdir = ::File.expand_path(::File.join(path, '..', subdir))
202
+ ::Dir[::File.join(route_subdir, "**/*#{extension}")].each do |route_config_path|
203
+ route_config_data = ::Mash.new(::YAML.load_file(route_config_path))
204
+ filename = ::File.basename(route_config_path)[0..-(extension.length + 1)]
205
+ hash_path = ::File.dirname(route_config_path)[(route_subdir.length + 1)..-1].split('/')
206
+ unless current_route_data = ::RightSupport::Data::HashTools.deep_get(route_data, hash_path)
207
+ current_route_data = ::Mash.new
208
+ ::RightSupport::Data::HashTools.deep_set!(route_data, hash_path, current_route_data)
209
+ end
210
+
211
+ # inject a 'stop' at the point where the sub-config file data was
212
+ # inserted into the big hash. the 'stop' basically distingishes the
213
+ # 'directory' from the 'file' information because the hash doesn't
214
+ # use classes to distinguish the data it contains; it only uses
215
+ # simple types. use of simple types makes it easy to YAMLize or
216
+ # JSONize or otherwise serialize in round-trip fashion.
217
+ merge_data = { filename => { STOP_TRAVERSAL_KEY => route_config_data } }
218
+ ::RightSupport::Data::HashTools.deep_merge!(current_route_data, merge_data)
219
+ end
220
+ end
221
+ end
222
+
223
+ # config
224
+ self.new(config_hash, options)
225
+ end
226
+
227
+ # Deeply mashes and duplicates (clones) a hash containing other hashes or
228
+ # arrays of hashes but not other types.
229
+ #
230
+ # Note that Mash.new(my_mash) will convert child hashes to mashes but not
231
+ # with the guarantee of cloning and detaching the deep mash. In other words.
232
+ # if any part of the hash is already a mash then it is not cloned by
233
+ # invoking Mash.new()
234
+ #
235
+ # FIX: put this in HashTools ?
236
+ #
237
+ # @return [Object] depends on input type
238
+ def self.deep_mash(any)
239
+ case any
240
+ when Array
241
+ # traverse arrays
242
+ any.map { |i| deep_mash(i) }
243
+ when Hash
244
+ # mash the hash
245
+ any.inject(::Mash.new) do |m, (k, v)|
246
+ m[k] = deep_mash(v)
247
+ m
248
+ end
249
+ else
250
+ any # whatever
251
+ end
252
+ end
253
+
254
+ protected
255
+
256
+ # @see RightDevelop::Testing::Client::RecordMetadata.normalize_header_key
257
+ def normalize_header_key(key)
258
+ METADATA_CLASS.normalize_header_key(key)
259
+ end
260
+
261
+ # @see Config.deep_mash
262
+ def deep_mash(any)
263
+ self.class.deep_mash(any)
264
+ end
265
+
266
+ def normalize_route_prefix(prefix)
267
+ prefix = prefix.to_s
268
+ unless prefix.end_with?('/')
269
+ prefix += '/'
270
+ end
271
+ prefix
272
+ end
273
+
274
+ def normalize_route_data(route_path, route_data)
275
+ position = ['routes', "#{route_path} (#{route_data[:subdir].inspect})"]
276
+ case route_data
277
+ when Hash
278
+ route_data = deep_mash(route_data) # deep clone and mash
279
+ case mode
280
+ when :record
281
+ uri = nil
282
+ begin
283
+ uri = ::URI.parse(route_data[:url])
284
+ rescue URI::InvalidURIError
285
+ # defer handling
286
+ end
287
+ unless uri && uri.scheme && uri.host
288
+ raise ConfigError, "#{position_string(position, 'url')} must be a valid HTTP(S) URL: #{route_data.inspect}"
289
+ end
290
+ unless uri.path.to_s.empty? && uri.query.to_s.empty?
291
+ raise ConfigError, "#{position_string(position, 'url')} has unexpected path or query string: #{route_data.inspect}"
292
+ end
293
+ end
294
+ subdir = route_data[:subdir]
295
+ if subdir.nil? || subdir.empty?
296
+ raise ConfigError, "#{position_string(position, 'subdir')} is required: #{route_data.inspect}"
297
+ end
298
+ if proxy_data = route_data[:proxy]
299
+ if header_data = proxy_data[:header]
300
+ if case_value = header_data[:case]
301
+ case case_value = case_value.to_s.to_sym
302
+ when :lower, :upper, :capitalize
303
+ header_data[:case] = case_value
304
+ else
305
+ raise ConfigError, "#{position_string(position, 'proxy/headers/case')} must be one of [lower, upper, capitalize]: #{route_data.inspect}"
306
+ end
307
+ end
308
+ if separator_value = header_data[:separator]
309
+ case separator_value = separator_value.to_s.to_sym
310
+ when :dash, :underscore
311
+ header_data[:separator] = separator_value
312
+ else
313
+ raise ConfigError, "#{position_string(position, 'proxy/headers/separator')} must be one of [dash, underscore]: #{route_data.inspect}"
314
+ end
315
+ end
316
+ end
317
+ end
318
+ matchers_key = METADATA_CLASS::MATCHERS_KEY
319
+ if matchers_data = route_data[matchers_key]
320
+ route_data[matchers_key] = normalize_route_configuration(
321
+ route_path,
322
+ position + [matchers_key],
323
+ matchers_data)
324
+ end
325
+ else
326
+ raise ConfigError, "route must be a hash: #{route_data.class}"
327
+ end
328
+ route_data
329
+ end
330
+
331
+ # Formats a displayable position string.
332
+ #
333
+ # @param [String] position as base
334
+ # @param [String|Array] subpath to join or nil
335
+ #
336
+ # @return [String] displayable config-root relative position string
337
+ def position_string(position, subpath = nil)
338
+ "might_config[#{(position + Array(subpath).join('/').split('/')).join('][')}]"
339
+ end
340
+
341
+ # Converts hierarchical hash of URI path fragments to sub-configurations to
342
+ # a flat hash of regular expressions matching URI to sub-configuration.
343
+ # Additional matchers such as verb or header are not included in the regex.
344
+ # this is intended to reduce how much searching is needed to find the
345
+ # configuration for a particular request/response. Ruby has no direct
346
+ # support for hashing by matching a regular expression to a value but the
347
+ # hash idiom is still useful here.
348
+ def normalize_route_configuration(uri_path, position, configuration_data)
349
+ uri_path = uri_path[1..-1] if uri_path.start_with?('/')
350
+ uri_path = uri_path.chomp('/').split('/')
351
+ recursive_traverse_uri(regex_to_data = {}, position, uri_path, configuration_data)
352
+ end
353
+
354
+ # Builds regular expressions from URI paths by recursively looking for
355
+ # either the 'stop' key or the start of matcher information. When the path
356
+ # for the URI is complete the wildcard 'file' path is converted to a regular
357
+ # expression and inserted into the regex_to_data hash. The data has a regex
358
+ # on the left-hand and a matcher data hash on the right-hand. The matcher
359
+ # data may have empty criteria if no matching beyond the URI path is needed.
360
+ def recursive_traverse_uri(regex_to_data, position, uri_path, data)
361
+ unless data.respond_to?(:has_key?)
362
+ message = "Expected a hash at #{position_string(position, uri_path)}; " +
363
+ "use the #{STOP_TRAVERSAL_KEY.inspect} key to stop traversal."
364
+ raise ConfigError, message
365
+ end
366
+
367
+ data.each do |k, v|
368
+ # stop key or 'type:name=value' qualifier stops URI path traversal.
369
+ k = k.to_s
370
+ if k == STOP_TRAVERSAL_KEY || k.index('=')
371
+ # create regular expression from uri_path elements up to this point.
372
+ # include a leading forward-slash (/) because uri.path will generally
373
+ # have it.
374
+ regex_string = '^/' + uri_path.map do |path_element|
375
+ case path_element
376
+ when '**'
377
+ '.*' # example: 'api/**'
378
+ else
379
+ # element may contain single wildcard character (*)
380
+ ::Regexp.escape(path_element).gsub("\\*", '[^/]*')
381
+ end
382
+ end.join('/') + '$'
383
+ regex = ::Regexp.compile(regex_string)
384
+
385
+ # URI path is the outermost qualifier, but there can be literal verb,
386
+ # header, query qualifiers as well. create another interesting map of
387
+ # (qualifier name to value) to (configuration data). the qualifiers
388
+ # can be hierarchical, of course, so we must traverse and flatten
389
+ # those also.
390
+ #
391
+ # examples of resulting uri regex to matcher data:
392
+ # request:
393
+ # /^api\/create$/ => { { 'verb' => 'POST', 'header' => { 'x_foo' => 'foo' } } => { 'body' => { 'name' => 'foo_name_variable' } } }
394
+ # response:
395
+ # { { 'verb' => 'GET', 'query' => { 'view' => 'full' } } => { 'body' => { 'name' => 'foo_name_variable' } } }
396
+ #
397
+ # FIX: don't think there is a need for wildcard qualifiers beyond URI
398
+ # path (i.e. wildcard matchers) so they are not currently supported.
399
+ qualifiers_to_data = regex_to_data[regex] ||= {}
400
+ current_qualifiers = ::Mash.new
401
+ if k == STOP_TRAVERSAL_KEY
402
+ # no qualifiers; stopped after URI path
403
+ qualifiers_to_data[current_qualifiers] = normalize_route_stop_configuration(position, uri_path + [k], v)
404
+ else
405
+ recursive_traverse_qualifiers(qualifiers_to_data, current_qualifiers, position, uri_path + [k], v)
406
+ end
407
+ else
408
+ # recursion
409
+ recursive_traverse_uri(regex_to_data, position, uri_path + [k], v)
410
+ end
411
+ end
412
+ regex_to_data
413
+ end
414
+
415
+ # Recursively builds one of more hash of qualifiers cumulatively with the
416
+ # parent qualifiers being included in child qualifier hashes. The completed
417
+ # qualifier hash is then inserted into the qualifiers_to_data hash.
418
+ def recursive_traverse_qualifiers(qualifiers_to_data, current_qualifiers, position, subpath, data)
419
+ unless data.kind_of?(::Hash)
420
+ message = "Expected a hash at #{position_string(position, subpath)}; " +
421
+ "use the #{STOP_TRAVERSAL_KEY.inspect} key to stop traversal."
422
+ raise ConfigError, message
423
+ end
424
+
425
+ # could be multiple qualifiers in a CGI-style string.
426
+ current_qualifiers = ::Mash.new(current_qualifiers)
427
+ more_qualifiers = ::Mash.new
428
+ ::CGI.unescape(subpath.last).split('&').each do |q|
429
+ if matched = TYPE_NAME_VALUE_REGEX.match(q)
430
+ case qualifier_type = matched[1]
431
+ when 'verb'
432
+ if matched[2]
433
+ message = "Verb qualifiers cannot have a name: " +
434
+ position_string(position, subpath)
435
+ raise ConfigError, message
436
+ end
437
+ verb = matched[3].upcase
438
+ unless METADATA_CLASS::VERBS.include?(verb)
439
+ message = "Unknown verb = #{verb}: " +
440
+ position_string(position, subpath + [k])
441
+ raise ConfigError, message
442
+ end
443
+ more_qualifiers[qualifier_type] = verb
444
+ else
445
+ unless matched[2]
446
+ message = "#{qualifier_type} qualifiers must have a name: " +
447
+ position_string(position, subpath)
448
+ raise ConfigError, message
449
+ end
450
+
451
+ # qualifier may be nested (for query string or body).
452
+ qualifier = ::Rack::Utils.parse_nested_query("#{matched[2]}=#{matched[3]}")
453
+
454
+ # names and values are case sensitive but header keys are usually
455
+ # not case-sensitive so convert the header keys to snake_case to
456
+ # match normalized headers from request.
457
+ if qualifier_type == 'header'
458
+ qualifier = qualifier.inject(::Mash.new) do |h, (k, v)|
459
+ h[normalize_header_key(k)] = v
460
+ h
461
+ end
462
+ end
463
+ ::RightSupport::Data::HashTools.deep_merge!(
464
+ more_qualifiers[qualifier_type] ||= ::Mash.new,
465
+ qualifier)
466
+ end
467
+ else
468
+ message = "Qualifier does not match expected pattern: " +
469
+ position_string(position, subpath)
470
+ raise ConfigError, message
471
+ end
472
+ end
473
+ ::RightSupport::Data::HashTools.deep_merge!(current_qualifiers, more_qualifiers)
474
+
475
+ data.each do |k, v|
476
+ # only the 'stop' key stops traversal now.
477
+ k = k.to_s
478
+ if k == STOP_TRAVERSAL_KEY
479
+ qualifiers_to_data[current_qualifiers] = normalize_route_stop_configuration(position, subpath, v)
480
+ else
481
+ # recursion
482
+ recursive_traverse_qualifiers(qualifiers_to_data, current_qualifiers, position, subpath + [k], v)
483
+ end
484
+ end
485
+ qualifiers_to_data
486
+ end
487
+
488
+ # Ensures that any header keys are normalized in deep stop configuration.
489
+ # Only header keys are normalized; other fields are case-sensitive and
490
+ # otherwise adhere to a standard specific to the API for field names.
491
+ def normalize_route_stop_configuration(position, subpath, route_stop_config)
492
+
493
+ # sanity check.
494
+ unwanted_keys = route_stop_config.keys.map(&:to_s) - ALLOWED_KINDS
495
+ unless unwanted_keys.empty?
496
+ message = 'The route configuration for route configuration at ' +
497
+ "#{position_string(position, subpath)} " +
498
+ "contained illegal kind specifiers = " +
499
+ "#{unwanted_keys.inspect}. Only #{ALLOWED_KINDS} are allowed."
500
+ raise ConfigError, message
501
+ end
502
+
503
+ route_stop_config.inject(::Mash.new) do |rst, (rst_k, rst_v)|
504
+
505
+ # sanity check.
506
+ unwanted_keys = rst_v.keys.map(&:to_s) - ALLOWED_CONFIG_ACTIONS
507
+ unless unwanted_keys.empty?
508
+ message = 'The route configuration for route configuration at ' +
509
+ "#{position_string(position, subpath + [rst_k])} " +
510
+ "contained illegal action specifiers = " +
511
+ "#{unwanted_keys.inspect}. Only #{ALLOWED_CONFIG_ACTIONS} are allowed."
512
+ raise ConfigError, message
513
+ end
514
+
515
+ rst[rst_k] = rst_v.inject(::Mash.new) do |kc, (kc_k, kc_v)|
516
+ case kc_k
517
+ when METADATA_CLASS::TIMEOUTS_KEY
518
+ # sanity check.
519
+ kc_v = kc_v.inject(::Mash.new) do |h, (k, v)|
520
+ h[k] = Integer(v)
521
+ h
522
+ end
523
+ unwanted_keys = kc_v.keys - ALLOWED_TIMEOUTS
524
+ unless unwanted_keys.empty?
525
+ message = 'The route configuration for timeouts at ' +
526
+ "#{position_string(position, subpath + [rst_k, kc_k])} " +
527
+ "contained illegal timeout specifiers = " +
528
+ "#{unwanted_keys.inspect}. Only #{ALLOWED_TIMEOUTS} are allowed."
529
+ raise ConfigError, message
530
+ end
531
+ else
532
+ # sanity check.
533
+ kc_v = deep_mash(kc_v)
534
+ unwanted_keys = kc_v.keys - ALLOWED_VARIABLE_TYPES
535
+ unless unwanted_keys.empty?
536
+ message = 'The route configuration for variables at ' +
537
+ "#{position_string(position, subpath + [rst_k, kc_k])} " +
538
+ "contained illegal variable specifiers = " +
539
+ "#{unwanted_keys.inspect}. Only #{ALLOWED_VARIABLE_TYPES} are allowed."
540
+ raise ConfigError, message
541
+ end
542
+
543
+ if headers = kc_v[:header]
544
+ case headers
545
+ when ::Array
546
+ # significant
547
+ kc_v[:header] = headers.inject([]) do |a, k|
548
+ a << normalize_header_key(k)
549
+ end
550
+ when ::Hash
551
+ # transform, variables
552
+ kc_v[:header] = headers.inject(::Mash.new) do |h, (k, v)|
553
+ h[normalize_header_key(k)] = v
554
+ h
555
+ end
556
+ else
557
+ message = "Expected an array at #{position_string(position, subpath + [rst_k, kc_k, :header])}; " +
558
+ "use the #{STOP_TRAVERSAL_KEY.inspect} key to stop traversal."
559
+ raise ConfigError, message
560
+ end
561
+ end
562
+ end
563
+ kc[kc_k] = kc_v
564
+ kc
565
+ end
566
+ rst
567
+ end
568
+ end
569
+
570
+ end # Config
571
+ end # RightDevelop::Testing::Recording