flapjack-diner 2.0.0b1 → 2.0.0.pre.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/.rubocop_todo.yml +135 -0
  4. data/.travis.yml +10 -5
  5. data/README.md +125 -143
  6. data/flapjack-diner.gemspec +1 -1
  7. data/lib/flapjack-diner.rb +24 -54
  8. data/lib/flapjack-diner/argument_validator.rb +17 -0
  9. data/lib/flapjack-diner/resources/checks.rb +52 -0
  10. data/lib/flapjack-diner/resources/contacts.rb +54 -0
  11. data/lib/flapjack-diner/resources/events.rb +54 -0
  12. data/lib/flapjack-diner/resources/maintenance_periods.rb +76 -0
  13. data/lib/flapjack-diner/resources/media.rb +75 -0
  14. data/lib/flapjack-diner/resources/metrics.rb +23 -0
  15. data/lib/flapjack-diner/resources/relationships.rb +275 -0
  16. data/lib/flapjack-diner/resources/rules.rb +75 -0
  17. data/lib/flapjack-diner/resources/states.rb +24 -0
  18. data/lib/flapjack-diner/resources/statistics.rb +24 -0
  19. data/lib/flapjack-diner/resources/tags.rb +47 -0
  20. data/lib/flapjack-diner/tools.rb +456 -46
  21. data/lib/flapjack-diner/version.rb +1 -1
  22. data/spec/flapjack-diner_spec.rb +18 -9
  23. data/spec/resources/checks_spec.rb +7 -7
  24. data/spec/resources/contacts_spec.rb +12 -14
  25. data/spec/resources/events_spec.rb +13 -13
  26. data/spec/resources/maintenance_periods_spec.rb +3 -3
  27. data/spec/resources/media_spec.rb +3 -3
  28. data/spec/resources/metrics_spec.rb +1 -1
  29. data/spec/{relationships_spec.rb → resources/relationships_spec.rb} +25 -71
  30. data/spec/resources/rules_spec.rb +62 -62
  31. data/spec/resources/states_spec.rb +1 -1
  32. data/spec/resources/statistics_spec.rb +1 -1
  33. data/spec/resources/tags_spec.rb +19 -75
  34. data/spec/support/fixture_data.rb +43 -80
  35. metadata +22 -17
  36. data/lib/flapjack-diner/configuration.rb +0 -417
  37. data/lib/flapjack-diner/log_formatter.rb +0 -22
  38. data/lib/flapjack-diner/query.rb +0 -114
  39. data/lib/flapjack-diner/relationships.rb +0 -180
  40. data/lib/flapjack-diner/request.rb +0 -280
  41. data/lib/flapjack-diner/resources.rb +0 -64
  42. data/lib/flapjack-diner/response.rb +0 -91
  43. data/lib/flapjack-diner/utility.rb +0 -16
@@ -1,72 +1,482 @@
1
+ require 'uri'
2
+
1
3
  module Flapjack
2
4
  module Diner
3
5
  module Tools
4
- module ClassMethods
5
- def included_data
6
- return if context.nil?
7
- context[return_keys_as_strings ? 'included' : :included]
6
+ SUCCESS_STATUS_CODES = [200, 201, 204]
7
+
8
+ attr_accessor :last_error, :context
9
+
10
+ private
11
+
12
+ def log_request(method_type, req_uri, data = nil)
13
+ return if logger.nil? || req_uri.nil?
14
+ log_msg = "#{method_type} #{req_uri}"
15
+ unless %w(GET DELETE).include?(method_type) || data.nil?
16
+ log_msg << "\n Body: #{data.inspect}"
8
17
  end
18
+ logger.info log_msg
19
+ end
20
+
21
+ def perform_get(path, ids = [], data = {}, opts = {})
22
+ @last_error = nil
23
+ @context = nil
9
24
 
10
- def related(record, rel, incl = included_data)
11
- return if incl.nil?
25
+ data = data.reduce({}, &:merge) if data.is_a?(Array)
26
+ if (ids.size > 1)
27
+ if data[:filter].nil?
28
+ data[:filter] = {:id => ids}
29
+ elsif !data[:filter].has_key?(:id)
30
+ data[:filter][:id] = ids
31
+ else
32
+ data[:filter][:id] |= ids
33
+ end
34
+ end
12
35
 
13
- type = record[return_keys_as_strings ? 'type' : :type]
14
- return if type.nil?
36
+ filt = data.delete(:filter)
37
+ unless filt.nil?
38
+ data[:filter] = filt.each_with_object([]) do |(k, v), memo|
39
+ value = case v
40
+ when Array, Set
41
+ v.to_a.join("|")
42
+ else
43
+ v.to_s
44
+ end
45
+ if opts[:assoc].nil?
46
+ memo << "#{k}:#{value}"
47
+ else
48
+ memo << "#{opts[:assoc]}.#{k}:#{value}"
49
+ end
50
+ end
51
+ end
15
52
 
16
- res = Flapjack::Diner::Configuration::RESOURCES.values.detect do |r|
17
- type.eql?(r[:resource])
53
+ incl = data[:include]
54
+ unless incl.nil?
55
+ case incl
56
+ when Array
57
+ raise ArgumentError.new("Include parameters must not contain commas") if incl.any? {|i| i =~ /,/}
58
+ data[:include] = if opts[:assoc].nil?
59
+ incl.join(",")
60
+ else
61
+ incl.map {|i|
62
+ if i.eql?(opts[:assoc].to_s) || (i =~ /^#{opts[:assoc]}\./)
63
+ i
64
+ else
65
+ "#{opts[:assoc]}.#{i}"
66
+ end
67
+ }.join(",")
68
+ end
69
+ when String
70
+ raise ArgumentError.new("Include parameters must not contain commas") if incl =~ /,/
71
+ unless opts[:assoc].nil? || (incl.eql?(opts[:assoc].to_s)) || (incl =~ /^#{opts[:assoc]}\./)
72
+ data[:include] = "#{opts[:assoc]}.#{incl}"
73
+ end
18
74
  end
19
- return if res.nil? || res[:relationships].nil?
75
+ end
20
76
 
21
- rel_cfg = res[:relationships][rel.to_sym]
22
- return if rel_cfg.nil?
77
+ req_uri = build_uri(path, ids, data)
78
+ log_request('GET', req_uri, data)
79
+ handle_response(get(req_uri.request_uri))
80
+ end
23
81
 
24
- rel_type = rel_cfg[:resource]
25
- has_data = incl.key?(rel_type)
26
- case rel_cfg[:number]
27
- when :singular
28
- has_data ? singularly_related(record, rel, rel_type, incl) : nil
29
- else
30
- has_data ? multiply_related(record, rel, rel_type, incl) : []
82
+ def record_data(source, type, method)
83
+ r_type = Flapjack::Diner::Resources::Relationships::TYPES[type]
84
+ req_data = {}
85
+ ['id', :id].each do |i|
86
+ req_data[:id] = source[i] if source.has_key?(i)
87
+ end
88
+ req_data[:type] = r_type
89
+
90
+ assocs = Flapjack::Diner::Resources::Relationships::ASSOCIATIONS[type] || {}
91
+
92
+ rel_singular = assocs.inject([]) do |memo, (assoc_name, assoc)|
93
+ if assoc[method].is_a?(TrueClass) && :singular.eql?(assoc[:number])
94
+ memo << assoc_name
95
+ end
96
+ memo
97
+ end
98
+
99
+ rel_multiple = assocs.inject([]) do |memo, (assoc_name, assoc)|
100
+ if assoc[method].is_a?(TrueClass) && :multiple.eql?(assoc[:number])
101
+ memo << assoc_name
102
+ end
103
+ memo
104
+ end
105
+
106
+ excluded = [:id, :type] + rel_singular + rel_multiple
107
+ attrs = source.reject do |k,v|
108
+ excluded.include?(k.to_sym)
109
+ end
110
+
111
+ req_data[:attributes] = attrs unless attrs.empty?
112
+
113
+ rel_data = {}
114
+
115
+ rel_singular.each do |singular_link|
116
+ converted = false
117
+ [singular_link, singular_link.to_s].each do |sl|
118
+ next if converted || !source.has_key?(sl)
119
+ rel_data[singular_link] = {:data => {:type => singular_link.to_s, :id => source[sl]}}
120
+ converted = true
121
+ end
122
+ end
123
+
124
+ rel_multiple.each do |multiple_link|
125
+ converted = false
126
+ ml_type = Flapjack::Diner::Resources::Relationships::TYPES[multiple_link]
127
+ [multiple_link, multiple_link.to_s].each do |ml|
128
+ next if converted || !source.has_key?(ml)
129
+ rel_data[multiple_link] = {
130
+ :data => source[ml].map {|id|
131
+ {:type => ml_type, :id => id}
132
+ }
133
+ }
134
+ converted = true
135
+ end
136
+ end
137
+
138
+ req_data[:relationships] = rel_data unless rel_data.empty?
139
+
140
+ req_data
141
+ end
142
+
143
+ def perform_post(type, path, data = {})
144
+ @last_error = nil
145
+ @context = nil
146
+
147
+ jsonapi_ext = ""
148
+ req_data = nil
149
+
150
+ case data
151
+ when Array
152
+ req_data = data.collect {|d| record_data(d, type, :post) }
153
+ jsonapi_ext = "; ext=bulk"
154
+ when Hash
155
+ req_data = record_data(data, type, :post)
156
+ end
157
+ req_uri = build_uri(path)
158
+ log_request('POST', req_uri, :data => req_data)
159
+
160
+ # TODO send current character encoding in content-type ?
161
+ opts = {:body => prepare_nested_query(:data => req_data).to_json,
162
+ :headers => {'Content-Type' => "application/vnd.api+json#{jsonapi_ext}"}}
163
+ handle_response(post(req_uri.request_uri, opts))
164
+ end
165
+
166
+ def perform_post_links(type, path, *ids)
167
+ @last_error = nil
168
+ @context = nil
169
+ data = ids.collect {|id| {:type => type, :id => id}}
170
+ req_uri = build_uri(path)
171
+ log_request('POST', req_uri, :data => data)
172
+ opts = {:body => prepare_nested_query(:data => data).to_json,
173
+ :headers => {'Content-Type' => 'application/vnd.api+json'}}
174
+ handle_response(post(req_uri.request_uri, opts))
175
+ end
176
+
177
+ def perform_patch(type, path, data = nil)
178
+ @last_error = nil
179
+ @context = nil
180
+
181
+ req_uri = nil
182
+ req_data = nil
183
+
184
+ jsonapi_ext = ""
185
+ case data
186
+ when Hash
187
+ raise "Update data does not contain :id" unless data[:id]
188
+ req_data = record_data(data, type, :patch)
189
+ ids = [data[:id]]
190
+ req_uri = build_uri(path, ids)
191
+ when Array
192
+ ids = []
193
+ req_data = []
194
+ data.each do |d|
195
+ d_id = d.has_key?(:id) ? d[:id] : nil
196
+ ids << d_id unless d_id.nil? || d_id.empty?
197
+ req_data << record_data(d, type, :patch)
31
198
  end
199
+ raise "Update data must each contain :id" unless ids.size == data.size
200
+ req_uri = build_uri(path)
201
+ jsonapi_ext = "; ext=bulk"
32
202
  end
33
203
 
34
- private
204
+ log_request('PATCH', req_uri, :data => req_data)
35
205
 
36
- def singularly_related(record, rel, type, incl)
37
- relat, data, id_a, rel = related_accessors(rel)
38
- return if record[relat].nil? ||
39
- record[relat][rel].nil? ||
40
- record[relat][rel][data].nil?
206
+ opts = if req_data.nil?
207
+ {}
208
+ else
209
+ {:body => prepare_nested_query(:data => req_data).to_json,
210
+ :headers => {'Content-Type' => "application/vnd.api+json#{jsonapi_ext}"}}
211
+ end
212
+ handle_response(patch(req_uri.request_uri, opts))
213
+ end
41
214
 
42
- id = record[relat][rel][data][id_a]
43
- return if id.nil? || !incl.key?(type)
44
- incl[type][id]
215
+ def perform_patch_links(type, path, single, *ids)
216
+ @last_error = nil
217
+ @context = nil
218
+ data = if single
219
+ raise "Must provide one ID for a singular link" unless ids.size == 1
220
+ [nil].eql?(ids) ? nil : {:type => type, :id => ids.first}
221
+ else
222
+ [[]].eql?(ids) ? [] : ids.collect {|id| {:type => type, :id => id}}
45
223
  end
46
224
 
47
- def multiply_related(record, rel, type, incl)
48
- relat, data, id_a, rel = related_accessors(rel)
49
- return [] if record[relat].nil? ||
50
- record[relat][rel].nil? ||
51
- record[relat][rel][data].nil? ||
52
- record[relat][rel][data].empty?
225
+ req_uri = build_uri(path)
226
+
227
+ opts = {:body => prepare_nested_query(:data => data).to_json,
228
+ :headers => {'Content-Type' => 'application/vnd.api+json'}}
229
+ log_request('PATCH', req_uri, opts)
230
+ handle_response(patch(req_uri.request_uri, opts))
231
+ end
232
+
233
+ def perform_delete(type, path, *ids)
234
+ @last_error = nil
235
+ @context = nil
236
+ type = Flapjack::Diner::Resources::Relationships::TYPES[type]
237
+
238
+ req_uri = build_uri(path, ids)
239
+ opts = if ids.size == 1
240
+ {}
241
+ else
242
+ data = ids.collect {|id| {:type => type, :id => id} }
243
+ {:body => prepare_nested_query(:data => data).to_json,
244
+ :headers => {'Content-Type' => 'application/vnd.api+json; ext=bulk'}}
245
+ end
246
+ log_request('DELETE', req_uri, opts)
247
+ handle_response(delete(req_uri.request_uri, opts))
248
+ end
53
249
 
54
- ids = record[relat][rel][data].map {|m| m[id_a] }
55
- return [] if ids.empty? || !incl.key?(type)
56
- incl[type].values_at(*ids)
250
+ def perform_delete_links(type, path, *ids)
251
+ @last_error = nil
252
+ @context = nil
253
+ req_uri = build_uri(path)
254
+ data = ids.collect {|id| {:type => type, :id => id}}
255
+ opts = {:body => prepare_nested_query(:data => data).to_json,
256
+ :headers => {'Content-Type' => 'application/vnd.api+json'}}
257
+ log_request('DELETE', req_uri, opts)
258
+ handle_response(delete(req_uri.request_uri, opts))
259
+ end
260
+
261
+ def log_response(response)
262
+ return if logger.nil? || !response.respond_to?(:code)
263
+ response_message = " Response Code: #{response.code}"
264
+ unless response.message.nil? || (response.message.eql?(''))
265
+ response_message << " #{response.message}"
57
266
  end
267
+ logger.info response_message
268
+ return if response.body.nil?
269
+ logger.info " Response Body: #{response.body[0..300]}"
270
+ end
58
271
 
59
- def related_accessors(*args)
60
- acc = [:relationships, :data, :id]
61
- return (acc + args).map(&:to_s) if return_keys_as_strings
62
- (acc + args.map(&:to_sym))
272
+ def handle_response(response)
273
+ log_response(response)
274
+ return true if 204.eql?(response.code)
275
+ parsed = if response.respond_to?(:parsed_response)
276
+ response.parsed_response
277
+ else
278
+ nil
279
+ end
280
+ strify = return_keys_as_strings.is_a?(TrueClass)
281
+ if [200, 201].include?(response.code)
282
+ return handle_response_data(parsed, strify)
63
283
  end
284
+ @last_error = handle_response_errors(parsed, strify)
285
+ nil
64
286
  end
65
287
 
66
- def self.included(base)
67
- base.extend ClassMethods
68
- # base.class_eval do
69
- # end
288
+ def flatten_jsonapi_data(data, opts = {})
289
+ ret = nil
290
+ case data
291
+ when Array
292
+ ret = data.inject([]) do |memo, d|
293
+ attrs = d['attributes'] || {}
294
+ d.each_pair do |k, v|
295
+ next if 'attributes'.eql?(k)
296
+ attrs.update(k => v)
297
+ end
298
+ memo += [attrs]
299
+ memo
300
+ end
301
+ when Hash
302
+ ret = data['attributes'] || {}
303
+ data.each_pair do |k, v|
304
+ next if 'attributes'.eql?(k)
305
+ ret.update(k => v)
306
+ end
307
+ else
308
+ ret = data
309
+ end
310
+ ret
311
+ end
312
+
313
+ def handle_response_errors(parsed, strify)
314
+ return parsed if parsed.nil? || !parsed.is_a?(Hash) ||
315
+ !parsed.has_key?('errors')
316
+ errs = parsed['errors']
317
+ strify ? errs : symbolize(errs)
318
+ end
319
+
320
+ def handle_response_data(parsed, strify)
321
+ return parsed if parsed.nil? || !parsed.is_a?(Hash) ||
322
+ !parsed.has_key?('data')
323
+ @context = {}
324
+ if parsed.has_key?('included')
325
+ incl = flatten_jsonapi_data(parsed['included'], :allow_relationships => true)
326
+ @context[:included] = (strify ? incl : symbolize(incl))
327
+ end
328
+ (['relationships', 'meta'] & parsed.keys).each do |k|
329
+ c = parsed[k]
330
+ @context[k.to_sym] = (strify ? c : symbolize(c))
331
+ end
332
+ ret = flatten_jsonapi_data(parsed['data'], :allow_relationships => false)
333
+ strify ? ret : symbolize(ret)
334
+ end
335
+
336
+ def validate_params(query = {}, &validation)
337
+ return unless block_given?
338
+ query = {} if [].eql?(query)
339
+ case query
340
+ when Array
341
+ query.each do |q|
342
+ ArgumentValidator.new(q).instance_eval(&validation)
343
+ end
344
+ else
345
+ ArgumentValidator.new(query).instance_eval(&validation)
346
+ end
347
+ end
348
+
349
+ # copied from Rack::Utils -- builds the query string for GETs
350
+ def build_nested_query(value, prefix = nil)
351
+ case value
352
+ when Array
353
+ build_array_query(value, prefix)
354
+ when Hash
355
+ build_hash_query(value, prefix)
356
+ else
357
+ build_data_query(value, prefix)
358
+ end
359
+ end
360
+
361
+ def build_array_query(value, prefix)
362
+ value.map {|v| build_nested_query(v, "#{prefix}[]") }.join('&')
363
+ end
364
+
365
+ def build_hash_query(value, prefix)
366
+ value.map do |k, v|
367
+ data = prefix ? "#{prefix}[#{k}]" : k
368
+ build_nested_query(v, data)
369
+ end.join('&')
370
+ end
371
+
372
+ def build_data_query(value, prefix)
373
+ if value.respond_to?(:iso8601)
374
+ raise(ArgumentError, 'Value must be a Hash') if prefix.nil?
375
+ "#{escape(prefix)}=#{escape(value.iso8601)}"
376
+ elsif value.is_a?(String) || value.is_a?(Integer)
377
+ raise(ArgumentError, 'Value must be a Hash') if prefix.nil?
378
+ "#{escape(prefix)}=#{escape(value.to_s)}"
379
+ else
380
+ prefix
381
+ end
382
+ end
383
+
384
+ def escape(s)
385
+ URI.encode_www_form_component(s)
386
+ end
387
+
388
+ def unwrap_ids(*args)
389
+ args.select {|a| a.is_a?(String) || a.is_a?(Integer) }
390
+ end
391
+
392
+ def unwrap_uuids(*args)
393
+ ids = args.select {|a| a.is_a?(String) || a.is_a?(Integer) }
394
+ raise "IDs must be RFC 4122-compliant UUIDs" unless ids.all? {|id|
395
+ id =~ /^#{Flapjack::Diner::UUID_RE}$/i
396
+ }
397
+ ids
398
+ end
399
+
400
+ def unwrap_data(*args)
401
+ data = args.reject {|a| a.is_a?(String) || a.is_a?(Integer) }
402
+ raise "Data must be passed as a Hash, or multiple Hashes" unless data.all? {|a| a.is_a?(Hash) }
403
+ return symbolize(data.first) if data.size == 1
404
+ data.each_with_object([]) {|d, o| o << symbolize(d) }
405
+ end
406
+
407
+ # used for the JSON data hashes in POST, PUT, DELETE
408
+ def prepare_nested_query(value)
409
+ case value
410
+ when Array
411
+ prepare_array_query(value)
412
+ when Hash
413
+ prepare_hash_query(value)
414
+ else
415
+ prepare_data_query(value)
416
+ end
417
+ end
418
+
419
+ def prepare_array_query(value)
420
+ value.map {|v| prepare_nested_query(v) }
421
+ end
422
+
423
+ def prepare_hash_query(value)
424
+ value.each_with_object({}) do |(k, v), a|
425
+ a[k] = prepare_nested_query(v)
426
+ end
427
+ end
428
+
429
+ def prepare_data_query(value)
430
+ if value.respond_to?(:iso8601)
431
+ value.iso8601
432
+ else
433
+ case value
434
+ when Integer, TrueClass, FalseClass, NilClass
435
+ value
436
+ else
437
+ value.to_s
438
+ end
439
+ end
440
+ end
441
+
442
+ def normalise_port(port_str, protocol)
443
+ if port_str.nil? || port_str.to_i < 1 || port_str.to_i > 65_535
444
+ 'https'.eql?(protocol) ? 443 : 80
445
+ else
446
+ port_str.to_i
447
+ end
448
+ end
449
+
450
+ def protocol_host_port
451
+ %r{^(?:(?<protocol>https?)://)
452
+ (?<host>[a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])
453
+ (?::(?<port>\d+))?
454
+ }ix =~ base_uri
455
+
456
+ protocol = protocol.nil? ? 'http' : protocol.downcase
457
+ [protocol, host, normalise_port(port, protocol)]
458
+ end
459
+
460
+ def build_uri(path, ids = [], params = [])
461
+ pr, ho, po = protocol_host_port
462
+ if ids.size == 1
463
+ path += "/#{URI.escape(ids.first.to_s)}"
464
+ end
465
+ params = params.reduce({}, &:merge) if params.is_a?(Array)
466
+ query = params.empty? ? nil : build_nested_query(params)
467
+ URI::HTTP.build(:protocol => pr, :host => ho, :port => po,
468
+ :path => path, :query => query)
469
+ end
470
+
471
+ def symbolize(obj)
472
+ case obj
473
+ when Hash
474
+ obj.each_with_object({}) {|(k, v), a| a[k.to_sym] = symbolize(v) }
475
+ when Array
476
+ obj.each_with_object([]) {|e, a| a << symbolize(e) }
477
+ else
478
+ obj
479
+ end
70
480
  end
71
481
  end
72
482
  end