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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +104 -0
- data/.fasterer.yml +2 -0
- data/.gitignore +13 -0
- data/.overcommit.yml +32 -0
- data/.reek.yml +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +426 -0
- data/.ruby-gemset +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +137 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/tempo/base.rb +486 -0
- data/lib/tempo/base_factory.rb +48 -0
- data/lib/tempo/client.rb +131 -0
- data/lib/tempo/has_many_proxy.rb +46 -0
- data/lib/tempo/http_client.rb +61 -0
- data/lib/tempo/http_error.rb +17 -0
- data/lib/tempo/request_client.rb +22 -0
- data/lib/tempo/resource/team.rb +22 -0
- data/lib/tempo/resource/team_member.rb +9 -0
- data/lib/tempo/version.rb +5 -0
- data/lib/tempo_ruby.rb +16 -0
- data/tempo-ruby.gemspec +49 -0
- metadata +299 -0
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
|
data/lib/tempo/client.rb
ADDED
|
@@ -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
|