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

Sign up to get free protection for your applications and to get access to all the features.
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