jira-ruby 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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