jira-ruby-dmg 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
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