rally_api 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []