sclemmer-jira-ruby 0.1.12

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/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