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 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
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :test => :spec
8
+ task :default => :spec
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,13 @@
1
+ module TFS
2
+ class Changesets < Queryable
3
+ class << self
4
+ # Changeset can be found by id alone
5
+ #
6
+ # TFS::Changeset.find(123)
7
+ #
8
+ def find(id)
9
+ TFS.changesets(id).run.first
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -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