jira-ruby-added-transitions 0.1.3

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