rally_api 0.4.0

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,104 @@
1
+ RallyAPI (rally_api)
2
+
3
+ ==Description
4
+
5
+ RallyAPI is a wrapper of Rally's Web Service API Json endpoints
6
+
7
+ == Installation
8
+
9
+ gem install rest-client
10
+ gem install json
11
+ gem install rally_api
12
+
13
+ ** Note if you are on Windows - you will need DevKit from http://rubyinstaller.org to properly install the json gem
14
+ ** - that native compiled parsing gives us some speed.
15
+ ** - Follow the instructions here to install DevKit: https://github.com/oneclick/rubyinstaller/wiki/Development-Kit
16
+
17
+ == Usage
18
+
19
+ require 'rally_api'
20
+
21
+ #Setting custom headers
22
+ headers = RallyAPI::CustomHttpHeader.new()
23
+ headers.name = "My Utility"
24
+ headers.vendor = "MyCompany"
25
+ headers.version = "1.0"
26
+
27
+ #==================== Making a connection to Rally ====================
28
+ config = {:base_url => "https://rally1.rallydev.com"}
29
+ config[:username] = "user@company.com"
30
+ config[:password] = "password"
31
+ config[:workspace] = "Workspace Name"
32
+ config[:project] = "Project Name"
33
+ config[:headers] = headers #from RallyAPI::CustomHttpHeader.new()
34
+
35
+ @rally = RallyAPI::RallyRestJson.new(config)
36
+
37
+ #==================== Querying Rally ==========================
38
+ test_query = RallyAPI::RallyQuery.new()
39
+ test_query.type = :defect
40
+ test_query.fetch = "Name"
41
+ test_query.workspace = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.25/workspace/12345.js" } #optional
42
+ test_query.project = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.25/project/12345.js" } #optional
43
+ test_query.page_size = 200 #optional - default is 200
44
+ test_query.limit = 1000 #optional - default is 99999
45
+ test_query.project_scope_up = false
46
+ test_query.project_scope_down = true
47
+ test_query.order = "Name Asc"
48
+ test_query.query_string = "(Severity = \"High\")"
49
+
50
+ results = @rally.find(test_query)
51
+
52
+ #tip - set the fetch string of the query to the fields you need -
53
+ #only resort to the read method if you want your code to be slow
54
+ results.each do |defect|
55
+ puts defect.Name
56
+ defect.read #read the whole defect from Rally to get all fields (eg Severity)
57
+ puts defect.Severity
58
+ end
59
+
60
+ #==================== Reading an Artifact ====================
61
+ defect = @rally.read(:defect, 12345) #by ObjectID
62
+ #or
63
+ defect = @rally.read(:defect, "FormattedID|DE42") #by FormattedID
64
+ #or if you already have an object like from a query
65
+ results = @rally.find(RallyAPI::RallyQuery.new({:type => :defect, :query_string => "(FormattedID = DE42)"}))
66
+ defect = results.first
67
+ defect.read
68
+
69
+ puts defect["Name"]
70
+ #or - fields can be read by bracket artifact["FieldDisplayName"] or artifact.FieldDisplayName
71
+ puts defect.Name
72
+
73
+ #An Important note about reading fields and fetching:
74
+ #If you query with a specific fetch string, for example query defect and fetch Name,Severity,Description
75
+ #You will *only* get back those fields defect.Priority will be nil, but may not be null in Rally
76
+ #Use object.read or @rally.read to make sure you read the whole object if you want what is in Rally
77
+ # This is done for speed - lazy loading (going back to get a value from Rally) can be unneccessarily slow
78
+ # *Pick you fetch strings wisely* fetch everything you need and don't rely on read if you don't need it the speed is worth it.
79
+
80
+ #==================== Creating an Artifact ====================
81
+ obj = {}
82
+ obj["Name"] = "Test Defect created #{DateTime.now()}"
83
+ new_de = @rally.create(:defect, obj)
84
+ puts new_de["FormattedID"]
85
+
86
+ #==================== Updating an Artifact ====================
87
+ fields = {}
88
+ fields["Severity"] = "Critical"
89
+ fields["Description"] = "Description for the issue"
90
+ updated_defect = @rally.update(:defect, 12345, fields) #by ObjectID
91
+ #or
92
+ updated_defect = @rally.update(:defect, "FormattedID|DE42", fields) #by FormattedID
93
+ # or
94
+ defect = @rally.read(:defect, 12345) #by lookup then udpating via the object
95
+ field_updates = {"Description" => "Changed Description"}
96
+ defect.update(field_updates)
97
+
98
+
99
+ #== License
100
+ #Copyright (c) 2002-2011 Rally Software Development Corp. All Rights Reserved.
101
+ #Your use of this Software is governed by the terms and conditions
102
+ #of the applicable Subscription Agreement between your company and
103
+ #Rally Software Development Corp.
104
+
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :make_rally_api_gem do
5
+ system("gem build rally_api.gemspec")
6
+ end
7
+
8
+ desc "run all api tests"
9
+ RSpec::Core::RakeTask.new('api_tests') do |t|
10
+ t.pattern = ['test/*_spec.rb']
11
+ end
12
+
13
+ desc "run api create tests"
14
+ RSpec::Core::RakeTask.new('api_create_tests') do |t|
15
+ t.pattern = ['test/*create_spec.rb']
16
+ end
17
+
@@ -0,0 +1,11 @@
1
+ require "rally_api/version"
2
+ require "rally_api/rally_rest_json"
3
+
4
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
5
+ #Your use of this Software is governed by the terms and conditions
6
+ #of the applicable Subscription Agreement between your company and
7
+ #Rally Software Development Corp.
8
+
9
+ module RallyAPI
10
+ # Your code goes here...
11
+ end
@@ -0,0 +1,37 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+
6
+ require_relative "version"
7
+
8
+ module RallyAPI
9
+
10
+ class CustomHttpHeader
11
+ attr_accessor :name, :version, :vendor
12
+ attr_reader :library, :os, :platform
13
+
14
+ HTTP_HEADER_FIELDS = [:name, :vendor, :version, :library, :platform, :os]
15
+ HTTP_HEADER_PREFIX = 'X-RallyIntegration'
16
+
17
+ def initialize
18
+ @os = RUBY_PLATFORM
19
+ @platform = "Ruby #{RUBY_VERSION}"
20
+ @library = "RallyRestJson version #{VERSION}"
21
+ @name = "RallyRestJson"
22
+ end
23
+
24
+ def headers
25
+ headers = {}
26
+ HTTP_HEADER_FIELDS.each do |field|
27
+ value = self.send(field)
28
+ next if value.nil?
29
+ header_key = "#{HTTP_HEADER_PREFIX}#{field.to_s.capitalize}"
30
+ headers[header_key.to_sym] = value
31
+ end
32
+ headers
33
+ end
34
+ end
35
+
36
+ end
37
+
@@ -0,0 +1,203 @@
1
+ require 'rest_client'
2
+ require 'json'
3
+
4
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
5
+ #Your use of this Software is governed by the terms and conditions
6
+ #of the applicable Subscription Agreement between your company and
7
+ #Rally Software Development Corp.
8
+ module RallyAPI
9
+
10
+
11
+ class RallyJsonConnection
12
+
13
+ DEFAULT_PAGE_SIZE = 200
14
+
15
+ attr_accessor :rally_headers, :retries, :retry_list
16
+
17
+ def initialize(headers, low_debug, proxy_info)
18
+ @rally_headers = headers
19
+ @low_debug = low_debug
20
+ @retries = 0
21
+ @retry_list = {}
22
+
23
+ if (!ENV["http_proxy"].nil?) && (proxy_info.nil?)
24
+ RestClient.proxy = ENV['http_proxy']
25
+ end
26
+
27
+ if !proxy_info.nil?
28
+ RestClient.proxy = proxy_info
29
+ end
30
+
31
+ end
32
+
33
+ def read_object(url, args)
34
+ args[:method] = :get
35
+ result = send_json_request(url, args)
36
+ puts result if @low_debug
37
+ rally_type = result.keys[0]
38
+ result[rally_type]
39
+ end
40
+
41
+ def create_object(url, args, rally_object)
42
+ args[:method] = :post
43
+ text_json = rally_object.to_json
44
+ args[:payload] = text_json
45
+ puts "payload json: #{text_json}" if @low_debug
46
+ result = send_json_request(url, args)
47
+ puts result if @low_debug
48
+ result["CreateResult"]["Object"]
49
+ end
50
+
51
+ def update_object(url, args, rally_fields)
52
+ args[:method] = :post
53
+ text_json = rally_fields.to_json
54
+ args[:payload] = text_json
55
+ puts "payload json: #{text_json}" if @low_debug
56
+ result = send_json_request(url, args)
57
+ puts result if @low_debug
58
+ result["OperationResult"]
59
+ end
60
+
61
+ def put_object(url, args, put_params, rally_fields)
62
+ args[:method] = :put
63
+ text_json = rally_fields.to_json
64
+ args[:payload] = text_json
65
+ result = send_json_request(url, args, put_params)
66
+ result["OperationResult"]
67
+ end
68
+
69
+ def delete_object(url,args)
70
+ args[:method] = :delete
71
+ result = send_json_request(url,args)
72
+ puts result if @low_debug
73
+ result["OperationResult"]
74
+ end
75
+
76
+ #---------------------------------------------
77
+ def get_all_json_results(url, args, query_params, limit = 99999)
78
+ all_results = []
79
+ args[:method] = :get
80
+ params = {}
81
+ params[:pagesize] = query_params[:pagesize] || DEFAULT_PAGE_SIZE
82
+ params[:start] = 1
83
+ params = params.merge(query_params)
84
+
85
+ query_result = send_json_request(url, args, params)
86
+ all_results.concat(query_result["QueryResult"]["Results"])
87
+ totals = query_result["QueryResult"]["TotalResultCount"]
88
+
89
+ limit < totals ? stop = limit : stop = totals
90
+ page = params[:pagesize] + 1
91
+ page_num = 2
92
+ query_array = []
93
+ page.step(stop, params[:pagesize]) do |new_page|
94
+ params[:start] = new_page
95
+ query_array.push({:page_num => page_num, :url => url, :args => args, :params => params.dup})
96
+ page_num = page_num + 1
97
+ end
98
+
99
+ all_res = []
100
+ all_res = run_threads(query_array) if query_array.length > 0
101
+ #stitch results back together in order
102
+ all_res.each { |page_res| all_results.concat(page_res[:results]["QueryResult"]["Results"]) }
103
+
104
+ query_result["QueryResult"]["Results"] = all_results
105
+ query_result
106
+ end
107
+
108
+ private
109
+
110
+ def run_threads(query_array)
111
+ num_threads = 4
112
+ thr_queries = []
113
+ (0...num_threads).each { |ind| thr_queries[ind] = [] }
114
+ query_array.each { |query| thr_queries[query[:page_num] % num_threads].push(query) }
115
+
116
+ thr_array = []
117
+ thr_queries.each { |thr_query_array| thr_array.push(run_single_thread(thr_query_array)) }
118
+
119
+ all_results = []
120
+ thr_array.each do |thr|
121
+ thr.value.each { |result_val| all_results.push(result_val) }
122
+ end
123
+ all_results.sort! { |resa, resb| resa[:page_num] <=> resb[:page_num] }
124
+ end
125
+
126
+ def run_single_thread(request_array)
127
+ Thread.new do
128
+ thread_results = []
129
+ request_array.each do |req|
130
+ page_res = send_json_request(req[:url], req[:args], req[:params])
131
+ thread_results.push({:page_num => req[:page_num], :results => page_res})
132
+ end
133
+ thread_results
134
+ end
135
+ end
136
+
137
+ #todo support proxy stuff
138
+ def send_json_request(url, args, url_params = nil)
139
+ request_args = {}
140
+ request_args[:url] = url
141
+ request_args[:user] = args[:user]
142
+ request_args[:password] = args[:password]
143
+ request_args[:method] = args[:method]
144
+ request_args[:timeout] = 120
145
+ request_args[:open_timeout] = 120
146
+
147
+ r_headers = @rally_headers.headers
148
+ r_headers[:params] = url_params
149
+
150
+ if (args[:method] == :post) || (args[:method] == :put)
151
+ r_headers[:content_type] = :json
152
+ r_headers[:accept] = :json
153
+ request_args[:payload] = args[:payload]
154
+ end
155
+
156
+ request_args[:headers] = r_headers
157
+
158
+ begin
159
+ req = RestClient::Request.new(request_args)
160
+ puts req.url if @low_debug
161
+ response = req.execute
162
+ rescue => ex
163
+ msg = "Rally Rest Json: - rescued exception - #{ex.message} on request to #{url} with params #{url_params}"
164
+ puts msg
165
+ if !@retry_list.has_key?(req.url)
166
+ @retry_list[req.url] = 0
167
+ end
168
+ if (@retries > 0) && (retry_list[req.url] < @retries)
169
+ @retry_list[req.url] += 1
170
+ retry
171
+ end
172
+ raise msg
173
+ end
174
+ @retry_list.delete(req.url)
175
+ puts response if @low_debug
176
+ json_obj = JSON.parse(response.body) #todo handle null post error
177
+ errs = check_for_errors(json_obj)
178
+ raise "\nError on request - #{url} - \n#{errs}" if errs[:errors].length > 0
179
+ json_obj
180
+ end
181
+
182
+
183
+ def check_for_errors(result)
184
+ errors = []
185
+ warnings = []
186
+ if !result["OperationResult"].nil?
187
+ errors = result["OperationResult"]["Errors"]
188
+ warnings = result["OperationResult"]["Warnings"]
189
+ elsif !result["QueryResult"].nil?
190
+ errors = result["QueryResult"]["Errors"]
191
+ warnings = result["QueryResult"]["Warnings"]
192
+ elsif !result["CreateResult"].nil?
193
+ errors = result["CreateResult"]["Errors"]
194
+ warnings = result["CreateResult"]["Warnings"]
195
+ end
196
+
197
+ {:errors => errors, :warnings => warnings}
198
+ end
199
+
200
+ end
201
+
202
+
203
+ end
@@ -0,0 +1,99 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+ module RallyAPI
6
+
7
+ class RallyObject
8
+
9
+ attr_reader :rally_object
10
+
11
+ def initialize(rally_rest, json_hash)
12
+ @type = json_hash["_type"] || json_hash["_ref"].split("/")[-2]
13
+ @rally_object = json_hash
14
+ @rally_rest = rally_rest
15
+ end
16
+
17
+ def update(fields)
18
+ oid = @rally_object["ObjectID"] || @rally_object["_ref"].split("/")[-1].split(".")[0]
19
+ @rally_object = @rally_rest.update(@type.downcase.to_sym, oid, fields).rally_object
20
+ end
21
+
22
+ def to_s(*args)
23
+ @rally_object.to_json
24
+ end
25
+
26
+ def to_json(*args)
27
+ @rally_object.to_json
28
+ end
29
+
30
+ def [](field_name)
31
+ get_val(field_name)
32
+ end
33
+
34
+ def read()
35
+ @rally_object = @rally_rest.reread(@rally_object)
36
+ self
37
+ end
38
+
39
+ def getref
40
+ @rally_object["_ref"]
41
+ end
42
+
43
+ def rank_above(relative_rally_object)
44
+ @rally_rest.rank_above(@rally_object["_ref"],relative_rally_object["_ref"])
45
+ end
46
+
47
+ def rank_below(relative_rally_object)
48
+ @rally_rest.rank_below(@rally_object["_ref"],relative_rally_object["_ref"])
49
+ end
50
+
51
+ def delete()
52
+ @rally_rest.delete(@rally_object["_ref"])
53
+ end
54
+
55
+ private
56
+
57
+ def method_missing(sym, *args)
58
+ get_val(sym.to_s)
59
+ end
60
+
61
+ #the magic of lazy loading is gone
62
+ #def method_missing(sym, *args)
63
+ # return_val = get_val(sym.to_s)
64
+ #
65
+ # if return_val.nil?
66
+ # @rally_object = @rally_rest.reread(@rally_object)
67
+ # return_val = get_val(sym.to_s)
68
+ # end
69
+ #
70
+ # return_val
71
+ #end
72
+
73
+ def get_val(field)
74
+ return_val = @rally_object[field]
75
+
76
+ if return_val.class == Hash
77
+ return RallyObject.new(@rally_rest, return_val)
78
+ end
79
+
80
+ if return_val.class == Array
81
+ make_object_array(field)
82
+ return_val = @rally_object[field]
83
+ end
84
+
85
+ return_val
86
+ end
87
+
88
+ def make_object_array(field)
89
+ object_array = []
90
+ @rally_object[field].each do |rally_obj|
91
+ object_array.push(RallyObject.new(@rally_rest, rally_obj))
92
+ end
93
+ @rally_object[field] = object_array
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+
@@ -0,0 +1,94 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+ module RallyAPI
6
+ # query is like:
7
+ # query_hash = {}
8
+ # query_hash[:type] = Defect, Story, etc
9
+ # query_hash[:query_string] = "(State = \"Closed\")"
10
+ # query_hash[:fetch] = "Name,State,etc"
11
+ # query_hash[:workspace] = workspace json object or ref #defaults to workspace passed in RallyRestJson.new if nil
12
+ # query_hash[:project] = project json object or ref #defaults to project passed in RallyRestJson.new if nil
13
+ # query_hash[:project_scope_up] = true/false
14
+ # query_hash[:project_scope_down] = true/false
15
+ # query_hash[:order] = "ObjectID asc"
16
+ # query_hash[:page_size]
17
+ # query_hash[:stopafter]
18
+
19
+ class RallyQuery
20
+ attr_accessor :type, :query_string, :fetch, :workspace, :project, :project_scope_up, :project_scope_down
21
+ attr_accessor :order, :page_size, :limit
22
+
23
+ def initialize(query_hash = nil)
24
+ parse_query_hash(query_hash) if !query_hash.nil?
25
+ @page_size = 200 if @page_size.nil?
26
+ @limit = 99999 if @limit.nil?
27
+ @project_scope_up = false if @project_scope_up.nil?
28
+ @project_scope_down = false if @project_scope_down.nil?
29
+ self
30
+ end
31
+
32
+ def make_query_params
33
+ query_params = {}
34
+ query_params[:query] = @query_string
35
+ query_params[:fetch] = @fetch
36
+ query_params[:workspace] = @workspace["_ref"] if !@workspace.nil?
37
+ query_params[:project] = @project["_ref"] if !@project.nil?
38
+ query_params[:projectScopeUp] = @project_scope_up
39
+ query_params[:projectScopeDown] = @project_scope_down
40
+ query_params[:order] = @order
41
+ query_params[:pagesize] = @page_size
42
+
43
+ query_params
44
+ end
45
+
46
+ def validate(allowed_objects)
47
+ errors = []
48
+
49
+ if @type.nil?
50
+ errors.push("Object type for query cannot be nil")
51
+ end
52
+
53
+ if @limit < 0
54
+ errors.push("Stop after - #{@stop_after} - must be a number")
55
+ end
56
+
57
+ if @page_size < 0
58
+ errors.push("Page size - #{@page_size} - must be a number")
59
+ end
60
+
61
+ if !@workspace.nil?
62
+ errors.push("Workspace - #{@workspace} - must have a ref") if @workspace["_ref"].nil?
63
+ end
64
+
65
+ if !@project.nil?
66
+ errors.push("Project - #{@project} - must have a ref") if @project["_ref"].nil?
67
+ end
68
+
69
+ if (allowed_objects[@type].nil?)
70
+ errors.push( "Object Type #{@type} is not query-able: inspect RallyRestJson.rally_objects for allowed types" )
71
+ end
72
+
73
+ errors
74
+ end
75
+
76
+ private
77
+
78
+ def parse_query_hash(query_hash)
79
+ @type = query_hash[:type]
80
+ @query_string = query_hash[:query_string]
81
+ @fetch = query_hash[:fetch]
82
+ @project_scope_down = query_hash[:project_scope_down]
83
+ @project_scope_up = query_hash[:project_scope_up]
84
+ @order = query_hash[:order]
85
+ @page_size = query_hash[:page_size]
86
+ @stop_after = query_hash[:limit]
87
+ @workspace = query_hash[:workspace]
88
+ @project = query_hash[:project]
89
+ end
90
+
91
+ end
92
+
93
+
94
+ end
@@ -0,0 +1,36 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+ module RallyAPI
6
+ class RallyQueryResult
7
+ include Enumerable
8
+
9
+ attr_accessor :results, :total_result_count
10
+
11
+ def initialize(rally_rest, json_results)
12
+ @results = json_results["QueryResult"]["Results"]
13
+ @total_result_count = json_results["QueryResult"]["TotalResultCount"]
14
+ @rally_rest = rally_rest
15
+ end
16
+
17
+ def each
18
+ @results.each do |result|
19
+ yield RallyObject.new(@rally_rest, result)
20
+ end
21
+ end
22
+
23
+ def [](index)
24
+ RallyObject.new(@rally_rest, @results[index])
25
+ end
26
+
27
+ def total_results
28
+ @total_result_count
29
+ end
30
+
31
+ def length
32
+ @results.length
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,300 @@
1
+ require_relative "custom_http_header"
2
+ require_relative "rally_json_connection"
3
+ require_relative "rally_object"
4
+ require_relative "rally_query"
5
+ require_relative "rally_query_result"
6
+
7
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
8
+ #Your use of this Software is governed by the terms and conditions
9
+ #of the applicable Subscription Agreement between your company and
10
+ #Rally Software Development Corp.
11
+ module RallyAPI
12
+
13
+ #this contstant is here - a tradeoff of speed vs completeness- right now speed wins because it is so
14
+ #expensive to query typedef and read all attributes for "OBJECT" or "COLLECTION" types
15
+ RALLY_REF_FIELDS = { "Subscription" => :subscription, "Workspace" => :workspace, "Project" => :project,
16
+ "Iteration" => :iteration, "Release" => :release, "WorkProduct" => :artifact,
17
+ "Requirement" => :hierarchicalrequirement, "Owner" => :user, "Tester" => :user,
18
+ "RevisionHistory" => :revisionhistory, "Revision" => :revision, "Revisions" => :revision,
19
+ "Blocker" => :artifact, "SubmittedBy" => :user, "TestCaseResult" => :testcaseresult,
20
+ "TestSet" => :testset, "Parent" => :hierarchicalrequirement, "TestFolder"=> :testfolder }
21
+
22
+
23
+
24
+ #Main Class to create from when using the tool
25
+ class RallyRestJson
26
+ DEFAULT_WSAPI_VERSION = "1.25"
27
+
28
+ attr_accessor :rally_url, :rally_user, :rally_password, :rally_workspace_name, :rally_project_name, :wsapi_version
29
+ attr_accessor :rally_headers, :rally_default_workspace, :rally_default_project, :low_debug, :proxy_info, :retries
30
+
31
+ attr_reader :rally_objects
32
+
33
+ def initialize(args)
34
+ @rally_url = args[:base_url] || "https://rally1.rallydev.com/slm"
35
+ @rally_user = args[:username]
36
+ @rally_password = args[:password]
37
+ @rally_workspace_name = args[:workspace]
38
+ @rally_project_name = args[:project]
39
+ @wsapi_version = args[:version] || DEFAULT_WSAPI_VERSION
40
+ @rally_headers = args[:headers] || CustomHttpHeader.new
41
+ @proxy_info = args[:proxy]
42
+ @retries = args[:retries] || 0
43
+
44
+ @low_debug = args[:debug] || false
45
+
46
+ @rally_connection = RallyJsonConnection.new(@rally_headers, @low_debug, @proxy_info)
47
+ if @retries > 0
48
+ @rally_connection.retries = @retries
49
+ end
50
+
51
+ @rally_objects = { :typedefinition => "TypeDefinition" }
52
+ cache_rally_objects()
53
+
54
+ if !@rally_workspace_name.nil?
55
+ @rally_default_workspace = find_workspace(@rally_workspace_name)
56
+ raise "unable to find default workspace #{@rally_workspace_name}" if @rally_default_workspace.nil?
57
+ end
58
+
59
+ if !@rally_project_name.nil?
60
+ @rally_default_project = find_project(@rally_default_workspace, @rally_project_name)
61
+ raise "unable to find default project #{@rally_project_name}" if @rally_default_project.nil?
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ def find_workspace(workspace_name)
68
+ sub = self.user.Subscription.read()
69
+ workspace = nil
70
+ sub.Workspaces.each do |ws|
71
+ ws.read
72
+ if (ws.Name == workspace_name) && (ws.State == "Open")
73
+ workspace = ws
74
+ break #doing a break for performance some customers have 100+ workspaces - no need to do the others
75
+ end
76
+ end
77
+ workspace
78
+ end
79
+
80
+ def find_project(workspace_object, project_name)
81
+ if workspace_object.nil?
82
+ raise "A workspace must be provided to find a project"
83
+ end
84
+
85
+ query = RallyQuery.new()
86
+ query.type = :project
87
+ query.query_string = "((Name = \"#{project_name}\") AND (State = \"Open\"))"
88
+ query.limit = 20
89
+ query.fetch = true
90
+ query.workspace = workspace_object
91
+
92
+ results = find(query)
93
+ return results.first if results.length > 0
94
+ nil
95
+ end
96
+
97
+ def user
98
+ args = {:user => @rally_user, :password => @rally_password}
99
+ json_response = @rally_connection.read_object(make_get_url(@rally_objects[:user]), args)
100
+ RallyObject.new(self, json_response)
101
+ end
102
+
103
+
104
+ def create(type, fields)
105
+ rally_type = check_type(type)
106
+
107
+ if (fields["Workspace"].nil? && fields["Project"].nil?)
108
+ fields["Workspace"] = @rally_default_workspace._ref
109
+ fields["Project"] = @rally_default_project._ref
110
+ end
111
+
112
+ args = {:user => @rally_user, :password => @rally_password }
113
+ object2create = { rally_type => make_ref_fields(fields) }
114
+ json_response = @rally_connection.create_object(make_create_url(rally_type), args, object2create)
115
+ RallyObject.new(self, json_response).read()
116
+ end
117
+
118
+
119
+ def read(type, obj_id)
120
+ rally_type = check_type(type)
121
+ ref = check_id(rally_type, obj_id)
122
+ args = {:user => @rally_user, :password => @rally_password}
123
+ json_response = @rally_connection.read_object(ref, args)
124
+ RallyObject.new(self, json_response)
125
+ end
126
+
127
+ def delete(ref_to_delete)
128
+ args = {:user => @rally_user, :password => @rally_password}
129
+ @rally_connection.delete_object(ref_to_delete, args)
130
+ end
131
+
132
+ def reread(json_object)
133
+ args = {:user => @rally_user, :password => @rally_password}
134
+ @rally_connection.read_object(json_object["_ref"], args)
135
+ end
136
+
137
+
138
+ def update(type, obj_id, fields)
139
+ rally_type = check_type(type)
140
+ ref = check_id(rally_type, obj_id)
141
+ json_update = { rally_type => make_ref_fields(fields) }
142
+ args = {:user => @rally_user, :password => @rally_password}
143
+ @rally_connection.update_object(ref, args, json_update)
144
+ RallyObject.new(self, reread({"_ref" => ref}))
145
+ end
146
+
147
+ #query_hash is a has like:
148
+ # query_hash = {}
149
+ # query_hash[:type] = Defect, Story, etc
150
+ # query_hash[:query_string] = "(State = \"Closed\")"
151
+ # query_hash[:fetch] = "Name,State,etc"
152
+ # query_hash[:workspace] = workspace json object or ref #defaults to workspace passed in RallyRestJson.new if nil
153
+ # query_hash[:project] = project json object or ref #defaults to project passed in RallyRestJson.new if nil
154
+ # query_hash[:project_scope_up] = true/false
155
+ # query_hash[:project_scope_down] = true/false
156
+ # query_hash[:order] = "ObjectID asc"
157
+ # query_hash[:pagesize]
158
+ # query_hash[:stopafter]
159
+ #todo default workspace and project if not set?
160
+ def find(query_obj)
161
+ errs = query_obj.validate(@rally_objects)
162
+ if errs.length > 0
163
+ raise "Errors making Rally Query: #{errs.to_s}"
164
+ end
165
+
166
+ query_url = make_query_url(@rally_url, @wsapi_version, check_type(query_obj.type))
167
+ query_params = query_obj.make_query_params
168
+ args = {:user => @rally_user, :password => @rally_password}
169
+ json_response = @rally_connection.get_all_json_results(query_url, args, query_params, query_obj.limit)
170
+ RallyQueryResult.new(self, json_response)
171
+ end
172
+
173
+ #rankAbove=%2Fhierarchicalrequirement%2F4624552599
174
+ #{"hierarchicalrequirement":{"_ref":"https://rally1.rallydev.com/slm/webservice/1.27/hierarchicalrequirement/4616818613.js"}}
175
+ def rank_above(ref_to_rank, relative_ref)
176
+ ref = ref_to_rank
177
+ params = {}
178
+ params[:rankAbove] = short_ref(relative_ref)
179
+ params[:fetch] = "true"
180
+ json_update = { get_type_from_ref(ref_to_rank) => {"_ref" => ref_to_rank} }
181
+ args = {:user => @rally_user, :password => @rally_password}
182
+ update = @rally_connection.put_object(ref, args, params, json_update)
183
+ RallyObject.new(self, update["Object"])
184
+ end
185
+
186
+ #ref to object.js? rankBelow=%2Fhierarchicalrequirement%2F4624552599
187
+ def rank_below(ref_to_rank, relative_ref)
188
+ ref = ref_to_rank
189
+ params = {}
190
+ params[:rankBelow] = short_ref(relative_ref)
191
+ params[:fetch] = "true"
192
+ json_update = { get_type_from_ref(ref_to_rank) => {"_ref" => ref_to_rank} }
193
+ args = {:user => @rally_user, :password => @rally_password}
194
+ update = @rally_connection.put_object(ref, args, params, json_update)
195
+ RallyObject.new(self, update["Object"])
196
+ end
197
+
198
+ private
199
+
200
+ def make_get_url(type)
201
+ "#{@rally_url}/webservice/#{@wsapi_version}/#{type}.js"
202
+ end
203
+
204
+ def make_read_url(type,oid)
205
+ "#{@rally_url}/webservice/#{@wsapi_version}/#{type}/#{oid}.js"
206
+ end
207
+
208
+ def make_create_url(type)
209
+ "#{@rally_url}/webservice/#{@wsapi_version}/#{type}/create.js"
210
+ end
211
+
212
+ def make_query_url(rally_url, wsapi_version, type)
213
+ "#{rally_url}/webservice/#{wsapi_version}/#{type}.js"
214
+ end
215
+
216
+ def short_ref(long_ref)
217
+ ref_pieces = long_ref.split("/")
218
+ "/#{ref_pieces[-2]}/#{ref_pieces[-1].split(".js")[0]}"
219
+ end
220
+
221
+ def check_type(type_name)
222
+ if @rally_objects[type_name].nil?
223
+ raise "The object type #{type_name} is not valid for the wsapi"
224
+ end
225
+ @rally_objects[type_name]
226
+ end
227
+
228
+ #ref should be like https://rally1.rallydev.com/slm/webservice/1.25/defect/12345.js
229
+ def has_ref?(json_object)
230
+ if json_object["_ref"].nil?
231
+ return false
232
+ end
233
+ return true if json_object["_ref"] =~ /^https:\/\/\S*(\/slm\/webservice)\S*.js$/
234
+ false
235
+ end
236
+
237
+ #expecting idstring to have "FormattedID|DE45" or the objectID
238
+ def check_id(type, idstring)
239
+ if idstring.class == Fixnum
240
+ return make_read_url(type, idstring)
241
+ end
242
+
243
+ if (idstring.class == String) && (idstring.index("FormattedID") == 0)
244
+ return ref_by_formatted_id(type, idstring.split("|")[1])
245
+ end
246
+ make_read_url(type, idstring)
247
+ end
248
+
249
+ def ref_by_formatted_id(type, fid)
250
+ query = RallyQuery.new()
251
+ query.type = type.downcase.to_sym
252
+ query.query_string = "(FormattedID = #{fid})"
253
+ query.limit = 20
254
+ query.fetch = "FormattedID"
255
+ query.workspace = @rally_default_workspace
256
+
257
+ results = find(query)
258
+ return results.first["_ref"] if results.length > 0
259
+ nil
260
+ end
261
+
262
+ #eg https://rally1.rallydev.com/slm/webservice/1.25/defect/12345.js
263
+ def get_type_from_ref(ref)
264
+ ref.split("/")[-2]
265
+ end
266
+
267
+ def make_ref_fields(fields)
268
+ fields.each do |key,val|
269
+ if (val.class == RallyObject)
270
+ fields[key] = val.getref
271
+ end
272
+ end
273
+ fields
274
+ end
275
+
276
+
277
+ def cache_rally_objects()
278
+ type_defs_query = RallyQuery.new()
279
+ type_defs_query.type = :typedefinition
280
+ type_defs_query.fetch = "Name"
281
+
282
+ type_defs = find(type_defs_query)
283
+ type_defs.each do |td|
284
+ type_sym = td.Name.downcase.gsub(" ", "").to_sym
285
+ @rally_objects[type_sym] = td.Name.gsub(" ","")
286
+ end
287
+
288
+ #some convenience keys to help people - someday we'll fix the api and make HR called story
289
+ @rally_objects[:artifact] = "Artifact"
290
+ @rally_objects[:persistableobject] = "PersistableObject"
291
+ @rally_objects[:useriterationcapacity] = "UserIterationCapacity"
292
+ @rally_objects[:userpermission] = "UserPermission"
293
+ @rally_objects[:story] = "HierarchicalRequirement"
294
+ @rally_objects[:userstory] = "HierarchicalRequirement"
295
+
296
+ end
297
+
298
+ end
299
+
300
+ end
@@ -0,0 +1,89 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+ module RallyAPI
6
+ # query is like:
7
+ # query_hash = {}
8
+ # query_hash[:type] = Defect, Story, etc
9
+ # query_hash[:query_string] = "(State = \"Closed\")"
10
+ # query_hash[:fetch] = "Name,State,etc"
11
+ # query_hash[:workspace] = workspace json object or ref #defaults to workspace passed in RallyRestJson.new if nil
12
+ # query_hash[:project] = project json object or ref #defaults to project passed in RallyRestJson.new if nil
13
+ # query_hash[:project_scope_up] = true/false
14
+ # query_hash[:project_scope_down] = true/false
15
+ # query_hash[:order] = "ObjectID asc"
16
+ # query_hash[:page_size]
17
+ # query_hash[:stopafter]
18
+
19
+ class TimeMachineQuery
20
+ attr_accessor :workspace
21
+
22
+ def initialize(query_hash = nil)
23
+ parse_query_hash(query_hash) if !query_hash.nil?
24
+ self
25
+ end
26
+
27
+ def make_query_params
28
+ query_params = {}
29
+ #query_params[:query] = @query_string
30
+ #query_params[:fetch] = @fetch
31
+ query_params[:workspace] = @workspace["_ref"] if !@workspace.nil?
32
+ #query_params[:project] = @project["_ref"] if !@project.nil?
33
+ #query_params[:projectScopeUp] = @project_scope_up
34
+ #query_params[:projectScopeDown] = @project_scope_down
35
+ #query_params[:order] = @order
36
+ #query_params[:pagesize] = @page_size
37
+
38
+ query_params
39
+ end
40
+
41
+ def validate(allowed_objects)
42
+ errors = []
43
+
44
+ if @type.nil?
45
+ errors.push("Object type for query cannot be nil")
46
+ end
47
+
48
+ if @limit < 0
49
+ errors.push("Stop after - #{@stop_after} - must be a number")
50
+ end
51
+
52
+ if @page_size < 0
53
+ errors.push("Page size - #{@page_size} - must be a number")
54
+ end
55
+
56
+ if !@workspace.nil?
57
+ errors.push("Workspace - #{@workspace} - must have a ref") if @workspace["_ref"].nil?
58
+ end
59
+
60
+ if !@project.nil?
61
+ errors.push("Project - #{@project} - must have a ref") if @project["_ref"].nil?
62
+ end
63
+
64
+ if (allowed_objects[@type].nil?)
65
+ errors.push( "Object Type #{@type} is not query-able: inspect RallyRestJson.rally_objects for allowed types" )
66
+ end
67
+
68
+ errors
69
+ end
70
+
71
+ private
72
+
73
+ def parse_query_hash(query_hash)
74
+ #@type = query_hash[:type]
75
+ #@query_string = query_hash[:query_string]
76
+ #@fetch = query_hash[:fetch]
77
+ #@project_scope_down = query_hash[:project_scope_down]
78
+ #@project_scope_up = query_hash[:project_scope_up]
79
+ #@order = query_hash[:order]
80
+ #@page_size = query_hash[:page_size]
81
+ #@stop_after = query_hash[:limit]
82
+ @workspace = query_hash[:workspace]
83
+ #@project = query_hash[:project]
84
+ end
85
+
86
+ end
87
+
88
+
89
+ end
@@ -0,0 +1,7 @@
1
+ #Copyright (c) 2002-2012 Rally Software Development Corp. All Rights Reserved.
2
+ #Your use of this Software is governed by the terms and conditions
3
+ #of the applicable Subscription Agreement between your company and
4
+ #Rally Software Development Corp.
5
+ module RallyAPI
6
+ VERSION = "0.4.0"
7
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rally_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dave Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-07 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: &70332191860000 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70332191860000
25
+ description: API wrapper for Rally's JSON REST web services api
26
+ email:
27
+ - dsmith@rallydev.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - README.rdoc
33
+ - Rakefile
34
+ - lib/rally_api/custom_http_header.rb
35
+ - lib/rally_api/rally_json_connection.rb
36
+ - lib/rally_api/rally_object.rb
37
+ - lib/rally_api/rally_query.rb
38
+ - lib/rally_api/rally_query_result.rb
39
+ - lib/rally_api/rally_rest_json.rb
40
+ - lib/rally_api/time_machine_query.rb
41
+ - lib/rally_api/version.rb
42
+ - lib/rally_api.rb
43
+ homepage: http://developer.rallydev.com/help
44
+ licenses: []
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project: rally_api
63
+ rubygems_version: 1.8.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: A wrapper for the Rally Web Services API using json
67
+ test_files: []