sclemmer-jira-ruby 0.1.12

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/lib/jira.rb +33 -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 +80 -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/tasks/generate.rake +18 -0
  37. data/sclemmer-jira-ruby.gemspec +28 -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 +583 -0
  53. data/spec/jira/client_spec.rb +188 -0
  54. data/spec/jira/has_many_proxy_spec.rb +47 -0
  55. data/spec/jira/http_client_spec.rb +109 -0
  56. data/spec/jira/http_error_spec.rb +26 -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 +123 -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.post.json +28 -0
  67. data/spec/mock_responses/component/10000.invalid.put.json +5 -0
  68. data/spec/mock_responses/component/10000.json +39 -0
  69. data/spec/mock_responses/component/10000.put.json +39 -0
  70. data/spec/mock_responses/field.json +32 -0
  71. data/spec/mock_responses/field/1.json +15 -0
  72. data/spec/mock_responses/issue.json +1108 -0
  73. data/spec/mock_responses/issue.post.json +5 -0
  74. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  75. data/spec/mock_responses/issue/10002.json +126 -0
  76. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  77. data/spec/mock_responses/issue/10002/comment.json +65 -0
  78. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  79. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  80. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  81. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  82. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  83. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  84. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  85. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  86. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  87. data/spec/mock_responses/issuetype.json +42 -0
  88. data/spec/mock_responses/issuetype/5.json +8 -0
  89. data/spec/mock_responses/priority.json +42 -0
  90. data/spec/mock_responses/priority/1.json +8 -0
  91. data/spec/mock_responses/project.json +12 -0
  92. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  93. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  94. data/spec/mock_responses/status.json +37 -0
  95. data/spec/mock_responses/status/1.json +7 -0
  96. data/spec/mock_responses/user_username=admin.json +17 -0
  97. data/spec/mock_responses/version.post.json +7 -0
  98. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  99. data/spec/mock_responses/version/10000.json +11 -0
  100. data/spec/mock_responses/version/10000.put.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 +194 -0
  107. metadata +302 -0
@@ -0,0 +1,112 @@
1
+ require 'rubygems'
2
+ require 'pp'
3
+ require 'jira'
4
+
5
+ if ARGV.length == 0
6
+ # If not passed any command line arguments, prompt the
7
+ # user for the username and password.
8
+ puts "Enter the username: "
9
+ username = gets.strip
10
+
11
+ puts "Enter the password: "
12
+ password = gets.strip
13
+ elsif ARGV.length == 2
14
+ username, password = ARGV[0], ARGV[1]
15
+ else
16
+ # Script must be passed 0 or 2 arguments
17
+ raise "Usage: #{$0} [ username password ]"
18
+ end
19
+
20
+ options = {
21
+ :username => username,
22
+ :password => password,
23
+ :site => 'http://localhost:8080/',
24
+ :context_path => '',
25
+ :auth_type => :basic,
26
+ :use_ssl => false
27
+ }
28
+
29
+ client = JIRA::Client.new(options)
30
+
31
+ # Show all projects
32
+ projects = client.Project.all
33
+
34
+ projects.each do |project|
35
+ puts "Project -> key: #{project.key}, name: #{project.name}"
36
+ end
37
+
38
+ # # Find a specific project by key
39
+ # # ------------------------------
40
+ # project = client.Project.find('SAMPLEPROJECT')
41
+ # pp project
42
+ # project.issues.each do |issue|
43
+ # puts "#{issue.id} - #{issue.fields['summary']}"
44
+ # end
45
+ #
46
+ # # List all Issues
47
+ # # ---------------
48
+ # client.Issue.all.each do |issue|
49
+ # puts "#{issue.id} - #{issue.fields['summary']}"
50
+ # end
51
+ #
52
+ # # List issues by JQL query
53
+ # # ------------------------
54
+ # client.Issue.jql('PROJECT = "SAMPLEPROJECT"', [comments, summary]).each do |issue|
55
+ # puts "#{issue.id} - #{issue.fields['summary']}"
56
+ # end
57
+ #
58
+ # # Delete an issue
59
+ # # ---------------
60
+ # issue = client.Issue.find('SAMPLEPROJECT-2')
61
+ # if issue.delete
62
+ # puts "Delete of issue SAMPLEPROJECT-2 sucessful"
63
+ # else
64
+ # puts "Delete of issue SAMPLEPROJECT-2 failed"
65
+ # end
66
+ #
67
+ # # Create an issue
68
+ # # ---------------
69
+ # issue = client.Issue.build
70
+ # issue.save({"fields"=>{"summary"=>"blarg from in example.rb","project"=>{"id"=>"10001"},"issuetype"=>{"id"=>"3"}}})
71
+ # issue.fetch
72
+ # pp issue
73
+ #
74
+ # # Update an issue
75
+ # # ---------------
76
+ # issue = client.Issue.find("10002")
77
+ # issue.save({"fields"=>{"summary"=>"EVEN MOOOOOOARRR NINJAAAA!"}})
78
+ # pp issue
79
+ #
80
+ # # Find a user
81
+ # # -----------
82
+ # user = client.User.find('admin')
83
+ # pp user
84
+ #
85
+ # # Get all issue types
86
+ # # -------------------
87
+ # issuetypes = client.Issuetype.all
88
+ # pp issuetypes
89
+ #
90
+ # # Get a single issue type
91
+ # # -----------------------
92
+ # issuetype = client.Issuetype.find('5')
93
+ # pp issuetype
94
+ #
95
+ # # Get all comments for an issue
96
+ # # -----------------------------
97
+ # issue.comments.each do |comment|
98
+ # pp comment
99
+ # end
100
+ #
101
+ # # Build and Save a comment
102
+ # # ------------------------
103
+ # comment = issue.comments.build
104
+ # comment.save!(:body => "New comment from example script")
105
+ #
106
+ # # Delete a comment from the collection
107
+ # # ------------------------------------
108
+ # issue.comments.last.delete
109
+ #
110
+ # # Update an existing comment
111
+ # # --------------------------
112
+ # issue.comments.first.save({"body" => "an updated comment frome example.rb"})
data/lib/jira.rb ADDED
@@ -0,0 +1,33 @@
1
+ $: << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'active_support/inflector'
4
+ ActiveSupport::Inflector.inflections do |inflector|
5
+ inflector.singular 'status', 'status'
6
+ end
7
+
8
+ require 'jira/base'
9
+ require 'jira/base_factory'
10
+ require 'jira/has_many_proxy'
11
+ require 'jira/http_error'
12
+
13
+ require 'jira/resource/user'
14
+ require 'jira/resource/attachment'
15
+ require 'jira/resource/component'
16
+ require 'jira/resource/issuetype'
17
+ require 'jira/resource/version'
18
+ require 'jira/resource/status'
19
+ require 'jira/resource/transition'
20
+ require 'jira/resource/project'
21
+ require 'jira/resource/priority'
22
+ require 'jira/resource/comment'
23
+ require 'jira/resource/worklog'
24
+ require 'jira/resource/issue'
25
+ require 'jira/resource/filter'
26
+ require 'jira/resource/field'
27
+
28
+ require 'jira/request_client'
29
+ require 'jira/oauth_client'
30
+ require 'jira/http_client'
31
+ require 'jira/client'
32
+
33
+ require 'jira/railtie' if defined?(Rails)
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