jira-ruby-dmg 0.1.10

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