jira-ruby 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +14 -0
  5. data/Guardfile +14 -0
  6. data/LICENSE +19 -0
  7. data/README.md +427 -0
  8. data/Rakefile +31 -0
  9. data/example.rb +224 -0
  10. data/http-basic-example.rb +113 -0
  11. data/jira-ruby.gemspec +35 -0
  12. data/lib/jira-ruby.rb +49 -0
  13. data/lib/jira/base.rb +525 -0
  14. data/lib/jira/base_factory.rb +46 -0
  15. data/lib/jira/client.rb +308 -0
  16. data/lib/jira/has_many_proxy.rb +42 -0
  17. data/lib/jira/http_client.rb +112 -0
  18. data/lib/jira/http_error.rb +14 -0
  19. data/lib/jira/jwt_client.rb +67 -0
  20. data/lib/jira/oauth_client.rb +114 -0
  21. data/lib/jira/railtie.rb +10 -0
  22. data/lib/jira/request_client.rb +31 -0
  23. data/lib/jira/resource/agile.rb +79 -0
  24. data/lib/jira/resource/applinks.rb +39 -0
  25. data/lib/jira/resource/attachment.rb +50 -0
  26. data/lib/jira/resource/board.rb +91 -0
  27. data/lib/jira/resource/board_configuration.rb +9 -0
  28. data/lib/jira/resource/comment.rb +12 -0
  29. data/lib/jira/resource/component.rb +8 -0
  30. data/lib/jira/resource/createmeta.rb +44 -0
  31. data/lib/jira/resource/field.rb +83 -0
  32. data/lib/jira/resource/filter.rb +15 -0
  33. data/lib/jira/resource/issue.rb +141 -0
  34. data/lib/jira/resource/issuelink.rb +20 -0
  35. data/lib/jira/resource/issuelinktype.rb +14 -0
  36. data/lib/jira/resource/issuetype.rb +8 -0
  37. data/lib/jira/resource/priority.rb +8 -0
  38. data/lib/jira/resource/project.rb +41 -0
  39. data/lib/jira/resource/rapidview.rb +67 -0
  40. data/lib/jira/resource/remotelink.rb +26 -0
  41. data/lib/jira/resource/resolution.rb +8 -0
  42. data/lib/jira/resource/serverinfo.rb +18 -0
  43. data/lib/jira/resource/sprint.rb +105 -0
  44. data/lib/jira/resource/sprint_report.rb +8 -0
  45. data/lib/jira/resource/status.rb +8 -0
  46. data/lib/jira/resource/transition.rb +29 -0
  47. data/lib/jira/resource/user.rb +30 -0
  48. data/lib/jira/resource/version.rb +8 -0
  49. data/lib/jira/resource/watcher.rb +35 -0
  50. data/lib/jira/resource/webhook.rb +37 -0
  51. data/lib/jira/resource/worklog.rb +14 -0
  52. data/lib/jira/tasks.rb +0 -0
  53. data/lib/jira/version.rb +3 -0
  54. data/lib/tasks/generate.rake +18 -0
  55. data/spec/integration/attachment_spec.rb +32 -0
  56. data/spec/integration/comment_spec.rb +52 -0
  57. data/spec/integration/component_spec.rb +39 -0
  58. data/spec/integration/field_spec.rb +32 -0
  59. data/spec/integration/issue_spec.rb +93 -0
  60. data/spec/integration/issuelinktype_spec.rb +26 -0
  61. data/spec/integration/issuetype_spec.rb +24 -0
  62. data/spec/integration/priority_spec.rb +24 -0
  63. data/spec/integration/project_spec.rb +49 -0
  64. data/spec/integration/rapidview_spec.rb +74 -0
  65. data/spec/integration/resolution_spec.rb +26 -0
  66. data/spec/integration/status_spec.rb +24 -0
  67. data/spec/integration/transition_spec.rb +49 -0
  68. data/spec/integration/user_spec.rb +41 -0
  69. data/spec/integration/version_spec.rb +39 -0
  70. data/spec/integration/watcher_spec.rb +62 -0
  71. data/spec/integration/webhook.rb +25 -0
  72. data/spec/integration/worklog_spec.rb +51 -0
  73. data/spec/jira/base_factory_spec.rb +45 -0
  74. data/spec/jira/base_spec.rb +598 -0
  75. data/spec/jira/client_spec.rb +291 -0
  76. data/spec/jira/has_many_proxy_spec.rb +46 -0
  77. data/spec/jira/http_client_spec.rb +328 -0
  78. data/spec/jira/http_error_spec.rb +24 -0
  79. data/spec/jira/jwt_uri_builder_spec.rb +59 -0
  80. data/spec/jira/oauth_client_spec.rb +162 -0
  81. data/spec/jira/request_client_spec.rb +41 -0
  82. data/spec/jira/resource/agile_spec.rb +135 -0
  83. data/spec/jira/resource/attachment_spec.rb +138 -0
  84. data/spec/jira/resource/board_spec.rb +224 -0
  85. data/spec/jira/resource/createmeta_spec.rb +258 -0
  86. data/spec/jira/resource/field_spec.rb +85 -0
  87. data/spec/jira/resource/filter_spec.rb +97 -0
  88. data/spec/jira/resource/issue_spec.rb +227 -0
  89. data/spec/jira/resource/issuelink_spec.rb +14 -0
  90. data/spec/jira/resource/project_factory_spec.rb +11 -0
  91. data/spec/jira/resource/project_spec.rb +123 -0
  92. data/spec/jira/resource/sprint_spec.rb +90 -0
  93. data/spec/jira/resource/user_factory_spec.rb +31 -0
  94. data/spec/jira/resource/worklog_spec.rb +22 -0
  95. data/spec/mock_responses/board/1.json +33 -0
  96. data/spec/mock_responses/board/1_issues.json +62 -0
  97. data/spec/mock_responses/component.post.json +28 -0
  98. data/spec/mock_responses/component/10000.invalid.put.json +5 -0
  99. data/spec/mock_responses/component/10000.json +39 -0
  100. data/spec/mock_responses/component/10000.put.json +39 -0
  101. data/spec/mock_responses/empty_issues.json +8 -0
  102. data/spec/mock_responses/field.json +32 -0
  103. data/spec/mock_responses/field/1.json +15 -0
  104. data/spec/mock_responses/issue.json +1108 -0
  105. data/spec/mock_responses/issue.post.json +5 -0
  106. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  107. data/spec/mock_responses/issue/10002.json +126 -0
  108. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  109. data/spec/mock_responses/issue/10002/attachments/10000.json +20 -0
  110. data/spec/mock_responses/issue/10002/comment.json +65 -0
  111. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  112. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  113. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  114. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  115. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  116. data/spec/mock_responses/issue/10002/watchers.json +13 -0
  117. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  118. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  119. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  120. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  121. data/spec/mock_responses/issueLinkType.json +25 -0
  122. data/spec/mock_responses/issueLinkType/10000.json +7 -0
  123. data/spec/mock_responses/issuetype.json +42 -0
  124. data/spec/mock_responses/issuetype/5.json +8 -0
  125. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook.json +11 -0
  126. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook/2.json +11 -0
  127. data/spec/mock_responses/priority.json +42 -0
  128. data/spec/mock_responses/priority/1.json +8 -0
  129. data/spec/mock_responses/project.json +12 -0
  130. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  131. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  132. data/spec/mock_responses/rapidview.json +10 -0
  133. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.full.json +276 -0
  134. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.json +111 -0
  135. data/spec/mock_responses/rapidview/SAMPLEPROJECT.json +6 -0
  136. data/spec/mock_responses/resolution.json +15 -0
  137. data/spec/mock_responses/resolution/1.json +7 -0
  138. data/spec/mock_responses/sprint/1_issues.json +125 -0
  139. data/spec/mock_responses/status.json +37 -0
  140. data/spec/mock_responses/status/1.json +7 -0
  141. data/spec/mock_responses/user_username=admin.json +17 -0
  142. data/spec/mock_responses/version.post.json +7 -0
  143. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  144. data/spec/mock_responses/version/10000.json +11 -0
  145. data/spec/mock_responses/version/10000.put.json +7 -0
  146. data/spec/mock_responses/webhook.json +11 -0
  147. data/spec/mock_responses/webhook/webhook.json +11 -0
  148. data/spec/spec_helper.rb +21 -0
  149. data/spec/support/clients_helper.rb +16 -0
  150. data/spec/support/matchers/have_attributes.rb +11 -0
  151. data/spec/support/matchers/have_many.rb +9 -0
  152. data/spec/support/matchers/have_one.rb +5 -0
  153. data/spec/support/shared_examples/integration.rb +177 -0
  154. metadata +491 -0
@@ -0,0 +1,525 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/inflector'
3
+ require 'set'
4
+
5
+ module JIRA
6
+ # This class provides the basic object <-> REST mapping for all JIRA::Resource subclasses,
7
+ # i.e. the Create, Retrieve, Update, Delete lifecycle methods.
8
+ #
9
+ # == Lifecycle methods
10
+ #
11
+ # Note that not all lifecycle
12
+ # methods are available for all resources, for example some resources cannot be updated
13
+ # or deleted.
14
+ #
15
+ # === Retrieving all resources
16
+ #
17
+ # client.Resource.all
18
+ #
19
+ # === Retrieving a single resource
20
+ #
21
+ # client.Resource.find(id)
22
+ #
23
+ # === Creating a resource
24
+ #
25
+ # resource = client.Resource.build({'name' => '')
26
+ # resource.save
27
+ #
28
+ # === Updating a resource
29
+ #
30
+ # resource = client.Resource.find(id)
31
+ # resource.save('updated_attribute' => 'new value')
32
+ #
33
+ # === Deleting a resource
34
+ #
35
+ # resource = client.Resource.find(id)
36
+ # resource.delete
37
+ #
38
+ # == Nested resources
39
+ #
40
+ # Some resources are not defined in the top level of the URL namespace
41
+ # within the JIRA API, but are always nested under the context of another
42
+ # resource. For example, a JIRA::Resource::Comment always belongs to a
43
+ # JIRA::Resource::Issue.
44
+ #
45
+ # These resources must be indexed and built from an instance of the class
46
+ # they are nested under:
47
+ #
48
+ # issue = client.Issue.find(id)
49
+ # comments = issue.comments
50
+ # new_comment = issue.comments.build
51
+ #
52
+ class Base
53
+ QUERY_PARAMS_FOR_SINGLE_FETCH = Set.new %i[expand fields]
54
+ QUERY_PARAMS_FOR_SEARCH = Set.new %i[expand fields startAt maxResults]
55
+
56
+ # A reference to the JIRA::Client used to initialize this resource.
57
+ attr_reader :client
58
+
59
+ # Returns true if this instance has been fetched from the server
60
+ attr_accessor :expanded
61
+
62
+ # Returns true if this instance has been deleted from the server
63
+ attr_accessor :deleted
64
+
65
+ # The hash of attributes belonging to this instance. An exact
66
+ # representation of the JSON returned from the JIRA API
67
+ attr_accessor :attrs
68
+
69
+ alias expanded? expanded
70
+ alias deleted? deleted
71
+
72
+ def initialize(client, options = {})
73
+ @client = client
74
+ @attrs = options[:attrs] || {}
75
+ @expanded = options[:expanded] || false
76
+ @deleted = false
77
+
78
+ # If this class has any belongs_to relationships, a value for
79
+ # each of them must be passed in to the initializer.
80
+ self.class.belongs_to_relationships.each do |relation|
81
+ if options[relation]
82
+ instance_variable_set("@#{relation}", options[relation])
83
+ instance_variable_set("@#{relation}_id", options[relation].key_value)
84
+ elsif options["#{relation}_id".to_sym]
85
+ instance_variable_set("@#{relation}_id", options["#{relation}_id".to_sym])
86
+ else
87
+ raise ArgumentError, "Required option #{relation.inspect} missing" unless options[relation]
88
+ end
89
+ end
90
+ end
91
+
92
+ # The class methods are never called directly, they are always
93
+ # invoked from a BaseFactory subclass instance.
94
+ def self.all(client, options = {})
95
+ response = client.get(collection_path(client))
96
+ json = parse_json(response.body)
97
+ json = json[endpoint_name.pluralize] if collection_attributes_are_nested
98
+ json.map do |attrs|
99
+ new(client, { attrs: attrs }.merge(options))
100
+ end
101
+ end
102
+
103
+ # Finds and retrieves a resource with the given ID.
104
+ def self.find(client, key, options = {})
105
+ instance = new(client, options)
106
+ instance.attrs[key_attribute.to_s] = key
107
+ instance.fetch(false, query_params_for_single_fetch(options))
108
+ instance
109
+ end
110
+
111
+ # Builds a new instance of the resource with the given attributes.
112
+ # These attributes will be posted to the JIRA Api if save is called.
113
+ def self.build(client, attrs)
114
+ new(client, attrs: attrs)
115
+ end
116
+
117
+ # Returns the name of this resource for use in URL components.
118
+ # E.g.
119
+ # JIRA::Resource::Issue.endpoint_name
120
+ # # => issue
121
+ def self.endpoint_name
122
+ name.split('::').last.downcase
123
+ end
124
+
125
+ # Returns the full path for a collection of this resource.
126
+ # E.g.
127
+ # JIRA::Resource::Issue.collection_path
128
+ # # => /jira/rest/api/2/issue
129
+ def self.collection_path(client, prefix = '/')
130
+ client.options[:rest_base_path] + prefix + endpoint_name
131
+ end
132
+
133
+ # Returns the singular path for the resource with the given key.
134
+ # E.g.
135
+ # JIRA::Resource::Issue.singular_path('123')
136
+ # # => /jira/rest/api/2/issue/123
137
+ #
138
+ # If a prefix parameter is provided it will be injected between the base
139
+ # path and the endpoint.
140
+ # E.g.
141
+ # JIRA::Resource::Comment.singular_path('456','/issue/123/')
142
+ # # => /jira/rest/api/2/issue/123/comment/456
143
+ def self.singular_path(client, key, prefix = '/')
144
+ collection_path(client, prefix) + '/' + key
145
+ end
146
+
147
+ # Returns the attribute name of the attribute used for find.
148
+ # Defaults to :id unless overridden.
149
+ def self.key_attribute
150
+ :id
151
+ end
152
+
153
+ def self.parse_json(string) # :nodoc:
154
+ JSON.parse(string)
155
+ end
156
+
157
+ # Declares that this class contains a singular instance of another resource
158
+ # within the JSON returned from the JIRA API.
159
+ #
160
+ # class Example < JIRA::Base
161
+ # has_one :child
162
+ # end
163
+ #
164
+ # example = client.Example.find(1)
165
+ # example.child # Returns a JIRA::Resource::Child
166
+ #
167
+ # The following options can be used to override the default behaviour of the
168
+ # relationship:
169
+ #
170
+ # [:attribute_key] The relationship will by default reference a JSON key on the
171
+ # object with the same name as the relationship.
172
+ #
173
+ # has_one :child # => {"id":"123",{"child":{"id":"456"}}}
174
+ #
175
+ # Use this option if the key in the JSON is named differently.
176
+ #
177
+ # # Respond to resource.child, but return the value of resource.attrs['kid']
178
+ # has_one :child, :attribute_key => 'kid' # => {"id":"123",{"kid":{"id":"456"}}}
179
+ #
180
+ # [:class] The class of the child instance will be inferred from the name of the
181
+ # relationship. E.g. <tt>has_one :child</tt> will return a <tt>JIRA::Resource::Child</tt>.
182
+ # Use this option to override the inferred class.
183
+ #
184
+ # has_one :child, :class => JIRA::Resource::Kid
185
+ # [:nested_under] In some cases, the JSON return from JIRA is nested deeply for particular
186
+ # relationships. This option allows the nesting to be specified.
187
+ #
188
+ # # Specify a single depth of nesting.
189
+ # has_one :child, :nested_under => 'foo'
190
+ # # => Looks for {"foo":{"child":{}}}
191
+ # # Specify deeply nested JSON
192
+ # has_one :child, :nested_under => ['foo', 'bar', 'baz']
193
+ # # => Looks for {"foo":{"bar":{"baz":{"child":{}}}}}
194
+ def self.has_one(resource, options = {})
195
+ attribute_key = options[:attribute_key] || resource.to_s
196
+ child_class = options[:class] || ('JIRA::Resource::' + resource.to_s.classify).constantize
197
+ define_method(resource) do
198
+ attribute = maybe_nested_attribute(attribute_key, options[:nested_under])
199
+ return nil unless attribute
200
+ child_class.new(client, attrs: attribute)
201
+ end
202
+ end
203
+
204
+ # Declares that this class contains a collection of another resource
205
+ # within the JSON returned from the JIRA API.
206
+ #
207
+ # class Example < JIRA::Base
208
+ # has_many :children
209
+ # end
210
+ #
211
+ # example = client.Example.find(1)
212
+ # example.children # Returns an instance of Jira::Resource::HasManyProxy,
213
+ # # which behaves exactly like an array of
214
+ # # Jira::Resource::Child
215
+ #
216
+ # The following options can be used to override the default behaviour of the
217
+ # relationship:
218
+ #
219
+ # [:attribute_key] The relationship will by default reference a JSON key on the
220
+ # object with the same name as the relationship.
221
+ #
222
+ # has_many :children # => {"id":"123",{"children":[{"id":"456"},{"id":"789"}]}}
223
+ #
224
+ # Use this option if the key in the JSON is named differently.
225
+ #
226
+ # # Respond to resource.children, but return the value of resource.attrs['kids']
227
+ # has_many :children, :attribute_key => 'kids' # => {"id":"123",{"kids":[{"id":"456"},{"id":"789"}]}}
228
+ #
229
+ # [:class] The class of the child instance will be inferred from the name of the
230
+ # relationship. E.g. <tt>has_many :children</tt> will return an instance
231
+ # of <tt>JIRA::Resource::HasManyProxy</tt> containing the collection of
232
+ # <tt>JIRA::Resource::Child</tt>.
233
+ # Use this option to override the inferred class.
234
+ #
235
+ # has_many :children, :class => JIRA::Resource::Kid
236
+ # [:nested_under] In some cases, the JSON return from JIRA is nested deeply for particular
237
+ # relationships. This option allows the nesting to be specified.
238
+ #
239
+ # # Specify a single depth of nesting.
240
+ # has_many :children, :nested_under => 'foo'
241
+ # # => Looks for {"foo":{"children":{}}}
242
+ # # Specify deeply nested JSON
243
+ # has_many :children, :nested_under => ['foo', 'bar', 'baz']
244
+ # # => Looks for {"foo":{"bar":{"baz":{"children":{}}}}}
245
+ def self.has_many(collection, options = {})
246
+ attribute_key = options[:attribute_key] || collection.to_s
247
+ child_class = options[:class] || ('JIRA::Resource::' + collection.to_s.classify).constantize
248
+ self_class_basename = name.split('::').last.downcase.to_sym
249
+ define_method(collection) do
250
+ child_class_options = { self_class_basename => self }
251
+ attribute = maybe_nested_attribute(attribute_key, options[:nested_under]) || []
252
+ collection = attribute.map do |child_attributes|
253
+ child_class.new(client, child_class_options.merge(attrs: child_attributes))
254
+ end
255
+ HasManyProxy.new(self, child_class, collection)
256
+ end
257
+ end
258
+
259
+ def self.belongs_to_relationships
260
+ @belongs_to_relationships ||= []
261
+ end
262
+
263
+ def self.belongs_to(resource)
264
+ belongs_to_relationships.push(resource)
265
+ attr_reader resource
266
+ attr_reader "#{resource}_id"
267
+ end
268
+
269
+ def self.collection_attributes_are_nested
270
+ @collection_attributes_are_nested ||= false
271
+ end
272
+
273
+ def self.nested_collections(value)
274
+ @collection_attributes_are_nested = value
275
+ end
276
+
277
+ def id
278
+ attrs['id']
279
+ end
280
+
281
+ # Returns a symbol for the given instance, for example
282
+ # JIRA::Resource::Issue returns :issue
283
+ def to_sym
284
+ self.class.endpoint_name.to_sym
285
+ end
286
+
287
+ # Checks if method_name is set in the attributes hash
288
+ # and returns true when found, otherwise proxies the
289
+ # call to the superclass.
290
+ def respond_to?(method_name, _include_all = false)
291
+ if attrs.key?(method_name.to_s)
292
+ true
293
+ else
294
+ super(method_name)
295
+ end
296
+ end
297
+
298
+ # Overrides method_missing to check the attribute hash
299
+ # for resources matching method_name and proxies the call
300
+ # to the superclass if no match is found.
301
+ def method_missing(method_name, *_args)
302
+ if attrs.key?(method_name.to_s)
303
+ attrs[method_name.to_s]
304
+ else
305
+ super(method_name)
306
+ end
307
+ end
308
+
309
+ # Each resource has a unique key attribute, this method returns the value
310
+ # of that key for this instance.
311
+ def key_value
312
+ @attrs[self.class.key_attribute.to_s]
313
+ end
314
+
315
+ def collection_path(prefix = '/')
316
+ # Just proxy this to the class method
317
+ self.class.collection_path(client, prefix)
318
+ end
319
+
320
+ # This returns the URL path component that is specific to this instance,
321
+ # for example for Issue id 123 it returns '/issue/123'. For an unsaved
322
+ # issue it returns '/issue'
323
+ def path_component
324
+ path_component = "/#{self.class.endpoint_name}"
325
+ path_component += '/' + key_value if key_value
326
+ path_component
327
+ end
328
+
329
+ # Fetches the attributes for the specified resource from JIRA unless
330
+ # the resource is already expanded and the optional force reload flag
331
+ # is not set
332
+ def fetch(reload = false, query_params = {})
333
+ return if expanded? && !reload
334
+ response = client.get(url_with_query_params(url, query_params))
335
+ set_attrs_from_response(response)
336
+ @expanded = true
337
+ end
338
+
339
+ # Saves the specified resource attributes by sending either a POST or PUT
340
+ # request to JIRA, depending on resource.new_record?
341
+ #
342
+ # Accepts an attributes hash of the values to be saved. Will throw a
343
+ # JIRA::HTTPError if the request fails (response is not HTTP 2xx).
344
+ def save!(attrs, path = nil)
345
+ path ||= new_record? ? url : patched_url
346
+ http_method = new_record? ? :post : :put
347
+ response = client.send(http_method, path, attrs.to_json)
348
+ set_attrs(attrs, false)
349
+ set_attrs_from_response(response)
350
+ @expanded = false
351
+ true
352
+ end
353
+
354
+ # Saves the specified resource attributes by sending either a POST or PUT
355
+ # request to JIRA, depending on resource.new_record?
356
+ #
357
+ # Accepts an attributes hash of the values to be saved. Will return false
358
+ # if the request fails.
359
+ def save(attrs, path = url)
360
+ begin
361
+ save_status = save!(attrs, path)
362
+ rescue JIRA::HTTPError => exception
363
+ begin
364
+ set_attrs_from_response(exception.response) # Merge error status generated by JIRA REST API
365
+ rescue JSON::ParserError => parse_exception
366
+ set_attrs('exception' => {
367
+ 'class' => exception.response.class.name,
368
+ 'code' => exception.response.code,
369
+ 'message' => exception.response.message
370
+ })
371
+ end
372
+ # raise exception
373
+ save_status = false
374
+ end
375
+ save_status
376
+ end
377
+
378
+ # Sets the attributes hash from a HTTPResponse object from JIRA if it is
379
+ # not nil or is not a json response.
380
+ def set_attrs_from_response(response)
381
+ unless response.body.nil? || (response.body.length < 2)
382
+ json = self.class.parse_json(response.body)
383
+ set_attrs(json)
384
+ end
385
+ end
386
+
387
+ # Set the current attributes from a hash. If clobber is true, any existing
388
+ # hash values will be clobbered by the new hash, otherwise the hash will
389
+ # be deeply merged into attrs. The target paramater is for internal use only
390
+ # and should not be used.
391
+ def set_attrs(hash, clobber = true, target = nil)
392
+ target ||= @attrs
393
+ if clobber
394
+ target.merge!(hash)
395
+ hash
396
+ else
397
+ hash.each do |k, v|
398
+ if v.is_a?(Hash)
399
+ set_attrs(v, clobber, target[k])
400
+ else
401
+ target[k] = v
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ # Sends a delete request to the JIRA Api and sets the deleted instance
408
+ # variable on the object to true.
409
+ def delete
410
+ client.delete(url)
411
+ @deleted = true
412
+ end
413
+
414
+ def has_errors?
415
+ respond_to?('errors')
416
+ end
417
+
418
+ def url
419
+ prefix = '/'
420
+ unless self.class.belongs_to_relationships.empty?
421
+ prefix = self.class.belongs_to_relationships.inject(prefix) do |prefix_so_far, relationship|
422
+ prefix_so_far.to_s + relationship.to_s + '/' + send("#{relationship}_id").to_s + '/'
423
+ end
424
+ end
425
+ if @attrs['self']
426
+ the_url = @attrs['self']
427
+ the_url = the_url.sub(@client.options[:site].chomp('/'), '') if @client.options[:site]
428
+ the_url
429
+ elsif key_value
430
+ self.class.singular_path(client, key_value.to_s, prefix)
431
+ else
432
+ self.class.collection_path(client, prefix)
433
+ end
434
+ end
435
+
436
+ # This method fixes issue that there is no / prefix in url. It is happened when we call for instance
437
+ # Looks like this issue is actual only in case if you use atlassian sdk your app path is not root (like /jira in example below)
438
+ # issue.save() for existing resource.
439
+ # As a result we got error 400 from JIRA API:
440
+ # [07/Jun/2015:15:32:19 +0400] "PUT jira/rest/api/2/issue/10111 HTTP/1.1" 400 -
441
+ # After applying this fix we have normal response:
442
+ # [07/Jun/2015:15:17:18 +0400] "PUT /jira/rest/api/2/issue/10111 HTTP/1.1" 204 -
443
+ def patched_url
444
+ result = url
445
+ return result if result.start_with?('/', 'http')
446
+ "/#{result}"
447
+ end
448
+
449
+ def to_s
450
+ "#<#{self.class.name}:#{object_id} @attrs=#{@attrs.inspect}>"
451
+ end
452
+
453
+ # Returns a JSON representation of the current attributes hash.
454
+ def to_json(options = {})
455
+ attrs.to_json(options)
456
+ end
457
+
458
+ # Determines if the resource is newly created by checking whether its
459
+ # key_value is set. If it is nil, the record is new and the method
460
+ # will return true.
461
+ def new_record?
462
+ key_value.nil?
463
+ end
464
+
465
+ protected
466
+
467
+ # This allows conditional lookup of possibly nested attributes. Example usage:
468
+ #
469
+ # maybe_nested_attribute('foo') # => @attrs['foo']
470
+ # maybe_nested_attribute('foo', 'bar') # => @attrs['bar']['foo']
471
+ # maybe_nested_attribute('foo', ['bar', 'baz']) # => @attrs['bar']['baz']['foo']
472
+ #
473
+ def maybe_nested_attribute(attribute_name, nested_under = nil)
474
+ self.class.maybe_nested_attribute(@attrs, attribute_name, nested_under)
475
+ end
476
+
477
+ def self.maybe_nested_attribute(attributes, attribute_name, nested_under = nil)
478
+ return attributes[attribute_name] if nested_under.nil?
479
+ if nested_under.instance_of? Array
480
+ final = nested_under.inject(attributes) do |parent, key|
481
+ break if parent.nil?
482
+ parent[key]
483
+ end
484
+ return nil if final.nil?
485
+ final[attribute_name]
486
+ else
487
+ return attributes[nested_under][attribute_name]
488
+ end
489
+ end
490
+
491
+ def url_with_query_params(url, query_params)
492
+ self.class.url_with_query_params(url, query_params)
493
+ end
494
+
495
+ def self.url_with_query_params(url, query_params)
496
+ if !query_params.empty?
497
+ "#{url}?#{hash_to_query_string query_params}"
498
+ else
499
+ url
500
+ end
501
+ end
502
+
503
+ def hash_to_query_string(query_params)
504
+ self.class.hash_to_query_string(query_params)
505
+ end
506
+
507
+ def self.hash_to_query_string(query_params)
508
+ query_params.map do |k, v|
509
+ CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
510
+ end.join('&')
511
+ end
512
+
513
+ def self.query_params_for_single_fetch(options)
514
+ Hash[options.select do |k, _v|
515
+ QUERY_PARAMS_FOR_SINGLE_FETCH.include? k
516
+ end]
517
+ end
518
+
519
+ def self.query_params_for_search(options)
520
+ Hash[options.select do |k, _v|
521
+ QUERY_PARAMS_FOR_SEARCH.include? k
522
+ end]
523
+ end
524
+ end
525
+ end