rally_rest_api 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.txt ADDED
@@ -0,0 +1,4 @@
1
+ 2006-11-27 Bob Cotton <bcotton@england.f4tech.com>
2
+
3
+ * lib/rally_rest_api/query.rb: Added docs.
4
+
data/History.txt ADDED
File without changes
data/Manifest.txt ADDED
@@ -0,0 +1,19 @@
1
+ CHANGELOG.txt
2
+ History.txt
3
+ lib/rally_rest_api/query.rb
4
+ lib/rally_rest_api/query_result.rb
5
+ lib/rally_rest_api/rally_rest.rb
6
+ lib/rally_rest_api.rb
7
+ lib/rally_rest_api/rest_builder.rb
8
+ lib/rally_rest_api/rest_object.rb
9
+ lib/rally_rest_api/typedef.rb
10
+ lib/rally_rest_api/version.rb
11
+ Manifest.txt
12
+ Rakefile
13
+ README.txt
14
+ setup.rb
15
+ test/tc_query_result.rb
16
+ test/tc_rest_api.rb
17
+ test/tc_rest_object.rb
18
+ test/tc_rest_query.rb
19
+ test/test_helper.rb
data/README.txt ADDED
@@ -0,0 +1,111 @@
1
+ rally-rest-api -- A Ruby-ized interface to Rally's REST webservice API
2
+
3
+ ==Introduction:
4
+ Rally Software Development's on-demand agile software life-cycle management services offers webservices API's for its customers. The API comes in both SOAP and REST style interfaces. This library is for accessing the REST API using Ruby. For more information about Rally's webservice APIs see https://rally1.rallydev.com/slm/doc/webservice/index.jsp.
5
+
6
+ This API provides full access to all CRUD operations and an rich interface to the query facility. An Enumerable interface is provided for the paginated query results.
7
+
8
+ == Rationale (i.e. Why not SOAP?):
9
+ Your subscription in Rally can be partitioned into several isolated "Workspaces", where the only thing shared between workspaces are your users. Any custom attributes you create will be specific to each workspace. When using the SOAP interface, the WSDL generated is specific to the workspace you are working in. Therefore the name-space (e.g. package in Java) will be different for each workspace you are working with.
10
+
11
+ Because REST webservices do not have WSDL (the XML schema is available for each workspace), there is no per-workspace interface. Combined with the dynamic nature of this API, you don't need to code to different Ruby namespaces when you are working with multiple workspaces. You will however, need to be aware of the workspaces your objects are in when working with multiple workspaces.
12
+
13
+ == Getting Started:
14
+ RallyRestAPI is the entry point to the api. Each instance corresponds to one user logged into Rally. There are several options that may be passed to the constructor:
15
+ :username => Your Rally login username.
16
+ :password => Your Rally login password. Username and password will be remembered by this
17
+ instance of the API and all objects created and read by this instance.
18
+ :base_url => The base url for the system you are talking to. Defaults to https://rally1.rallydev.com/slm/
19
+ :raise_on_warning => true|false or the exception class you would like raised. If true, RuntimeError will be raised.
20
+ :logger => A logger to log to. There is interesting logging info for DEBUG and INFO
21
+
22
+ == Rest Object:
23
+ All rally resources referenced by the api are of type RestObject, there are no subclasses. In its initial form a RestObject is just a URL representing a resource. This URL is accessed using RestObject#ref. When more information is requested about the object, the API will read the content of that resource. This read is done lazily and transparently.
24
+
25
+ Objects can reference other objects in Rally. When establishing these relationships using this API, they are done using RestObjects. For example, to associate a user story to a defect, you would use the 'requirement' association on defect to reference the User Story. Here 'defect' and 'user_story' already exist, and the variables contain RestObjects representing them:
26
+
27
+ defect.update(:requirement => user_story)
28
+
29
+ == CRUD and Query:
30
+
31
+ Given an instance of the RallyRestAPI:
32
+
33
+ rally = RallyRestAPI.new(:username => <username>,
34
+ :password => <password>)
35
+
36
+ === Create:
37
+
38
+ RallyRestAPI#create(<rally artifact type>, <artifact attributes as a hash>) returns a RestObject:
39
+
40
+ defect = rally.create(:defect, :name => "Defect name")
41
+
42
+ #create will also accept a block, and yield the newly created reference to the block
43
+
44
+ rally.create(:defect, :name => "Defect name") do |defect|
45
+ # do something with defect here
46
+ end
47
+
48
+ The block form is useful for creating relationships between objects in a readable way. For example, to create a User Story (represented by the type HierarchicalRequirement) with a task:
49
+
50
+ rally.create(:hierarchical_requirement, :name => "User Story One", :iteration => iteration_one) do |user_story|
51
+ rally.create(:task, :name => "Task One", :work_product => user_story)
52
+ end
53
+
54
+ The above example will create a UserStory, pass it to the block, then create a Task on that User Story using the task's 'WorkProduct' relationship.
55
+
56
+ === Read:
57
+ As mentioned above, RestObject will lazy read themselves on demand. If you need to force a RestObject to re-read itself, call RestObject#refresh.
58
+
59
+ === Update:
60
+ There are two ways to update an object:
61
+
62
+ RallyRestAPI#update(<rest object>, <attributes>)
63
+ RestObject#update(<attributes>)
64
+
65
+ which is to say, a RestObject can update itself
66
+
67
+ defect.update(:name => "new name")
68
+
69
+ Or the rest api can update it:
70
+
71
+ rally.update(defect, :name => "new name")
72
+
73
+ === Delete:
74
+ There are two ways to delete an object:
75
+
76
+ RallyRestAPI#delete(<rest object>)
77
+ RestObject#delete
78
+
79
+ which is to say, a RestObject can delete itself
80
+
81
+ defect.delete
82
+
83
+ Or the rest api can delete it:
84
+
85
+ rally.delete(defect)
86
+
87
+ === Query:
88
+ RallyRestAPI#find is the interface to the query syntax of Rally's webservice APIs. The query interface in Ruby provides full support for this query syntax including all the operators. A quick example:
89
+
90
+ query_result = rally.find(:defect) { equal :name, "Defect name" }
91
+
92
+ In addition to the type, #find accepts other arguments as a hash:
93
+
94
+ :pagesize => <number> - The number of results per page. Max of 100
95
+ :start => <number> - The record number to start with. Assuming more then page size records.
96
+ :fetch => <boolean> - If this is set to true then entire objects will be returned inside the query result. If set to false (the default) then only object references will be returned.
97
+ :workspace - If not present, then the query will run in the user's default workspace. If present, this should be the RestObject containing the workspace the user wants to search in.
98
+ :project - If not set, or specified as "null" then the "parent project" in the given workspace is used. If set, this should be the RestObject containing the project. Furthermore, if set you may omit the workspace parameter because the workspace will be inherited from the project.
99
+ :project_scope_up - Default is true. In addition to the specified project, include projects above the specified one.
100
+ :project_scope_down - Default is true. In addition to the specified project, include child projects below the current one.
101
+
102
+ The return from #find is always a QueryResult. The QueryResult provides an interface to the paginated query result.
103
+
104
+ #each will iterate all results on all pages.
105
+ #total_result_count is the number of results for the whole query.
106
+ #page_length is the number of elements in the current page of result.
107
+ #results returns an Array for the current page of results.
108
+
109
+ Because of the paginated nature of the result list, deleting elements while using #each is undefined.
110
+
111
+ See the rdoc for RestQuery for more query examples.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'rally_rest_api', 'version')
13
+
14
+ AUTHOR = "Bob Cotton" # can also be an array of Authors
15
+ EMAIL = "bob.cotton@ralldev.com"
16
+ DESCRIPTION = "A ruby-ized interface to Rally's REST webservices API"
17
+ GEM_NAME = "rally_rest_api" # what ppl will type to install your gem
18
+ RUBYFORGE_PROJECT = "rally-rest-api" # The unix name for your project
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip
21
+
22
+
23
+ NAME = "rally_rest_api"
24
+ REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
25
+ VERS = ENV['VERSION'] || (RallyRestAPI::VERSION::STRING + (REV ? ".#{REV}" : ""))
26
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
27
+ RDOC_OPTS = ['--quiet', '--title', "rally_rest_api documentation",
28
+ "--opname", "index.html",
29
+ "--line-numbers",
30
+ "--main", "README",
31
+ "--inline-source"]
32
+
33
+ # Generate all the Rake tasks
34
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
35
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
36
+ p.author = AUTHOR
37
+ p.description = DESCRIPTION
38
+ p.email = EMAIL
39
+ p.summary = DESCRIPTION
40
+ p.url = HOMEPATH
41
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
42
+ p.test_globs = ["test/**/tc_*.rb"]
43
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
44
+ p.extra_deps = ["builder"]
45
+
46
+ # == Optional
47
+ #p.changes - A description of the release's latest changes.
48
+
49
+ #p.spec_extras - A hash of extra values to set in the gemspec.
50
+ end
51
+
@@ -0,0 +1 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'rally_rest_api/**/*.rb')].sort.each { |lib| require lib }
@@ -0,0 +1,170 @@
1
+ require 'uri'
2
+
3
+ class String # :nodoc: all
4
+ def to_camel
5
+ self.split(/\./).map { |word| word.split(/_/).map { |word| word.capitalize }.join}.join('.')
6
+ end
7
+
8
+ alias :to_q :to_s
9
+ end
10
+
11
+ class Symbol # :nodoc: all
12
+ def to_q
13
+ self.to_s.to_camel
14
+ end
15
+ end
16
+
17
+ # == Generate a query string for Rally's webservice query interface.
18
+ # Arguments are:
19
+ # type - the type to query for
20
+ # args - arguments to the query. Supported values are
21
+ # :pagesize => <number> - The number of results per page. Max of 100
22
+ # :start => <number> - The record number to start with. Assuming more then page size records.
23
+ # :fetch => <boolean> - If this is set to true then entire objects will be returned inside the query result. If set to false (the default) then only object references will be returned.
24
+ # :workspace - If not present, then the query will run in the user's default workspace. If present, this should be the RestObject containing the workspace the user wants to search in.
25
+ # :project - If not set, or specified as "null" then the "parent project" in the given workspace is used. If set, this should be the RestObject containing the project. Furthermore, if set you may omit the workspace parameter because the workspace will be inherited from the project.
26
+ # :project_scope_up - Default is true. In addition to the specified project, include projects above the specified one.
27
+ # :project_scope_down - Default is true. In addition to the specified project, include child projects below the current one.
28
+ # &block - the query parameters
29
+ #
30
+ # === The query parameters block
31
+ #
32
+ # The query parameters block is a DSL the specifying the query parameters. Single attribute specifiers are
33
+ # written in prefix notation in the form:
34
+ # <operator> <attribute symbol>, <value>
35
+ # for example
36
+ # equal :name, "My Name"
37
+ # Allowed operators and their corresponding generated query strings are:
38
+ # equal => "="
39
+ # not_equal => "!="
40
+ # contains => "contains"
41
+ # greater_than => ">"
42
+ # gt => ">"
43
+ # less_than => "<"
44
+ # lt => "<"
45
+ # greater_than_equal => ">="
46
+ # gte => ">="
47
+ # less_then_equal => "<="
48
+ # lte => "<="
49
+ #
50
+ # == Boolean logic.
51
+ #
52
+ # By default, if more then one query parameter is specified in the block, then those parameters will be ANDed together.
53
+ # For example, if the query parameter block contains the follow expression:
54
+ # equal :name, "My Name"
55
+ # greater_than :priority, "Fix Immediately"
56
+ # these expressions will be ANDed together. You may specify explicit AND and OR operators using the
57
+ # _and_ and _or_ operators, which also accept parameter blocks. For example the above expression could also have
58
+ # been written:
59
+ # _and_ {
60
+ # equal :name, "My Name"
61
+ # greater_than :priority, "Fix Immediately"
62
+ # }
63
+ # \_or_ works in the same fashion. _and_s and _or_s may be nested as needed. See the test cases for RestQuery for
64
+ # more complex examples
65
+ #
66
+ class RestQuery
67
+ attr_reader :type
68
+
69
+ def initialize(type, *args, &block)
70
+ @type = type
71
+ @query_string = "query=" << URI.escape(QueryBuilder.new("and", &block).to_q) if block_given?
72
+ @query_params = process_args(args[0])
73
+ end
74
+
75
+ def process_args(args) # :nodoc"
76
+ return if args.nil?
77
+ query_string = ""
78
+ args.each do |key, value|
79
+ case key
80
+ when :order
81
+ # this is a hack, we need a better way to express descending
82
+ value = [value].flatten.map { |e| e.to_s.to_camel }.join(", ").gsub(", Desc", " desc")
83
+ end
84
+ query_string << "&#{key}=#{URI.escape(value.to_s)}"
85
+ end
86
+ query_string
87
+ end
88
+
89
+ def next_page(args)
90
+ @query_params = process_args(args)
91
+ self
92
+ end
93
+
94
+ def to_q
95
+ "#{@query_string}#{@query_params}"
96
+ end
97
+
98
+ def self.query(&block)
99
+ QueryBuilder.new("and", &block).to_q
100
+ end
101
+ end
102
+
103
+ # Internal support for generating query string for the Rally Webservice.
104
+ # See RestQuery for examples.
105
+ class QueryBuilder # :nodoc: all
106
+ attr_reader :operator
107
+
108
+ # Define the operators on the query terms. I've include perl-like operators for
109
+ # less_than and greater_then etc.
110
+ {
111
+ :equal => "=",
112
+ :not_equal => "!=",
113
+ :contains => "contains",
114
+ :greater_than => ">",
115
+ :gt => ">",
116
+ :less_than => "<",
117
+ :lt => "<",
118
+ :greater_than_equal => ">=",
119
+ :gte => ">=",
120
+ :less_then_equal => "<=",
121
+ :lte => "<=",
122
+ }.each do |method, operator|
123
+ module_eval %{def #{method}(lval, rval)
124
+ rval = \"\\"\#{rval}\\"\" if rval =~ / /
125
+ add(QueryString.new(lval, \"#{operator}\", rval), @operator)
126
+ end}
127
+ end
128
+
129
+
130
+ def initialize(operator, &block)
131
+ @operator = operator
132
+ instance_eval(&block)
133
+ end
134
+
135
+ def _and_(&block)
136
+ add(QueryBuilder.new("and", &block), @operator)
137
+ end
138
+
139
+ def _or_(&block)
140
+ add(QueryBuilder.new("or", &block), @operator)
141
+ end
142
+
143
+ def add(new_value, op)
144
+ if value.empty?
145
+ value.push new_value
146
+ else
147
+ value.push QueryString.new(value.pop, op, new_value)
148
+ end
149
+ self
150
+ end
151
+
152
+ def value
153
+ @value ||= []
154
+ end
155
+
156
+ def to_q
157
+ value[0].to_q
158
+ end
159
+
160
+ end
161
+
162
+ class QueryString # :nodoc: all
163
+ def initialize(lhs, op, rhs)
164
+ @lhs, @op, @rhs = lhs, op, rhs
165
+ end
166
+
167
+ def to_q
168
+ "(#{@lhs.to_q} #{@op.to_q} #{@rhs.to_q})"
169
+ end
170
+ end
@@ -0,0 +1,73 @@
1
+ require 'rally_rest_api/rest_object'
2
+
3
+ # == An interface to the paged query result
4
+ #
5
+ # QueryResult is a wrapper around the xml returned from a webservice
6
+ # query operation. A query could result in a large number of hits
7
+ # being returned, therefore the query result is paged into page_size
8
+ # chunks (20 by default). QueryResult will seamlessly deal with the
9
+ # paging when using the #each iterator.
10
+ #
11
+ # === Example
12
+ # rally = RallyRestAPI.new(...)
13
+ # results = rally.find(:defect) { equal :name, "My Defects" }
14
+ # results.each do |defect|
15
+ # defect.update(:state => "Closed")
16
+ # end
17
+ #
18
+ #
19
+ class QueryResult < RestObject
20
+ attr_reader :total_result_count
21
+ attr_reader :page_size
22
+ attr_reader :start_index
23
+
24
+ def initialize(query, rally_rest, document_content)
25
+ super(rally_rest, document_content)
26
+ elements[:results] = case self.results
27
+ when Array : self.results.flatten
28
+ when Hash : self.results.values.flatten
29
+ when nil : []
30
+ end
31
+
32
+ @query = query
33
+
34
+ @total_result_count = elements[:total_result_count].to_i
35
+ @page_size = elements[:page_size].to_i
36
+ @start_index = elements[:start_index].to_i
37
+ end
38
+
39
+ # fetch the next page of results. Uses the original query to generate the query string.
40
+ def next_page
41
+ @rally_rest.query(@query.next_page(:start => self.start_index + self.page_size,
42
+ :pagesize => self.page_size))
43
+ end
44
+
45
+ # Iteration all pages of the result
46
+ def each
47
+ current_result = self
48
+ while current_result.more_pages?
49
+ current_result.elements[:results].each do |result|
50
+ # The collection of refs we are holding onto could grow without bounds, so dup
51
+ # the ref
52
+ yield result.dup
53
+ end
54
+ current_result = current_result.next_page
55
+ end
56
+ end
57
+
58
+ # return the first element. Useful for queries that return only one result
59
+ def first
60
+ results.first
61
+ end
62
+
63
+ # The length of the current page of results
64
+ def page_length
65
+ return 0 if self.elements[:results].nil?
66
+ self.elements[:results].length
67
+ end
68
+
69
+ # Are there more pages?
70
+ def more_pages?
71
+ (self.start_index + self.page_length) < self.total_result_count
72
+ end
73
+ end
@@ -0,0 +1,100 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'rexml/document'
4
+ require 'ostruct'
5
+
6
+ require 'rally_rest_api/rest_builder'
7
+ require 'rally_rest_api/query'
8
+ require 'rally_rest_api/rest_object'
9
+ require 'rally_rest_api/query_result'
10
+
11
+ #
12
+ # RallyRestAPI - A Ruby-ized interface to Rally's REST webservice API
13
+ #
14
+ class RallyRestAPI
15
+ include RestBuilder
16
+
17
+ attr_reader :username, :password, :base_url, :raise_on_warning, :logger
18
+
19
+ ALLOWED_TYPES = %w[subscription workspace project iteration release defect defect_suite test_case
20
+ feature supplemental_requirement use_case story actor card
21
+ program task hierarchical_requirement test_case_result test_case_step]
22
+
23
+ # new - Create an instance of RallyRestAPI. Each instance corresponds to one named user.
24
+ #
25
+ # options (as a Hash):
26
+ # * username - The Rally username
27
+ # * password - The password for the named user
28
+ # * base_url - The base url of the system. Defaults to https://rally1.rallydev.com/slm
29
+ # * raise_on_warn - true|false|Exception Class. If you want exceptions raised on warnings. Default is false. if 'true' RuntimeException will be raised. If ExceptionClass, then an instance of that will be raised.
30
+ # * logger - a Logger to log to.
31
+ #
32
+ def initialize(options = {:base_url => "https://rally1.rallydev.com/slm"})
33
+ parse_options options
34
+ end
35
+
36
+ def parse_options(options)
37
+ @username = options[:username]
38
+ @password = options[:password]
39
+ @base_url = options[:base_url]
40
+ @raise_on_warning = options[:raise_on_warning]
41
+ @logger = options[:logger]
42
+ end
43
+
44
+ # return an instance of User, for the currently logged in user.
45
+ def user
46
+ RestObject.new(self, read_rest("#{@base_url}/webservice/1.0/user", @username, @password))
47
+ end
48
+ alias :start :user # :nodoc:
49
+
50
+ # This is deprecated, use create instead
51
+ def method_missing(type, args, &block) # :nodoc:
52
+ # raise "'#{type}' is not a supported type. Supported types are: #{ALLOWED_TYPES.inspect}" unless ALLOWED_TYPES.include?(type.to_s)
53
+ create_rest(type, args, @username, @password)
54
+ end
55
+
56
+ # Create an object.
57
+ # type - The type to create, as a symbol (e.g. :test_case)
58
+ # values - The attributes of the new object.
59
+ #
60
+ # The created instance will be passed to the block
61
+ #
62
+ # returns the created object as a RestObject.
63
+ def create(type, values) # :yields: new_object
64
+ # raise "'#{type}' is not a supported type. Supported types are: #{ALLOWED_TYPES.inspect}" unless ALLOWED_TYPES.include?(type.to_s)
65
+ object = create_rest(type, values, @username, @password)
66
+ yield object if block_given?
67
+ object
68
+ end
69
+
70
+ # Query Rally for a collection of objects
71
+ # Example :
72
+ # rally.find(:artifact, :page_size => 20, :start_index => 20) { equal :name, "name" }
73
+ # See RestQuery for more info.
74
+ def find(type, *args, &query)
75
+ # pass the args to RestQuery, make it generate the string and handle generating the query for the
76
+ # next page etc.
77
+ query = RestQuery.new(type, args, &query)
78
+ query(query)
79
+ end
80
+
81
+ # update - update an object
82
+ def update(rest_object, attributes)
83
+ rest_object.update(attributes)
84
+ end
85
+
86
+ # delete an object
87
+ def delete(rest_object)
88
+ rest_object.delete
89
+ end
90
+
91
+ private
92
+ def query(query)
93
+ query_url = "#{@base_url}/webservice/1.0/#{query.type.to_s.to_camel}?" << query.to_q
94
+ xml = read_rest(query_url, @username, @password)
95
+ QueryResult.new(query, self, xml)
96
+ end
97
+
98
+ end
99
+
100
+