fisheye-crucible 0.0.1

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.
@@ -0,0 +1,39 @@
1
+ module CommonHelpers
2
+ def in_tmp_folder(&block)
3
+ FileUtils.chdir(@tmp_root, &block)
4
+ end
5
+
6
+ def in_project_folder(&block)
7
+ project_folder = @active_project_folder || @tmp_root
8
+ FileUtils.chdir(project_folder, &block)
9
+ end
10
+
11
+ def in_home_folder(&block)
12
+ FileUtils.chdir(@home_path, &block)
13
+ end
14
+
15
+ def force_local_lib_override(project_name = @project_name)
16
+ rakefile = File.read(File.join(project_name, 'Rakefile'))
17
+ File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
18
+ f << "$:.unshift('#{@lib_path}')\n"
19
+ f << rakefile
20
+ end
21
+ end
22
+
23
+ def setup_active_project_folder project_name
24
+ @active_project_folder = File.join(@tmp_root, project_name)
25
+ @project_name = project_name
26
+ end
27
+ end
28
+
29
+ World(CommonHelpers)
30
+
31
+ # Common steps
32
+ Given /^I have logged in$/ do
33
+ @fc.login 'gemtest', 'gemtest'
34
+ @fc.instance_eval("@token").should_not be_nil
35
+ end
36
+
37
+ Given /^I have not logged in$/ do
38
+ @fc.instance_eval("@token").should be_nil
39
+ end
@@ -0,0 +1,3 @@
1
+ ---
2
+ username:
3
+ password:
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + "/../../lib/fisheye-crucible"
2
+ require File.dirname(__FILE__) + "/../../lib/fisheye-crucible/client/legacy"
3
+
4
+ gem 'cucumber'
5
+ require 'cucumber'
6
+ gem 'rspec'
7
+ require 'spec'
8
+
9
+ Before do
10
+ @tmp_root = File.dirname(__FILE__) + "/../../tmp"
11
+ @home_path = File.expand_path(File.join(@tmp_root, "home"))
12
+ @lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib")
13
+ FileUtils.rm_rf @tmp_root
14
+ FileUtils.mkdir_p @home_path
15
+ ENV['HOME'] = @home_path
16
+ end
@@ -0,0 +1,9 @@
1
+ Before do
2
+ @fc = FisheyeCrucible::Client::Legacy.new 'http://sandbox.fisheye.atlassian.com'
3
+ end
4
+
5
+ =begin
6
+ at_exit do
7
+ @fc.logout if @fc
8
+ end
9
+ =end
@@ -0,0 +1,11 @@
1
+ module Matchers
2
+ def contain(expected)
3
+ simple_matcher("contain #{expected.inspect}") do |given, matcher|
4
+ matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}"
5
+ matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}"
6
+ given.index expected
7
+ end
8
+ end
9
+ end
10
+
11
+ World(Matchers)
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) ||
3
+ $:.include?(File.expand_path(File.dirname(__FILE__)))
4
+
5
+ require 'fisheye_crucible_exception'
6
+
7
+ module FisheyeCrucible
8
+ VERSION = '0.0.1'
9
+ WWW = 'http://github.com/turboladen/fisheye-crucible'
10
+ end
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'fisheye-crucible'
3
+ require 'rest-client'
4
+
5
+ # This is the parent class for accessing Fisheye/Crucible. This will change
6
+ # quite a bit once work on the current Fisheye/Crucible gets started. For
7
+ # now, look at the docs for FisheyeCrucible::Client::Legacy to get started.
8
+ class FisheyeCrucible::Client
9
+
10
+ # @return [Boolean] Turn debug on or off
11
+ attr_accessor :do_debug
12
+
13
+ ##
14
+ # Sets up a Rest object to interact with the server(s).
15
+ #
16
+ # @param [String] server The base URL of the server to connect to.
17
+ def initialize(server)
18
+ @server = server
19
+ @fisheye_rest = RestClient::Resource.new(@server)
20
+ @token = nil
21
+ @do_debug = false
22
+ end
23
+
24
+ ##
25
+ # Print out string if debug is turned on.
26
+ #
27
+ # @param [String] string The debug string to print out.
28
+ def debug(string)
29
+ if @do_debug
30
+ puts string
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,315 @@
1
+ require 'rubygems'
2
+ require 'fisheye-crucible/client'
3
+ require 'fisheye-crucible/type_converter'
4
+ require 'rexml/document'
5
+
6
+ ##
7
+ # This class provides access to the Fisheye/Crucible REST API that was
8
+ # used before version 2.0. More info here:
9
+ # http://confluence.atlassian.com/display/FECRUDEV/FishEye+Legacy+Remote+API
10
+ #
11
+ # All methods are named in Ruby style, however aliases are provided that are
12
+ # named after the Fisheye/Crucible function name (if different).
13
+ class FisheyeCrucible::Client::Legacy < FisheyeCrucible::Client
14
+
15
+ def initialize(server)
16
+ super(server)
17
+ end
18
+
19
+ ##
20
+ # Logs in with provided credentials and returns a token that can be used for
21
+ # all other calls.
22
+ #
23
+ # @param [String] username The user to login with.
24
+ # @param [String] password The password of the user to login with.
25
+ # @return [String] The token to use for other calls.
26
+ def login(username=nil, password=nil)
27
+ @token = build_rest_call('api/rest/login',
28
+ :post,
29
+ {
30
+ :username => username,
31
+ :password => password
32
+ }
33
+ )
34
+ end
35
+
36
+ ##
37
+ # Logs out of Fisheye/Crucible.
38
+ #
39
+ # @return [Boolean] Returns true if logout was successful.
40
+ def logout
41
+ unless @token
42
+ error_message = "Can't log out--it seems you're not logged in."
43
+ raise FisheyeCrucibleError, error_message
44
+ end
45
+
46
+ result = build_rest_call('api/rest/logout', :post, { :auth => @token })
47
+
48
+ @token = '' if result == true
49
+
50
+ result
51
+ end
52
+
53
+ ##
54
+ # Gets the version of Fisheye.
55
+ #
56
+ # @return [String] The version of Fisheye.
57
+ # @alias fisheyeVersion
58
+ def fisheye_version
59
+ build_rest_call('api/rest/fisheyeVersion', :get)
60
+ end
61
+ alias_method :fisheyeVersion, :fisheye_version
62
+
63
+ ##
64
+ # Gets the version of Crucible.
65
+ #
66
+ # @return [String] The version of Crucible.
67
+ def crucible_version
68
+ build_rest_call('api/rest/fisheyeVersion', :get)
69
+ end
70
+ alias_method :crucibleVersion, :crucible_version
71
+
72
+ ##
73
+ # Gets the list of repositories to an array.
74
+ #
75
+ # @return [Array] The list of repositories.
76
+ def repositories
77
+ build_rest_call('api/rest/repositories',
78
+ :post,
79
+ { :auth => @token }
80
+ )
81
+ end
82
+ alias_method :listRepositories, :repositories
83
+
84
+ ##
85
+ # Gets the file/dir listing from a repository.
86
+ #
87
+ # @param [String] repository The repository to get the listing for.
88
+ # @param [String] path The directory in the repository to get the listing
89
+ # for. If no path is given, listing is from /.
90
+ # @return [Hash<String><Hash>] The listing, where the key is the
91
+ # file/directory and the value is another Hash that contains properties of
92
+ # the file/directory.
93
+ def list_paths_from(repository, path='')
94
+ build_rest_call('api/rest/listPaths',
95
+ :post,
96
+ {
97
+ :auth => @token,
98
+ :rep => repository,
99
+ :path => path
100
+ }
101
+ )
102
+ end
103
+ alias_method :listPaths, :list_paths_from
104
+
105
+ ##
106
+ # Gets details about a specific file/directory revision from the given
107
+ # repository.
108
+ #
109
+ # @param [String] repository The repository in which the file resides.
110
+ # @param [String] path The path, relative to the repository, in which the
111
+ # file resides.
112
+ # @param [Fixnum] revision The revision of the file/directory to get the info
113
+ # about.
114
+ # @return [Hash] The list of details about the file revision.
115
+ def revision(repository, path, revision)
116
+ build_rest_call('api/rest/revision',
117
+ :post,
118
+ {
119
+ :auth => @token,
120
+ :rep => repository,
121
+ :path => path,
122
+ :rev => revision.to_s
123
+ }
124
+ )
125
+ end
126
+ alias_method :getRevision, :revision
127
+
128
+ ##
129
+ # Gets tags associated with a file/directory revision.
130
+ #
131
+ # @param [String] repository The repository in which the file resides.
132
+ # @param [String] path The path, relative to the repository, in which the
133
+ # file resides.
134
+ # @param [Fixnum] revision The revision of the file/directory to get the tags
135
+ # for.
136
+ # @return [Hash] The list of tags for the file revision.
137
+ def tags(repository, path, revision)
138
+ puts "WARNING: This method is untested!"
139
+
140
+ tags = build_rest_call('api/rest/tags',
141
+ :post,
142
+ {
143
+ :auth => @token,
144
+ :rep => repository,
145
+ :path => path,
146
+ :rev => revision.to_s
147
+ }
148
+ )
149
+ =begin
150
+ tags_xml = @fisheye_rest['api/rest/tags'].post :auth => @token,
151
+ :rep => repository,
152
+ :path => path,
153
+ :rev => revision.to_s
154
+
155
+ #debug tags_xml
156
+ return tags_xml.to_ruby
157
+ doc = REXML::Document.new(tags_xml)
158
+
159
+ if doc.root.name.eql? 'error'
160
+ raise doc.root.text
161
+ elsif doc.root.name.eql? 'response' and doc.root.has_elements?
162
+ # TODO: Not sure if this works since I can't find any files
163
+ # with tags.
164
+ return doc.root.elements['//tags'].text
165
+ elsif doc.root.name.eql? 'response' and !doc.root.has_elements?
166
+ return ""
167
+ end
168
+ =end
169
+ end
170
+ alias_method :listTagsForRevision, :tags
171
+
172
+ ##
173
+ # Gets the history for a file/directory, which is a list of revisions and
174
+ # their associated info.
175
+ #
176
+ # @param [String] repository The repository for which to get the history
177
+ # info about.
178
+ # @param [String] path The path, relative to root, for which to get info
179
+ # about.
180
+ # @return [Array<Hash>] The list of revisions.
181
+ def path_history(repository, path='')
182
+ build_rest_call('api/rest/pathHistory',
183
+ :post,
184
+ {
185
+ :auth => @token,
186
+ :rep => repository,
187
+ :path => path
188
+ }
189
+ )
190
+ end
191
+ alias :pathHistory :path_history
192
+
193
+ ##
194
+ # Gets information about a changeset.
195
+ #
196
+ # @param [String] repository The repository for which to get the changeset
197
+ # info about.
198
+ # @param [Fixnum] csid The changeset ID to get the info about.
199
+ # @return [Hash] All of the changeset info as defined by the API.
200
+ def changeset(repository, csid)
201
+ build_rest_call('api/rest/changeset',
202
+ :post,
203
+ {
204
+ :auth => @token,
205
+ :rep => repository,
206
+ :csid => csid.to_s
207
+ }
208
+ )
209
+ end
210
+ alias_method :getChangeset, :changeset
211
+
212
+ ##
213
+ # Gets all changeset IDs (csids) that match the parameters given.
214
+ #
215
+ # @param [String] repository The repository for which to get the changesets
216
+ # for.
217
+ # @param [String] path Path to the item(s) to get changesets for. Can only
218
+ # be a directory.
219
+ # @param [Fixnum] max_return The maximum number of values to return. If not
220
+ # set, this is limited by the internal Fisheye server. If set, the most
221
+ # recent results are returned.
222
+ # @param [DateTime] start_date The DateTime object representing the start
223
+ # date for which to filter the query.
224
+ # @param [DateTime] end_date The DateTime object representing the end date
225
+ # for which to filter the query.
226
+ # @return [Hash<Array,String>] :csids => the Array of changeset IDs;
227
+ # :max_return => Max values returned.
228
+ def changesets(repository, path='/', max_return=nil, start_date=nil,
229
+ end_date=nil)
230
+ build_rest_call('api/rest/changesets',
231
+ :post,
232
+ {
233
+ :auth => @token,
234
+ :rep => repository,
235
+ :path => path,
236
+ :start => start_date,
237
+ :end => end_date,
238
+ :maxReturn => max_return
239
+ }
240
+ )
241
+ end
242
+ alias_method :listChangesets, :changesets
243
+
244
+ ##
245
+ # Sends an EyeQL query to the server for a given repository. Return types
246
+ # can differ depending on the 'return-clause' used in the query. For more
247
+ # info, see http://confluence.atlassian.com/display/FISHEYE/EyeQL+Reference+Guide.
248
+ #
249
+ # @param [String] repository The repository to run the EyeQL query on.
250
+ # @param [String] query The EyeQL query to run.
251
+ # @return [Object]
252
+ def query(repository, query)
253
+ build_rest_call('api/rest/query',
254
+ :post,
255
+ {
256
+ :auth => @token,
257
+ :rep => repository,
258
+ :query => query
259
+ }
260
+ )
261
+ end
262
+
263
+ ##
264
+ # Privates
265
+ private
266
+
267
+ ##
268
+ # Builds and makes the REST call from the arguments given.
269
+ #
270
+ # @param [String] url The API portion of the URL as defined by the API.
271
+ # @param [String] action 'post' or 'get'.
272
+ # @param [Hash] options The REST params to pass to the REST call.
273
+ # @return [Object] The Object that #to_ruby returns.
274
+ def build_rest_call(url, action, options=nil)
275
+ rest_call = "@fisheye_rest['#{url}'].#{action}"
276
+
277
+ unless options.nil?
278
+ actual_options = non_nil_options_in options
279
+
280
+ option_count = 1
281
+ actual_options.each_pair do |key,value|
282
+ unless value.nil?
283
+ rest_call << " :#{key} => '#{value}'"
284
+ rest_call << ',' unless option_count == actual_options.length
285
+ option_count += 1
286
+ end
287
+ end
288
+ end
289
+
290
+ response_xml = eval(rest_call)
291
+ response = response_xml.to_ruby
292
+
293
+ if response.class == FisheyeCrucibleError
294
+ raise response
295
+ end
296
+
297
+ response
298
+ end
299
+
300
+ ##
301
+ # Removes all key/value pairs from Hash that have a nil value and returns
302
+ # a Hash with keys that have values.
303
+ #
304
+ # @param [Hash] options The Hash to remove nil key/value pairs from.
305
+ def non_nil_options_in options
306
+ non_nil_options = {}
307
+ options.each_pair do |k,v|
308
+ non_nil_options[k] = v unless v.nil?
309
+ end
310
+
311
+ non_nil_options
312
+ end
313
+ rescue FisheyeCrucibleError => e
314
+ puts e.message
315
+ end
@@ -0,0 +1,285 @@
1
+ require 'rexml/document'
2
+ require 'fisheye-crucible'
3
+
4
+ # By adding this method to String, the #to_ruby method can be called directly
5
+ # on the return data from RestClient. This, effectively, accomplishes turning
6
+ # a String of XML into the Ruby data types that make sense for the return types
7
+ # that Atlassian defined.
8
+ class String
9
+
10
+ # Takes a String of XML then converts in to a correlating Ruby data type.
11
+ # Aside from the documentation for each private method below, here is the
12
+ # conversion table:
13
+ # | Fisheye/Crucible Type | Ruby Type |
14
+ # | string | String |
15
+ # | boolean | TrueClass, FalseClass |
16
+ # | pathinfo | Hash with child Hashes |
17
+ # | revision | Hash |
18
+ # | history | Array of revisions |
19
+ # | changeset | Hash |
20
+ # | changesets | Hash of changesets |
21
+ # | revisionkey | Array with child Hashes |
22
+ # | row | Array |
23
+ def to_ruby
24
+ doc = REXML::Document.new self
25
+
26
+ type = doc.root.name
27
+ doc_text = doc.root.text
28
+
29
+ responses = []
30
+ response_type = ''
31
+
32
+ if type == 'error'
33
+ return FisheyeCrucibleError.new(doc_text)
34
+ elsif type == 'response'
35
+ doc.root.each_element do |element|
36
+ # The data type
37
+ response_type = element.name
38
+
39
+ # Text for the next element
40
+ responses << element.text
41
+ end
42
+ else
43
+ message = "Not sure what to do with this response:\n#{doc_text}"
44
+ return FisheyeCrucibleError.new(message)
45
+ end
46
+
47
+ # If we have 0 or 1 actual strings, return the string or ""
48
+ if response_type.eql? 'string' and responses.length <= 1
49
+ return string_to_string(doc)
50
+ # If we have mulitple strings, return the Array of Strings
51
+ elsif response_type.eql? 'string'
52
+ return string_to_array(doc)
53
+ elsif response_type.eql? 'boolean'
54
+ return boolean_to_true_false(doc)
55
+ elsif response_type.eql? 'pathinfo'
56
+ return pathinfo_to_hash(doc)
57
+ elsif response_type.eql? 'revision'
58
+ return revision_to_hash(doc)
59
+ elsif response_type.eql? 'history'
60
+ return history_to_array(doc)
61
+ elsif response_type.eql? 'changeset'
62
+ return changeset_to_hash(doc)
63
+ elsif response_type.eql? 'changesets'
64
+ return changesets_to_hash(doc)
65
+ elsif response_type.eql? 'revisionkey'
66
+ return revisionkeys_to_array(doc)
67
+ elsif response_type.eql? 'row'
68
+ return custom_to_array(doc)
69
+ end
70
+
71
+ message = "Response type unknown: '#{response_type}'"
72
+ return FisheyeCrucibleError.new(message)
73
+ end
74
+
75
+ ##
76
+ # PRIVATES!
77
+ private
78
+
79
+ ##
80
+ # Converts a REXML::Document with 1 element of type <string> into a String.
81
+ #
82
+ # @param [REXML::Document] xml_doc The XML document to convert.
83
+ # @return [String] The string from the XML document.
84
+ def string_to_string(xml_doc)
85
+ xml_doc.root.elements[1].text
86
+ end
87
+
88
+ ##
89
+ # Converts a String to its related Boolean type. If the string doesn't
90
+ # contain such a type, nil is returned.
91
+ #
92
+ # @param [String] string The String to convert.
93
+ # @return [Boolean,String] true, false, or the original string.
94
+ def string_to_true_false(string)
95
+ return true if string.eql? 'true'
96
+ return false if string.eql? 'false'
97
+ return string
98
+ end
99
+
100
+ ##
101
+ # Converts a REXML::Document with element <boolean> into a true or false
102
+ # value.
103
+ #
104
+ # @param [REXML::Document] xml_doc The XML document to convert.
105
+ # @return [Boolean] true, false, or nil.
106
+ def boolean_to_true_false(xml_doc)
107
+ string_to_true_false(xml_doc.root.elements[1].text)
108
+ end
109
+
110
+ ##
111
+ # Converts a REXML::Document with multiple elements of type <string> into an
112
+ # Array.
113
+ #
114
+ # @param [REXML::Document] xml_doc The XML document to convert.
115
+ # @return [Array] The list of strings.
116
+ def string_to_array(xml_doc)
117
+ responses = []
118
+ xml_doc.root.each_element do |element|
119
+ response_type = element.name
120
+ responses << element.text
121
+ end
122
+
123
+ responses
124
+ end
125
+
126
+ ##
127
+ # Takes Fisheye/Crucible's <pathinfo> return type and turns it in to
128
+ # a Hash of Hashes.
129
+ #
130
+ # @param [REXML::Document] xml_doc The XML document to convert.
131
+ # @return [Hash<Hash>] The path info as a Hash. The Hash contains keys
132
+ # which are the file/directory names in the path; values for those keys
133
+ # are Hashes which contain the properties of that file/directory.
134
+ def pathinfo_to_hash(xml_doc)
135
+ path_name = ''
136
+ path_names = {}
137
+
138
+ xml_doc.root.each_element do |element|
139
+ path_name = element.attributes["name"]
140
+ path_names[path_name] = {}
141
+
142
+ element.attributes.each_attribute do |attribute|
143
+ next attribute if attribute.name.eql? 'name'
144
+ boolean_value = string_to_true_false(attribute.value)
145
+ path_names[path_name][attribute.name.to_sym] = boolean_value
146
+ end
147
+ end
148
+
149
+ path_names
150
+ end
151
+
152
+ ##
153
+ # Takes Fisheye/Crucible's <revision> return type and turns it in to a single
154
+ # Hash.
155
+ #
156
+ # @param [REXML::Document] xml_doc The XML document to convert.
157
+ # @return [Hash] The info about the revision.
158
+ def revision_to_hash(xml_doc)
159
+ details = {}
160
+
161
+ xml_doc.root.elements['//revision'].attributes.each do |attribute|
162
+ # Convert the value to an Int if the string is just a number
163
+ if attribute[1] =~ /^\d+$/
164
+ details[attribute.first.to_sym] = attribute[1].to_i
165
+ else
166
+ details[attribute.first.to_sym] = attribute[1]
167
+ end
168
+ end
169
+ details[:log] = xml_doc.root.elements['//log'].text
170
+
171
+ details
172
+ end
173
+
174
+ ##
175
+ # Takes Fisheye/Crucible's <history> return type and turns it in to an
176
+ # Array of revisions, which are Hashes.
177
+ #
178
+ # @param [REXML::Document] xml_doc The XML document to convert.
179
+ # @return [Array<Hash>] The Array of revision Hashes.
180
+ def history_to_array(xml_doc)
181
+ revisions = []
182
+
183
+ revisions_xml = REXML::XPath.match(xml_doc, "//revisions/revision")
184
+ revisions_xml.each do |revision_xml|
185
+ revision = REXML::Document.new(revision_xml.to_s)
186
+ revisions << revision_to_hash(revision)
187
+ end
188
+
189
+ revisions
190
+ end
191
+
192
+ ##
193
+ # Takes Fisheye/Crucible's <changeset> return type and turns it in to a Hash.
194
+ #
195
+ # @param [REXML::Document] xml_doc The XML document to convert.
196
+ # @return [Hash] Hash containting the changeset history information as
197
+ # defined by the API.
198
+ def changeset_to_hash(xml_doc)
199
+ details = {}
200
+
201
+ xml_doc.root.elements['//changeset'].attributes.each do |attribute|
202
+ # Convert the value to an Int if the string is just a number
203
+ int_attribute = attribute[1]
204
+ if int_attribute =~ /^\d+$/
205
+ details[attribute.first.to_sym] = int_attribute.to_i
206
+ else
207
+ details[attribute.first.to_sym] = int_attribute
208
+ end
209
+ end
210
+ details[:log] = xml_doc.root.elements['//log'].text
211
+
212
+ # Revisions is an Array of Hashes, where each Hash is a key/value pair that
213
+ # contains the path and revsion of one of the files/directories that's
214
+ # part of the changeset.
215
+ details[:revisions] = []
216
+ details[:revisions] << revisionkeys_to_array(xml_doc)
217
+
218
+ details
219
+ end
220
+
221
+ ##
222
+ # Takes Fisheye/Crucible's <revisionkey> return type and turns it in to an
223
+ # Array of Hashes.
224
+ #
225
+ # @param [REXML::Document] xml_doc The XML document to convert.
226
+ # @return [Array<Hash>] The Array of path & rev data.
227
+ def revisionkeys_to_array(xml_doc)
228
+ revisionkeys = []
229
+
230
+ xml_doc.root.elements.each('//revisionkey') do |element|
231
+ revisionkey = { :path => element.attributes['path'],
232
+ :rev => element.attributes['rev'].to_i
233
+ }
234
+
235
+ revisionkeys << revisionkey
236
+ end
237
+
238
+ revisionkeys
239
+ end
240
+
241
+ ##
242
+ # Takes Fisheye/Crucible's <changesets> return type and turns it in to a
243
+ # Hash.
244
+ #
245
+ # @param [REXML::Document] xml_doc The XML document to convert.
246
+ # @return [Hash] Contains the changeset IDs as defined by the query.
247
+ def changesets_to_hash(xml_doc)
248
+ changesets = {}
249
+ changesets[:csids] = []
250
+
251
+ changesets[:max_return] = xml_doc.root.elements['//changesets'].
252
+ attributes['maxReturn']
253
+
254
+ xml_doc.root.elements['//csids'].each_element do |element|
255
+ changesets[:csids] << element.text.to_i
256
+ end
257
+
258
+ changesets
259
+ end
260
+
261
+ ##
262
+ # Takes Fisheye/Crucible's custom <row> return type (from a query) and turns
263
+ # it in to an Array of Hashes containing the results from the query.
264
+ #
265
+ # @param [REXML::Document] xml_doc The XML document to convert.
266
+ # @return [Array] The result from the query.
267
+ def custom_to_array(xml_doc)
268
+ responses = []
269
+
270
+ xml_doc.elements.each('//row') do |element|
271
+ response = {}
272
+ element.each do |subs|
273
+ if subs.is_a? REXML::Text
274
+ else
275
+ # TODO: If subs.text is a Boolean string, it doesn't get converted
276
+ # to the actual Boolean. Same if it's an int or empty string.
277
+ response[subs.name.to_sym] = subs.text
278
+ end
279
+ end
280
+ responses << response
281
+ end
282
+
283
+ responses
284
+ end
285
+ end