jira-ruby-added-transitions 0.1.3

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