tempo-ruby 0.1.0

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.
data/lib/tempo/base.rb ADDED
@@ -0,0 +1,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'active_support/inflector'
5
+
6
+ module Tempo
7
+ class Base
8
+ # A reference to the Tempo::Client used to initialize this resource.
9
+ attr_reader :client
10
+
11
+ # Returns true if this instance has been fetched from the server
12
+ attr_accessor :expanded
13
+
14
+ # Returns true if this instance has been deleted from the server
15
+ attr_accessor :deleted
16
+
17
+ # The hash of attributes belonging to this instance. An exact
18
+ # representation of the JSON returned from the Tempo API
19
+ attr_accessor :attrs
20
+
21
+ alias expanded? expanded
22
+ alias deleted? deleted
23
+
24
+ def initialize(client, options = {})
25
+ @client = client
26
+ @attrs = options[:attrs] || {}
27
+ @expanded = options[:expanded] || false
28
+ @deleted = false
29
+
30
+ # If this class has any belongs_to relationships, a value for
31
+ # each of them must be passed in to the initializer.
32
+ self.class.belongs_to_relationships.each do |relation|
33
+ if options[relation]
34
+ instance_variable_set("@#{relation}", options[relation])
35
+ instance_variable_set("@#{relation}_id", options[relation].key_value)
36
+ elsif options["#{relation}_id".to_sym]
37
+ instance_variable_set("@#{relation}_id", options["#{relation}_id".to_sym])
38
+ else
39
+ raise ArgumentError, "Required option #{relation.inspect} missing" unless options[relation]
40
+ end
41
+ end
42
+ end
43
+
44
+ # The class methods are never called directly, they are always
45
+ # invoked from a BaseFactory subclass instance.
46
+ def self.all(client, options = {})
47
+ response = client.get(collection_path(client))
48
+ json = parse_json(response.body)['results']
49
+ json = json[endpoint_name.pluralize] if collection_attributes_are_nested
50
+ json.map do |attrs|
51
+ new(client, { attrs: attrs }.merge(options))
52
+ end
53
+ end
54
+
55
+ # Finds and retrieves a resource with the given ID.
56
+ def self.find(client, key, options = {})
57
+ instance = new(client, options)
58
+ instance.attrs[key_attribute.to_s] = key
59
+ instance.fetch(false, query_params_for_single_fetch(options))
60
+ instance
61
+ end
62
+
63
+ # Builds a new instance of the resource with the given attributes.
64
+ # These attributes will be posted to the Tempo Api if save is called.
65
+ def self.build(client, attrs)
66
+ new(client, attrs: attrs)
67
+ end
68
+
69
+ # Returns the name of this resource for use in URL components.
70
+ # E.g.
71
+ # Tempo::resource::Issue.endpoint_name
72
+ # # => issue
73
+ def self.endpoint_name
74
+ name.split('::').last.pluralize.downcase
75
+ end
76
+
77
+ # Returns the full path for a collection of this resource.
78
+ # E.g.
79
+ # Tempo::resource::Issue.collection_path
80
+ # # => /tempo/core/3/teams
81
+ def self.collection_path(client, prefix = '/')
82
+ client.options[:rest_base_path] + prefix + endpoint_name
83
+ end
84
+
85
+ # Returns the singular path for the resource with the given key.
86
+ # E.g.
87
+ # Tempo::resource::Issue.singular_path('123')
88
+ # # => /tempo/core/3/teams/123
89
+ #
90
+ # If a prefix parameter is provided it will be injected between the base
91
+ # path and the endpoint.
92
+ # E.g.
93
+ # Tempo::resource::Member.singular_path('456','/teams/123/')
94
+ # # => /tempo/core/3/teams/123/comment/456
95
+ def self.singular_path(client, key, prefix = '/')
96
+ "#{collection_path(client, prefix)}/#{key}"
97
+ end
98
+
99
+ def path_base(client, prefix = '/')
100
+ client.options[:rest_base_path] + prefix
101
+ end
102
+
103
+ # Returns the attribute name of the attribute used for find.
104
+ # Defaults to :id unless overridden.
105
+ def self.key_attribute
106
+ :id
107
+ end
108
+
109
+ def self.parse_json(string) # :nodoc:
110
+ JSON.parse(string) # TODO: .deep_symbolize_keys
111
+ end
112
+
113
+ # Declares that this class contains a singular instance of another resource
114
+ # within the JSON returned from the Tempo API.
115
+ #
116
+ # class Example < Tempo::Base
117
+ # has_one :child
118
+ # end
119
+ #
120
+ # example = client.Example.find(1)
121
+ # example.child # Returns a Tempo::resource::Child
122
+ #
123
+ # The following options can be used to override the default behaviour of the
124
+ # relationship:
125
+ #
126
+ # [:attribute_key] The relationship will by default reference a JSON key on the
127
+ # object with the same name as the relationship.
128
+ #
129
+ # has_one :child # => {"id":"123",{"child":{"id":"456"}}}
130
+ #
131
+ # Use this option if the key in the JSON is named differently.
132
+ #
133
+ # # Respond to resource.child, but return the value of resource.attrs['kid']
134
+ # has_one :child, :attribute_key => 'kid' # => {"id":"123",{"kid":{"id":"456"}}}
135
+ #
136
+ # [:class] The class of the child instance will be inferred from the name of the
137
+ # relationship. E.g. <tt>has_one :child</tt> will return a <tt>Tempo::resource::Child</tt>.
138
+ # Use this option to override the inferred class.
139
+ #
140
+ # has_one :child, :class => Tempo::resource::Kid
141
+ # [:nested_under] In some cases, the JSON return from Tempo is nested deeply for particular
142
+ # relationships. This option allows the nesting to be specified.
143
+ #
144
+ # # Specify a single depth of nesting.
145
+ # has_one :child, :nested_under => 'foo'
146
+ # # => Looks for {"foo":{"child":{}}}
147
+ # # Specify deeply nested JSON
148
+ # has_one :child, :nested_under => ['foo', 'bar', 'baz']
149
+ # # => Looks for {"foo":{"bar":{"baz":{"child":{}}}}}
150
+ def self.has_one(resource, options = {})
151
+ attribute_key = options[:attribute_key] || resource.to_s
152
+ child_class = options[:class] || "Tempo::resource::#{resource.to_s.classify}".constantize
153
+ define_method(resource) do
154
+ attribute = maybe_nested_attribute(attribute_key, options[:nested_under])
155
+ return nil unless attribute
156
+ child_class.new(client, attrs: attribute)
157
+ end
158
+ end
159
+
160
+ # Declares that this class contains a collection of another resource
161
+ # within the JSON returned from the Tempo API.
162
+ #
163
+ # class Example < Tempo::Base
164
+ # has_many :children
165
+ # end
166
+ #
167
+ # example = client.Example.find(1)
168
+ # example.children # Returns an instance of Jira::resource::HasManyProxy,
169
+ # # which behaves exactly like an array of
170
+ # # Tempo::resource::Child
171
+ #
172
+ # The following options can be used to override the default behaviour of the
173
+ # relationship:
174
+ #
175
+ # [:attribute_key] The relationship will by default reference a JSON key on the
176
+ # object with the same name as the relationship.
177
+ #
178
+ # has_many :children # => {"id":"123",{"children":[{"id":"456"},{"id":"789"}]}}
179
+ #
180
+ # Use this option if the key in the JSON is named differently.
181
+ #
182
+ # # Respond to resource.children, but return the value of resource.attrs['kids']
183
+ # has_many :children, :attribute_key => 'kids' # => {"id":"123",{"kids":[{"id":"456"},{"id":"789"}]}}
184
+ #
185
+ # [:class] The class of the child instance will be inferred from the name of the
186
+ # relationship. E.g. <tt>has_many :children</tt> will return an instance
187
+ # of <tt>JIRA::resource::HasManyProxy</tt> containing the collection of
188
+ # <tt>JIRA::resource::Child</tt>.
189
+ # Use this option to override the inferred class.
190
+ #
191
+ # has_many :children, :class => Tempo::resource::Kid
192
+ # [:nested_under] In some cases, the JSON return from JIRA is nested deeply for particular
193
+ # relationships. This option allows the nesting to be specified.
194
+ #
195
+ # # Specify a single depth of nesting.
196
+ # has_many :children, :nested_under => 'foo'
197
+ # # => Looks for {"foo":{"children":{}}}
198
+ # # Specify deeply nested JSON
199
+ # has_many :children, :nested_under => ['foo', 'bar', 'baz']
200
+ # # => Looks for {"foo":{"bar":{"baz":{"children":{}}}}}
201
+ def self.has_many(collection, options = {})
202
+ attribute_key = options[:attribute_key] || collection.to_s
203
+ child_class = options[:class] || "Tempo::resource::#{collection.to_s.classify}".constantize
204
+ self_class_basename = name.split('::').last.downcase.to_sym
205
+ define_method(collection) do
206
+ child_class_options = { self_class_basename => self }
207
+ attribute = maybe_nested_attribute(attribute_key, options[:nested_under]) || []
208
+ collection = attribute.map do |child_attributes|
209
+ child_class.new(client, child_class_options.merge(attrs: child_attributes))
210
+ end
211
+ HasManyProxy.new(self, child_class, collection)
212
+ end
213
+ end
214
+
215
+ def self.belongs_to_relationships
216
+ @belongs_to_relationships ||= []
217
+ end
218
+
219
+ def self.belongs_to(resource)
220
+ belongs_to_relationships.push(resource)
221
+ attr_reader resource
222
+ attr_reader "#{resource}_id"
223
+ end
224
+
225
+ def self.collection_attributes_are_nested
226
+ @collection_attributes_are_nested ||= false
227
+ end
228
+
229
+ def self.nested_collections(value)
230
+ @collection_attributes_are_nested = value
231
+ end
232
+
233
+ def id
234
+ attrs['id']
235
+ end
236
+
237
+ # Returns a symbol for the given instance, for example
238
+ # Tempo::resource::Team returns :team
239
+ def to_sym
240
+ self.class.endpoint_name.to_sym
241
+ end
242
+
243
+ # Checks if method_name is set in the attributes hash
244
+ # and returns true when found, otherwise proxies the
245
+ # call to the superclass.
246
+ def respond_to?(method_name, _include_all = false)
247
+ if attrs.key?(method_name.to_s)
248
+ true
249
+ else
250
+ super(method_name)
251
+ end
252
+ end
253
+
254
+ # Overrides method_missing to check the attribute hash
255
+ # for resources matching method_name and proxies the call
256
+ # to the superclass if no match is found.
257
+ def method_missing(method_name, *_args)
258
+ if attrs.key?(method_name.to_s)
259
+ attrs[method_name.to_s]
260
+ else
261
+ super(method_name)
262
+ end
263
+ end
264
+
265
+ # Each resource has a unique key attribute, this method returns the value
266
+ # of that key for this instance.
267
+ def key_value
268
+ @attrs[self.class.key_attribute.to_s]
269
+ end
270
+
271
+ def collection_path(prefix = '/')
272
+ # Just proxy this to the class method
273
+ self.class.collection_path(client, prefix)
274
+ end
275
+
276
+ # This returns the URL path component that is specific to this instance,
277
+ # for example for Issue id 123 it returns '/teams/123'. For an unsaved
278
+ # issue it returns '/teams'
279
+ def path_component
280
+ path_component = "/#{self.class.endpoint_name}"
281
+ path_component += "/#{key_value}" if key_value
282
+ path_component
283
+ end
284
+
285
+ # Fetches the attributes for the specified resource from Tempo unless
286
+ # the resource is already expanded and the optional force reload flag
287
+ # is not set
288
+ def fetch(reload = false, query_params = {})
289
+ return if expanded? && !reload
290
+ response = client.get(url_with_query_params(url, query_params))
291
+ set_attrs_from_response(response)
292
+ @expanded = true
293
+ end
294
+
295
+ # Saves the specified resource attributes by sending either a POST or PUT
296
+ # request to Tempo, depending on resource.new_record?
297
+ #
298
+ # Accepts an attributes hash of the values to be saved. Will throw a
299
+ # Tempo::HTTPError if the request fails (response is not HTTP 2xx).
300
+ def save!(attrs, path = nil)
301
+ path ||= new_record? ? url : patched_url
302
+ http_method = new_record? ? :post : :put
303
+ response = client.send(http_method, path, attrs.to_json)
304
+ set_attrs(attrs, false)
305
+ set_attrs_from_response(response)
306
+ @expanded = false
307
+ true
308
+ end
309
+
310
+ # Saves the specified resource attributes by sending either a POST or PUT
311
+ # request to Tempo, depending on resource.new_record?
312
+ #
313
+ # Accepts an attributes hash of the values to be saved. Will return false
314
+ # if the request fails.
315
+ #
316
+ # rubocop:disable Lint/UselessAssignment
317
+ def save(attrs, path = url)
318
+ begin
319
+ save_status = save!(attrs, path)
320
+ rescue Tempo::HTTPError => exception
321
+ begin
322
+ set_attrs_from_response(exception.response) # Merge error status generated by Tempo REST API
323
+ rescue JSON::ParserError => parse_exception
324
+ set_attrs('exception' => {
325
+ 'class' => exception.response.class.name,
326
+ 'code' => exception.response.code,
327
+ 'message' => exception.response.message
328
+ })
329
+ end
330
+ # raise exception
331
+ save_status = false
332
+ end
333
+ save_status
334
+ end
335
+ # rubocop:enable Lint/UselessAssignment
336
+
337
+ # Sets the attributes hash from a HTTPResponse object from JIRA if it is
338
+ # not nil or is not a json response.
339
+ def set_attrs_from_response(response)
340
+ unless response.body.nil? || (response.body.length < 2)
341
+ json = self.class.parse_json(response.body)
342
+ set_attrs(json)
343
+ end
344
+ end
345
+
346
+ # Set the current attributes from a hash. If clobber is true, any existing
347
+ # hash values will be clobbered by the new hash, otherwise the hash will
348
+ # be deeply merged into attrs. The target paramater is for internal use only
349
+ # and should not be used.
350
+ def set_attrs(hash, clobber = true, target = nil)
351
+ target ||= @attrs
352
+ if clobber
353
+ target.merge!(hash)
354
+ hash
355
+ else
356
+ hash.each do |k, v|
357
+ if v.is_a?(Hash)
358
+ set_attrs(v, clobber, target[k])
359
+ else
360
+ target[k] = v
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ # Sends a delete request to the Tempo Api and sets the deleted instance
367
+ # variable on the object to true.
368
+ def delete
369
+ client.delete(url)
370
+ @deleted = true
371
+ end
372
+
373
+ def has_errors?
374
+ respond_to?(:errors)
375
+ end
376
+
377
+ def url
378
+ prefix = '/'
379
+ unless self.class.belongs_to_relationships.empty?
380
+ prefix = self.class.belongs_to_relationships.inject(prefix) do |prefix_so_far, relationship|
381
+ "#{prefix_so_far}#{relationship}/#{send("#{relationship}_id")}/"
382
+ end
383
+ end
384
+ if @attrs['self']
385
+ the_url = @attrs['self']
386
+ the_url = the_url.sub(@client.options[:site].chomp('/'), '') if @client.options[:site]
387
+ the_url
388
+ elsif key_value
389
+ self.class.singular_path(client, key_value.to_s, prefix)
390
+ else
391
+ self.class.collection_path(client, prefix)
392
+ end
393
+ end
394
+
395
+ # This method fixes issue that there is no / prefix in url. It is happened when we call for instance
396
+ # Looks like this issue is actual only in case if you use atlassian sdk your app path is not root (like /tempo in example below)
397
+ # team.save() for existing resource.
398
+ # As a result we got error 400 from Tempo API:
399
+ # [07/Jun/2015:15:32:19 +0400] "PUT tempo/core/3/teams/10111 HTTP/1.1" 400 -
400
+ # After applying this fix we have normal response:
401
+ # [07/Jun/2015:15:17:18 +0400] "PUT /tempo/core/3/teams/10111 HTTP/1.1" 204 -
402
+ def patched_url
403
+ result = url
404
+ return result if result.start_with?('/', 'http')
405
+ "/#{result}"
406
+ end
407
+
408
+ def to_s
409
+ "#<#{self.class.name}:#{object_id} @attrs=#{@attrs.inspect}>"
410
+ end
411
+
412
+ # Returns a JSON representation of the current attributes hash.
413
+ def to_json(options = {})
414
+ attrs.to_json(options)
415
+ end
416
+
417
+ # Determines if the resource is newly created by checking whether its
418
+ # key_value is set. If it is nil, the record is new and the method
419
+ # will return true.
420
+ def new_record?
421
+ key_value.nil?
422
+ end
423
+
424
+ protected
425
+
426
+ # This allows conditional lookup of possibly nested attributes. Example usage:
427
+ #
428
+ # maybe_nested_attribute('foo') # => @attrs['foo']
429
+ # maybe_nested_attribute('foo', 'bar') # => @attrs['bar']['foo']
430
+ # maybe_nested_attribute('foo', ['bar', 'baz']) # => @attrs['bar']['baz']['foo']
431
+ #
432
+ def maybe_nested_attribute(attribute_name, nested_under = nil)
433
+ self.class.maybe_nested_attribute(@attrs, attribute_name, nested_under)
434
+ end
435
+
436
+ def self.maybe_nested_attribute(attributes, attribute_name, nested_under = nil)
437
+ return attributes[attribute_name] if nested_under.nil?
438
+ if nested_under.instance_of? Array
439
+ final = nested_under.inject(attributes) do |parent, key|
440
+ break if parent.nil?
441
+ parent[key]
442
+ end
443
+ return nil if final.nil?
444
+ final[attribute_name]
445
+ else
446
+ attributes[nested_under][attribute_name]
447
+ end
448
+ end
449
+
450
+ def url_with_query_params(url, query_params)
451
+ self.class.url_with_query_params(url, query_params)
452
+ end
453
+
454
+ def self.url_with_query_params(url, query_params)
455
+ if query_params.empty?
456
+ url
457
+ else
458
+ "#{url}?#{hash_to_query_string query_params}"
459
+ end
460
+ end
461
+
462
+ def hash_to_query_string(query_params)
463
+ self.class.hash_to_query_string(query_params)
464
+ end
465
+
466
+ def self.hash_to_query_string(query_params)
467
+ query_params.map do |k, v|
468
+ "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
469
+ end.join('&')
470
+ end
471
+
472
+ # TODO: Remove
473
+ def self.query_params_for_single_fetch(options)
474
+ options.select do |k, _v|
475
+ [].include? k
476
+ end.to_h
477
+ end
478
+
479
+ # TODO: Remove
480
+ def self.query_params_for_search(options)
481
+ options.select do |k, _v|
482
+ [].include? k
483
+ end.to_h
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tempo
4
+ # This is the base class for all the Tempo resource factory instances.
5
+ class BaseFactory
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # Return the name of the class which this factory generates, i.e.
13
+ # Tempo::resource::FooFactory creates Tempo::resource::Foo instances.
14
+ def target_class
15
+ # Need to do a little bit of work here as Module.const_get doesn't work
16
+ # with nested class names, i.e. Tempo::resource::Foo.
17
+ #
18
+ # So create a method chain from the class components. This code will
19
+ # unroll to:
20
+ # Module.const_get('Tempo').const_get('resource').const_get('Foo')
21
+ #
22
+ target_class_name = self.class.name.sub(/Factory$/, '')
23
+ class_components = target_class_name.split('::')
24
+
25
+ class_components.inject(Module) do |mod, const_name|
26
+ mod.const_get(const_name)
27
+ end
28
+ end
29
+
30
+ def self.delegate_to_target_class(*method_names)
31
+ method_names.each do |method_name|
32
+ define_method method_name do |*args|
33
+ target_class.send(method_name, @client, *args)
34
+ end
35
+ end
36
+ end
37
+
38
+ # The principle purpose of this class is to delegate methods to the corresponding
39
+ # non-factory class and automatically prepend the client argument to the argument
40
+ # list.
41
+ delegate_to_target_class :all, :find, :collection_path, :singular_path
42
+
43
+ # This method needs special handling as it has a default argument value
44
+ def build(attrs = {})
45
+ target_class.build(@client, attrs)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'forwardable'
5
+ require 'ostruct'
6
+
7
+ module Tempo
8
+ # This class is the main access point for all Tempo::resource instances.
9
+ #
10
+ # The client must be initialized with an options hash containing
11
+ # configuration options. The available options are:
12
+ #
13
+ # :site => 'http://localhost:2990',
14
+ # :api_key => 'api_key_from_tempo'
15
+ # :context_path => '/',
16
+ # :rest_base_path => "/core/3",
17
+ # :default_headers => {},
18
+ # :read_timeout => nil,
19
+ # :http_debug => false,
20
+ #
21
+ # See the Tempo::Base class methods for all of the available methods on these accessor
22
+ # objects.
23
+
24
+ class Client
25
+ extend Forwardable
26
+
27
+ # The configuration options for this client instance
28
+ attr_reader :options
29
+
30
+ # TODO: MAke sure it's needed
31
+ def_delegators :@request_client, :init_access_token, :set_access_token, :set_request_token, :request_token, :access_token, :authenticated?
32
+
33
+ DEFINED_OPTIONS = %i[
34
+ site
35
+ api_key
36
+ auth_type
37
+ context_path
38
+ rest_base_path
39
+ default_headers
40
+ read_timeout
41
+ http_debug
42
+ issuer
43
+ base_url
44
+ ].freeze
45
+
46
+ DEFAULT_OPTIONS = {
47
+ site: 'http://api.tempo.io',
48
+ context_path: '/',
49
+ rest_base_path: 'core/3',
50
+ auth_type: :api_key,
51
+ api_key: '',
52
+ http_debug: false,
53
+ default_headers: {}
54
+ }.freeze
55
+
56
+ def initialize(options = {})
57
+ options = DEFAULT_OPTIONS.merge(options)
58
+ @options = options
59
+ @options[:rest_base_path] = @options[:context_path] + @options[:rest_base_path]
60
+
61
+ unknown_options = options.keys.reject { |o| DEFINED_OPTIONS.include?(o) }
62
+ raise ArgumentError, "Unknown option(s) given: #{unknown_options}" unless unknown_options.empty?
63
+
64
+ case options[:auth_type]
65
+ when :api_key
66
+ @request_client = HttpClient.new(@options)
67
+ else
68
+ raise ArgumentError, 'Options: ":auth_type" must be ":oauth",":oauth_2legged", ":cookie" or ":basic"'
69
+ end
70
+
71
+ @http_debug = @options[:http_debug]
72
+ @options.freeze
73
+ # @cache = OpenStruct.new
74
+ end
75
+
76
+ def Team # :nodoc:
77
+ Tempo::Resource::TeamFactory.new(self)
78
+ end
79
+
80
+ def TeamMember # :nodoc:
81
+ Tempo::Resource::TeamMemberFactory.new(self)
82
+ end
83
+
84
+ # HTTP methods without a body
85
+ def delete(path, headers = {})
86
+ request(:delete, path, nil, merge_default_headers(headers))
87
+ end
88
+
89
+ def get(path, headers = {})
90
+ request(:get, path, nil, merge_default_headers(headers))
91
+ end
92
+
93
+ def head(path, headers = {})
94
+ request(:head, path, nil, merge_default_headers(headers))
95
+ end
96
+
97
+ # HTTP methods with a body
98
+ def post(path, body = '', headers = {})
99
+ headers = { 'Content-Type' => 'application/json' }.merge(headers)
100
+ request(:post, path, body, merge_default_headers(headers))
101
+ end
102
+
103
+ def post_multipart(path, file, headers = {})
104
+ puts "post multipart: #{path} - [#{file}]" if @http_debug
105
+ @request_client.request_multipart(path, file, headers)
106
+ end
107
+
108
+ def put(path, body = '', headers = {})
109
+ headers = { 'Content-Type' => 'application/json' }.merge(headers)
110
+ request(:put, path, body, merge_default_headers(headers))
111
+ end
112
+
113
+ # Sends the specified HTTP request to the REST API through the
114
+ # appropriate method (oauth, basic).
115
+ def request(http_method, path, body = '', headers = {})
116
+ puts "#{http_method}: #{path} - [#{body}]" if @http_debug
117
+ @request_client.request(http_method, path, body, headers)
118
+ end
119
+
120
+ # Stops sensitive client information from being displayed in logs
121
+ def inspect
122
+ "#<Tempo::Client:#{object_id}>"
123
+ end
124
+
125
+ protected
126
+
127
+ def merge_default_headers(headers)
128
+ { 'Accept' => 'application/json' }.merge(@options[:default_headers]).merge(headers)
129
+ end
130
+ end
131
+ end