Pivotal Tracker API Ruby Wrapper

UPDATE — Code is now maintained on GitHub: http://github.com/jsmestad/pivotal-tracker/tree/master

I have been playing around with the recently released public API for Pivotal Tracker. In the process of converting this into a Datamapper Adapter, I tested it with some simple Net::HTTP Ruby code. It provides a nice simple, and limited, illustration of what is possible with this public API. One thing I would like to see added to the API however is the ability to limit the number of results when querying with filters. Attached is the slightly modified source code (removed my project id and token key). Tests are included as always, but fair warning, some are brittle! I will post another revision once the Datamapper Adapter is created.

[sourcecode language="ruby"]
require ‘rubygems’
require ‘hpricot’
require ‘net/http’
require ‘uri’
require ‘cgi’

##
# Pivotal Tracker API Ruby Wrapper
# November 11, 2008
# Justin Smestad
# http://www.evalcode.com
##

class Tracker
def initialize(project_id = changeme, token = changeme)
@project_id, @token = project_id, token
end

def project
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.get(resource_uri.path, {‘Token’ => @token})
end

doc = Hpricot(response.body).at(‘project’)

@project = {
:name => doc.at(‘name’).innerHTML,
:iteration_length => doc.at(‘iteration_length’).innerHTML,
:week_start_day => doc.at(‘week_start_day’).innerHTML,
:point_scale => doc.at(‘point_scale’).innerHTML
}
end

def stories
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.get(resource_uri.path, {‘Token’ => @token})
end

doc = Hpricot(response.body)

@stories = []

doc.search(’stories > story’).each do |story|
@stories << {
:id => story.at(‘id’).innerHTML.to_i,
:type => story.at(’story_type’).innerHTML,
:name => story.at(‘name’).innerHTML
}
end
return @stories
end

# would ideally like to pass a size, aka :all to limit search
def find(filters = {})
uri = "http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories"
unless filters.empty?
uri << "?filter="
filters.each do |key, value|
uri << CGI::escape("#{key}\"#{value}\""
end
end

resource_uri = URI.parse(uri)
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.get(resource_uri.path, {‘Token’ => @token})
end

doc = Hpricot(response.body)

@stories = []

doc.search(’stories > story’).each do |story|
@stories << {
:id => story.at(‘id’).innerHTML.to_i,
:type => story.at(’story_type’).innerHTML,
:name => story.at(‘name’).innerHTML
}
end
return @stories
end

def find_story(id)
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories/#{id}")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.get(resource_uri.path, {‘Token’ => @token, ‘Content-Type’ => ‘application/xml’})
end

doc = Hpricot(response.body).at(’story’)

@story = {
:id => doc.at(‘id’).innerHTML.to_i,
:type => doc.at(’story_type’).innerHTML,
:name => doc.at(‘name’).innerHTML
}
end

def create_story(story)
story_xml = build_story_xml(story)
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.post(resource_uri.path, story_xml, {‘Token’ => @token, ‘Content-Type’ => ‘application/xml’})
end
end

def update_story(story)
story_xml = build_story_xml(story)
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories/#{story[:id]}")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.put(resource_uri.path, story_xml, {‘Token’ => @token, ‘Content-Type’ => ‘application/xml’})
end
end

def delete_story(story_id)
resource_uri = URI.parse("http://www.pivotaltracker.com/services/v1/projects/#{@project_id}/stories/#{story_id}")
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
http.delete(resource_uri.path, {‘Token’ => @token})
end
end

private

def build_story_xml(story)
story_xml = "<story>"
story.each do |key, value|
story_xml << "<#{key}>#{value.to_s}</#{key}>"
end
story_xml << "</story>"
end
end
[/sourcecode]

[sourcecode language="ruby"]
require ‘pivotal_tracker’
require ‘test/unit’

class PivotalTrackerTest < Test::Unit::TestCase

def setup
@tracker = Tracker.new
end

def test_assert_stories_return
assert_equal 3, @tracker.stories.size
end

def test_assert_project_response
project = @tracker.project
assert_equal "Factory Test", project[:name]
assert_equal "1", project[:iteration_length]
end

def test_find_without_filters
result = @tracker.find
assert_equal @tracker.stories.size, result.size
end

def test_find_with_filters
result = @tracker.find :name => ‘Create another one’
assert_equal result[0][:name], ‘Create another one’
end

def test_assert_story_creation
current_size = @tracker.stories.size
story = {
:name => ‘Create another one’,
:story_type => "feature",
:requested_by => "Justin Smestad"
}
@tracker.create_story(story)
assert_equal (current_size + 1), @tracker.stories.size
end

def test_story_updates
story = {
:id => 272626,
:name => ‘This has changed’
}
@tracker.update_story(story)
assert_equal @tracker.find_story(story[:id])[:name], story[:name]
end

def test_story_deletion
current_size = @tracker.stories.size
id = @tracker.stories[0][:id]
@tracker.delete_story(id)
assert_equal (current_size – 1), @tracker.stories.size
end

end
[/sourcecode]



2 Responses to “Pivotal Tracker API Ruby Wrapper”

  1. cory Says:

    Hey, could you put this up on github? (or d you mind if I do?) I’d like to make some modifications to it.

  2. Justin Smestad Says:

Leave a Reply