ruby_tfs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.md +7 -0
- data/README.md +55 -0
- data/Rakefile +8 -0
- data/lib/tfs/builds.rb +29 -0
- data/lib/tfs/changesets.rb +13 -0
- data/lib/tfs/class_helpers.rb +19 -0
- data/lib/tfs/client.rb +53 -0
- data/lib/tfs/configuration.rb +78 -0
- data/lib/tfs/projects.rb +19 -0
- data/lib/tfs/query_engine.rb +103 -0
- data/lib/tfs/queryable.rb +42 -0
- data/lib/tfs/work_items.rb +42 -0
- data/lib/tfs.rb +30 -0
- data/ruby_tfs.gemspec +23 -0
- data/spec/fixtures/cassettes/builds.yml +405 -0
- data/spec/fixtures/cassettes/changeset_queries.yml +374 -0
- data/spec/fixtures/cassettes/changesets.yml +237 -0
- data/spec/fixtures/cassettes/project_workitems_queries.yml +793 -0
- data/spec/fixtures/cassettes/projects.yml +210 -0
- data/spec/fixtures/cassettes/workitems.yml +2037 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/tfs/builds_spec.rb +34 -0
- data/spec/tfs/changesets_spec.rb +34 -0
- data/spec/tfs/client_spec.rb +62 -0
- data/spec/tfs/projects_spec.rb +35 -0
- data/spec/tfs/query_engine_spec.rb +37 -0
- data/spec/tfs/work_items_spec.rb +68 -0
- data/spec/tfs_spec.rb +25 -0
- metadata +123 -0
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2013 Luke van der Hoeven
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
4
|
+
|
5
|
+
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
6
|
+
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# ruby_tfs
|
2
|
+
TFS.rb is a wapper around the odata API for TFS
|
3
|
+
|
4
|
+
- [The project](http://www.microsoft.com/en-us/download/details.aspx?id=36230)
|
5
|
+
- [The specification][1]
|
6
|
+
|
7
|
+
## Disclaimer
|
8
|
+
This wrapper is mostly a shell around the `ruby_odata` gem to provide a "kinder"api specific to TFS and the capabilities within. If you want to get access beyond what this api provides, you can simply access the `TFS::Client.connection` object and run direct queries against odata, though if you want that kind of flexibility, it's probably better just got straight to the `ruby_odata` gem and skip this wrapper.
|
9
|
+
|
10
|
+
## API
|
11
|
+
|
12
|
+
The [TFS OData api][1] supports the following object types:
|
13
|
+
|
14
|
+
- Builds
|
15
|
+
- Build Definitions
|
16
|
+
- Changesets
|
17
|
+
- Changes
|
18
|
+
- Branches
|
19
|
+
- WorkItems
|
20
|
+
- Attachments
|
21
|
+
- Links
|
22
|
+
- Projects
|
23
|
+
- Queries
|
24
|
+
- AreaPaths
|
25
|
+
- IterationPaths
|
26
|
+
|
27
|
+
Currently, we support the following (due to my own purposes) with plans to further support the rest as well:
|
28
|
+
|
29
|
+
- Builds
|
30
|
+
- Changesets
|
31
|
+
- WorkItems
|
32
|
+
- Projects
|
33
|
+
- WorkItems
|
34
|
+
|
35
|
+
### Querying
|
36
|
+
|
37
|
+
All queries require a call to `#run` to finalize the query. This also makes it possible in most cases where you are defining a query to instead use `#to_query` to see the actual url-based query that will be run.
|
38
|
+
|
39
|
+
## Notes
|
40
|
+
While the api for `ruby_tfs` looks similar to Rails' `ActiveRecord` api, it is not meant to be an exact translation. The base type objects (`TFS::Builds`, `TFS::Projects`, etc) are setup to follow more of the [Repository pattern](http://martinfowler.com/eaaCatalog/repository.html) rather than an ORM-like pattern. The objects returned from the repository are actually from the [`ruby_odata`][2] library, which is a core dependency of this project. The odata lib does a great job of representing the OData… data, so I felt no need to re-wrap in a secondary set of layers. This opionon may change depending on the direction of the [`ruby_data`][2] project.
|
41
|
+
|
42
|
+
## Plans
|
43
|
+
The query engine currently works against the actual [OData api](http://www.odata.org/documentation/uri-conventions#QueryStringOptions). Eventually it'd be great to have a more "Ruby Way™" of doing this by doing some sort of query compilation between a Ruby DSL and the OData api. That will come in time.
|
44
|
+
|
45
|
+
## Credits
|
46
|
+
- Thanks to Damien White ([visoft](https://github.com/visoft)) for his [`ruby_odata`][2] wrapper. It made this project very painless to write.
|
47
|
+
- Thanks to Microsoft for allowing an OData wrapper to exist for TFS. It makes writing third-party, non .NET apps much more fun. Go open source!
|
48
|
+
|
49
|
+
## License
|
50
|
+
Apache v2
|
51
|
+
|
52
|
+
See the LICENSE.md file for more details.
|
53
|
+
|
54
|
+
[1]: https://tfsodata.visualstudio.com/
|
55
|
+
[2]: https://github.com/visoft/ruby_odata
|
data/Rakefile
ADDED
data/lib/tfs/builds.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'tfs/work_items'
|
2
|
+
require 'tfs/changesets'
|
3
|
+
|
4
|
+
module TFS
|
5
|
+
class Builds < Queryable
|
6
|
+
add_child TFS::WorkItems
|
7
|
+
add_child TFS::Changesets
|
8
|
+
|
9
|
+
STATES = %w(All
|
10
|
+
Failed
|
11
|
+
InProgress
|
12
|
+
None
|
13
|
+
NotStarted
|
14
|
+
PartiallySucceeded
|
15
|
+
Stopped
|
16
|
+
Succeeded)
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# To do an explicit build find, the API requires the definiton the build is from,
|
20
|
+
# the project it exists within, and the build number.
|
21
|
+
#
|
22
|
+
# TFS::Builds.find("DevBuild", "My New Project", 'DevBuild.3')
|
23
|
+
#
|
24
|
+
def find(definition, project, number)
|
25
|
+
TFS.builds("Definition='#{definition}',Project='#{project}',Number='#{number}'").run.first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module TFS
|
2
|
+
module ClassHelpers
|
3
|
+
def base_class(for_class=self)
|
4
|
+
name = (Class === for_class) ? for_class.name : for_class
|
5
|
+
name.split("::").last
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_name_from_class(name=self.name)
|
9
|
+
base_class(name).downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
SPECIAL_CASES = { workitems: "WorkItems" }
|
13
|
+
|
14
|
+
def odata_class_from_method_name(method_name)
|
15
|
+
return SPECIAL_CASES[method_name] if SPECIAL_CASES.has_key? method_name
|
16
|
+
method_name.to_s.capitalize
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/tfs/client.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'ruby_odata'
|
2
|
+
require 'tfs/configuration'
|
3
|
+
|
4
|
+
require 'tfs/class_helpers'
|
5
|
+
require 'tfs/query_engine'
|
6
|
+
|
7
|
+
module TFS
|
8
|
+
class Client
|
9
|
+
include TFS::Configuration
|
10
|
+
extend TFS::ClassHelpers
|
11
|
+
|
12
|
+
# Options specific to the provider (odata in this case)
|
13
|
+
PROVIDER_OPTIONS = [:username, :password, :verify_ssl]
|
14
|
+
|
15
|
+
attr_reader :connection, :endpoint
|
16
|
+
|
17
|
+
# Creates an instance of the client
|
18
|
+
def initialize(options={})
|
19
|
+
TFS::Configuration.keys.each do |key|
|
20
|
+
instance_variable_set(:"@#{key}", options[key] || TFS.instance_variable_get(:"@#{key}"))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates the connection to the data provider source
|
25
|
+
def connect
|
26
|
+
@connection = @provider.new endpoint, opts_for_connection
|
27
|
+
end
|
28
|
+
|
29
|
+
[TFS::Builds, TFS::Changesets, TFS::Projects, TFS::WorkItems].each do |klass|
|
30
|
+
define_method(base_class(klass).downcase) do |*params|
|
31
|
+
TFS::QueryEngine.new(klass, @connection, params)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run
|
36
|
+
@connection.execute
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(method_name, *args, &block)
|
40
|
+
return super unless @connection.respond_to? method_name
|
41
|
+
@connection.send(method_name, *args, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def opts_for_connection
|
47
|
+
{
|
48
|
+
username: @username,
|
49
|
+
password: @password
|
50
|
+
}.merge connection_options
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Lots of these ideas were stolen from the spectacular
|
2
|
+
# [sferik/twitter gem](https://github.com/sferik/twitter) gem
|
3
|
+
module TFS
|
4
|
+
module Configuration
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_writer :username, :password
|
8
|
+
attr_accessor :endpoint, :connection_options, :provider
|
9
|
+
|
10
|
+
def_delegator :options, :hash
|
11
|
+
|
12
|
+
CONNECTION_OPTIONS = {
|
13
|
+
:headers => {
|
14
|
+
:accept => 'application/json',
|
15
|
+
:user_agent => "TFS Ruby Gem",
|
16
|
+
},
|
17
|
+
:request => {
|
18
|
+
:open_timeout => 5,
|
19
|
+
:timeout => 10,
|
20
|
+
},
|
21
|
+
:verify_ssl => false
|
22
|
+
} unless defined? TFS::Configuration::CONNECTION_OPTIONS
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def keys
|
26
|
+
@keys ||= [
|
27
|
+
:username,
|
28
|
+
:password,
|
29
|
+
:endpoint,
|
30
|
+
:connection_options,
|
31
|
+
:provider
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def connection_options
|
36
|
+
CONNECTION_OPTIONS
|
37
|
+
end
|
38
|
+
|
39
|
+
def username
|
40
|
+
ENV['TFS_USERNAME']
|
41
|
+
end
|
42
|
+
|
43
|
+
def password
|
44
|
+
ENV['TFS_PASSWORD']
|
45
|
+
end
|
46
|
+
|
47
|
+
def endpoint
|
48
|
+
ENV['TFS_ENDPOINT']
|
49
|
+
end
|
50
|
+
|
51
|
+
def provider
|
52
|
+
OData::Service
|
53
|
+
end
|
54
|
+
|
55
|
+
def options
|
56
|
+
Hash[Configuration.keys.map{|key| [key, send(key)]}]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure
|
61
|
+
yield self
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def reset!
|
66
|
+
TFS::Configuration.keys.each do |key|
|
67
|
+
instance_variable_set(:"@#{key}", Configuration.options[key])
|
68
|
+
end
|
69
|
+
self
|
70
|
+
end
|
71
|
+
alias setup reset!
|
72
|
+
|
73
|
+
private
|
74
|
+
def options
|
75
|
+
Hash[TFS::Configuration.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/tfs/projects.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'tfs/work_items'
|
2
|
+
|
3
|
+
module TFS
|
4
|
+
class Projects < Queryable
|
5
|
+
add_child TFS::WorkItems
|
6
|
+
add_child TFS::Builds
|
7
|
+
add_child TFS::Changesets
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Projects can be found by name alone
|
11
|
+
#
|
12
|
+
# TFS::Projects.find("BFG")
|
13
|
+
#
|
14
|
+
def find(name)
|
15
|
+
TFS.projects(name).run.first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'tfs/queryable'
|
2
|
+
require 'tfs/builds'
|
3
|
+
require 'tfs/changesets'
|
4
|
+
require 'tfs/projects'
|
5
|
+
|
6
|
+
require 'tfs/work_items'
|
7
|
+
|
8
|
+
module TFS
|
9
|
+
class QueryEngine
|
10
|
+
extend Forwardable
|
11
|
+
include TFS::ClassHelpers
|
12
|
+
|
13
|
+
attr_reader :type
|
14
|
+
|
15
|
+
VALID_CLASSES = [
|
16
|
+
TFS::Builds,
|
17
|
+
TFS::Changesets,
|
18
|
+
TFS::Projects,
|
19
|
+
TFS::WorkItems
|
20
|
+
]
|
21
|
+
|
22
|
+
DEFAULT_LIMIT = 50
|
23
|
+
|
24
|
+
def initialize(for_class, connection, params="")
|
25
|
+
check_type(for_class)
|
26
|
+
@type, @connection = for_class, connection
|
27
|
+
|
28
|
+
@native_query = @connection.send(base_class(for_class), normalize(params))
|
29
|
+
end
|
30
|
+
|
31
|
+
def raw
|
32
|
+
@native_query
|
33
|
+
end
|
34
|
+
|
35
|
+
def limit(count)
|
36
|
+
@native_query = @native_query.top(count)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def order_by(query)
|
41
|
+
@native_query = @native_query.order_by(query)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def where(filter)
|
46
|
+
@native_query = @native_query.filter(filter)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def count
|
51
|
+
@native_query = @native_query.count
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def include(klass)
|
56
|
+
check_type(klass)
|
57
|
+
@native_query.expand(base_class(klass))
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def page(start)
|
62
|
+
@native_query = @native_query.skip(start)
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def run
|
67
|
+
@connection.execute
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_query
|
71
|
+
@native_query.query
|
72
|
+
end
|
73
|
+
|
74
|
+
def method_missing(method_name, *args, &block)
|
75
|
+
return super unless @type.send "#{method_name}?".to_sym
|
76
|
+
@native_query.navigate(odata_class_from_method_name(method_name))
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def check_type(for_class)
|
82
|
+
raise TypeError, "#{for_class.to_s} is not a valid query type." unless VALID_CLASSES.include? for_class
|
83
|
+
end
|
84
|
+
|
85
|
+
def normalize(params)
|
86
|
+
args = params.first
|
87
|
+
case args
|
88
|
+
when String
|
89
|
+
format_parameter(args)
|
90
|
+
when Array
|
91
|
+
args.map {|item| format_parameter(item) }.join(",")
|
92
|
+
when Hash
|
93
|
+
args.map {|k,v| "#{k.to_s.capitalize}=#{format_parameter(v)}"}.join(',')
|
94
|
+
else
|
95
|
+
args
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def format_parameter(param)
|
100
|
+
"'#{param}'"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TFS
|
2
|
+
class Queryable
|
3
|
+
class << self
|
4
|
+
include TFS::ClassHelpers
|
5
|
+
# Always limit to the default limit of 50 so as not to overwhelm the service
|
6
|
+
# If more are required, set `#limit` explicitly.
|
7
|
+
def all
|
8
|
+
get_query.limit(QueryEngine::DEFAULT_LIMIT).run
|
9
|
+
end
|
10
|
+
|
11
|
+
def inherited(klass)
|
12
|
+
klass.instance_eval do
|
13
|
+
@children = []
|
14
|
+
|
15
|
+
def add_child(child_class)
|
16
|
+
|
17
|
+
base = method_name_from_class(child_class).to_sym
|
18
|
+
self.send(:define_singleton_method, "#{base}?") { true }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# #odata_query allows you to access the raw query sytax provide by the OData api
|
24
|
+
#
|
25
|
+
# TFS::Builds.odata_query('Status eq "Succeeded"')
|
26
|
+
#
|
27
|
+
def odata_query(raw_query)
|
28
|
+
get_query.where(raw_query)
|
29
|
+
end
|
30
|
+
alias :where :odata_query
|
31
|
+
|
32
|
+
private
|
33
|
+
def get_query
|
34
|
+
TFS.send(method_name_from_class)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Eventually I'd like to see a query compiler that makes more
|
41
|
+
# "ruby like" queries possible, instead of forcing users to use the OData
|
42
|
+
# query api. For now, this is good enough.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TFS
|
2
|
+
class WorkItems < Queryable
|
3
|
+
InvalidRecord = Class.new(StandardError)
|
4
|
+
|
5
|
+
REQUIRED_PARAMS = [
|
6
|
+
"Title",
|
7
|
+
"Type",
|
8
|
+
"Project",
|
9
|
+
"Description"
|
10
|
+
]
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Changeset can be found by id alone
|
14
|
+
#
|
15
|
+
# TFS::Changeset.find(123)
|
16
|
+
#
|
17
|
+
def find(id)
|
18
|
+
TFS.workitems(id).run.first
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(item)
|
22
|
+
REQUIRED_PARAMS.each do |param|
|
23
|
+
raise InvalidRecord, "Missing required parameter '#{param}'" if item.send(param).nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
client.AddToWorkItems(item)
|
27
|
+
item = client.save_changes
|
28
|
+
item.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def update(item)
|
32
|
+
client.update_object(item)
|
33
|
+
client.save_changes
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def client
|
38
|
+
@client ||= TFS.client
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/tfs.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'tfs/client'
|
2
|
+
|
3
|
+
module TFS
|
4
|
+
# Wrapper for TFS::Client.new
|
5
|
+
class << self
|
6
|
+
include Configuration
|
7
|
+
|
8
|
+
# Shortcut to create and connect a client
|
9
|
+
def client
|
10
|
+
@client = begin
|
11
|
+
client = Client.new(options)
|
12
|
+
client.connect
|
13
|
+
client
|
14
|
+
end unless defined?(@client) && @client.hash == options.hash
|
15
|
+
|
16
|
+
@client
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(method_name, include_private=false); client.respond_to?(method_name, include_private); end if RUBY_VERSION >= "1.9"
|
20
|
+
def respond_to?(method_name, include_private=false); client.respond_to?(method_name, include_private) || super; end if RUBY_VERSION < "1.9"
|
21
|
+
|
22
|
+
private
|
23
|
+
def method_missing(method_name, *args, &block)
|
24
|
+
return super unless client.respond_to?(method_name)
|
25
|
+
client.send(method_name, *args, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
TFS.setup
|
data/ruby_tfs.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.add_dependency 'ruby_odata'
|
7
|
+
|
8
|
+
spec.add_development_dependency 'bundler', '~> 1.0'
|
9
|
+
|
10
|
+
spec.authors = ["Luke van der Hoeven"]
|
11
|
+
spec.description = %q{A Ruby interface to the TFS OData API.}
|
12
|
+
spec.email = ['hungerandthirst@gmail.com']
|
13
|
+
spec.files = %w(Rakefile LICENSE.md README.md ruby_tfs.gemspec)
|
14
|
+
spec.files += Dir.glob("lib/**/*.rb")
|
15
|
+
spec.homepage = 'https://github.com/BFGCOMMUNICATIONS/ruby_tfs'
|
16
|
+
spec.licenses = ['APLv2']
|
17
|
+
spec.name = 'ruby_tfs'
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
spec.required_rubygems_version = '>= 1.3.6'
|
20
|
+
spec.summary = spec.description
|
21
|
+
spec.test_files = Dir.glob("spec/**/*")
|
22
|
+
spec.version = "0.0.1"
|
23
|
+
end
|