From 3d9b34b3a7b12e66da76e9d83c90bf6d20c7bc35 Mon Sep 17 00:00:00 2001 From: Patrick Reagan Date: Thu, 18 Jun 2009 21:52:40 -0400 Subject: [PATCH] Initial implementation of Project retrieval --- .gitignore | 3 ++ README.rdoc | 66 +++++++++++++++++++++++++ Rakefile | 39 +++++++++++++++ lib/unfuzzle.rb | 59 ++++++++++++++++++++++ lib/unfuzzle/project.rb | 59 ++++++++++++++++++++++ lib/unfuzzle/request.rb | 37 ++++++++++++++ lib/unfuzzle/response.rb | 32 ++++++++++++ lib/unfuzzle/version.rb | 13 +++++ test/fixtures/projects.json | 32 ++++++++++++ test/test_helper.rb | 37 ++++++++++++++ test/unit/unfuzzle/project_test.rb | 76 +++++++++++++++++++++++++++++ test/unit/unfuzzle/request_test.rb | 63 ++++++++++++++++++++++++ test/unit/unfuzzle/response_test.rb | 65 ++++++++++++++++++++++++ test/unit/unfuzzle_test.rb | 24 +++++++++ 14 files changed, 605 insertions(+) create mode 100644 .gitignore create mode 100644 README.rdoc create mode 100644 Rakefile create mode 100644 lib/unfuzzle.rb create mode 100644 lib/unfuzzle/project.rb create mode 100644 lib/unfuzzle/request.rb create mode 100644 lib/unfuzzle/response.rb create mode 100644 lib/unfuzzle/version.rb create mode 100644 test/fixtures/projects.json create mode 100644 test/test_helper.rb create mode 100644 test/unit/unfuzzle/project_test.rb create mode 100644 test/unit/unfuzzle/request_test.rb create mode 100644 test/unit/unfuzzle/response_test.rb create mode 100644 test/unit/unfuzzle_test.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1a668a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/pkg/ +/doc/ +/coverage/ \ No newline at end of file diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000..db46a4b --- /dev/null +++ b/README.rdoc @@ -0,0 +1,66 @@ += Unfuzzle + +== Description + +The Unfuzzle gem provides an interface to the Unfuddle JSON API + +== Installation + + sudo gem install vigetlabs-unfuzzle --source=http://gems.github.com + +== Usage + +To get started, you'll need your Unfuddle subdomain and a valid username / +password combination: + + require 'unfuzzle' + + Unfuzzle.subdomain = 'viget' + Unfuzzle.username = 'bopbip' + Unfuzzle.password = 'bleep' + +Once that is configured, you can start accessing data from the API. + +=== Projects + +Pulling back a list of projects is simple. Based on the currently logged-in +user, you can see which ones he has access to: + + projects = Unfuzzle::Project.all # => [# , ...] + project = projects.first + +There are a few attributes available for a project: + + project.id # => 123 + project.slug # => "salty" + project.name # => "Salty Co." + project.archived? # => false + project.created_at.strftime('%Y-%m-%d') # => "2008-07-28" + +To see a list of additional attributes, take a look at the documentation for +Unfuzzle::Project. + +== License + +Copyright (c) 2009 Patrick Reagan of Viget Labs (mailto:patrick.reagan@viget.com) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2d8db5f --- /dev/null +++ b/Rakefile @@ -0,0 +1,39 @@ +require 'rubygems' +require 'rake/gempackagetask' +require 'rake/testtask' + +require 'lib/unfuzzle/version' + +task :default => :test + +spec = Gem::Specification.new do |s| + s.name = 'unfuzzle' + s.version = Unfuzzle::Version.to_s + s.has_rdoc = true + s.extra_rdoc_files = %w(README.rdoc) + s.rdoc_options = %w(--main README.rdoc) + s.summary = "This gem does ... " + s.author = 'First Last' + s.email = 'user@example.com' + s.homepage = 'http://my-site.net' + s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,test}/**/*") + + s.add_dependency('json', '>= 1.1.6') +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.gem_spec = spec +end + +Rake::TestTask.new do |t| + t.libs << 'test' + t.test_files = FileList["test/**/*_test.rb"] + t.verbose = true +end + +desc 'Generate the gemspec to serve this Gem from Github' +task :github do + file = File.dirname(__FILE__) + "/#{spec.name}.gemspec" + File.open(file, 'w') {|f| f << spec.to_ruby } + puts "Created gemspec: #{file}" +end \ No newline at end of file diff --git a/lib/unfuzzle.rb b/lib/unfuzzle.rb new file mode 100644 index 0000000..c2d6dcd --- /dev/null +++ b/lib/unfuzzle.rb @@ -0,0 +1,59 @@ +$:.unshift File.dirname(__FILE__) + +require 'uri' +require 'net/http' +require 'json' + +require 'unfuzzle/request' +require 'unfuzzle/response' +require 'unfuzzle/project' + +# = Unfuzzle: A simple wrapper around the Unfuddle JSON API +# +# == Quick Start +# +# To get started, you need to set the subdomain and a valid username / +# password combination: +# +# require 'rubygems' +# require 'unfuzzle' +# +# Unfuzzle.subdomain = 'viget' +# Unfuzzle.username = 'bopbip' +# Unfuzzle.password = 'bleep' +# +# From there, you can start accessing a list of projects: +# +# Project.all +# +module Unfuzzle + + # Set the subdomain for all requests + def self.subdomain=(subdomain) + @subdomain = subdomain + end + + # Set the username for all requests. Data retrieved from the API will be + # scoped to the data that this user has access to. + def self.username=(username) + @username = username + end + + # Set the password for the supplied username + def self.password=(password) + @password = password + end + + def self.subdomain # :nodoc: + @subdomain + end + + def self.username # :nodoc: + @username + end + + def self.password # :nodoc: + @password + end + +end \ No newline at end of file diff --git a/lib/unfuzzle/project.rb b/lib/unfuzzle/project.rb new file mode 100644 index 0000000..d48f8a1 --- /dev/null +++ b/lib/unfuzzle/project.rb @@ -0,0 +1,59 @@ +module Unfuzzle + + # = Project + # + # Represents an Unfuddle project. Has the following attributes: + # + # [id] The unique identifier for this project + # [slug] The "short name" for this project + # [name] The name of this project + # [description] The description for the project + # + class Project + + def self.attribute(name, options = {}) # :nodoc: + key = options.delete(:from) || name + + class_eval %( + def #{name} + @response_data['#{key}'] + end + ) + end + + attribute :id + attribute :slug, :from => :short_name + attribute :archived + attribute :name, :from => :title + attribute :description + attribute :created_timestamp, :from => :created_at + attribute :updated_timestamp, :from => :updated_at + + # Return a list of all projects to which the current user has access + def self.all + response = Request.get('/projects') + response.parse.map {|data| Project.new(data) } + end + + # Create a new project from JSON response data + def initialize(response_data) + @response_data = response_data + end + + # Has this project been archived? + def archived? + archived == true + end + + # The DateTime that this project was created + def created_at + DateTime.parse(created_timestamp) + end + + # The DateTime that this project was last updated + def updated_at + DateTime.parse(updated_timestamp) + end + + end +end \ No newline at end of file diff --git a/lib/unfuzzle/request.rb b/lib/unfuzzle/request.rb new file mode 100644 index 0000000..12c1044 --- /dev/null +++ b/lib/unfuzzle/request.rb @@ -0,0 +1,37 @@ +module Unfuzzle + + # = Request + # + # A basic wrapper for GET requests to the Unfuddle API + # + class Request + + # Retrieve a response from the given resource path + def self.get(resource_path) + request = new(resource_path) + request.get + end + + # Create a new request for the given resource path + def initialize(resource_path) + @resource_path = resource_path + end + + def endpoint_uri # :nodoc: + URI.parse("http://#{Unfuzzle.subdomain}.unfuddle.com/api/v1#{@resource_path}.json") + end + + def client # :nodoc: + Net::HTTP.new(endpoint_uri.host) + end + + # Retrieve a response from the current resource path + def get + request = Net::HTTP::Get.new(endpoint_uri.path) + request.basic_auth Unfuzzle.username, Unfuzzle.password + + Response.new(client.request(request)) + end + + end +end diff --git a/lib/unfuzzle/response.rb b/lib/unfuzzle/response.rb new file mode 100644 index 0000000..007f951 --- /dev/null +++ b/lib/unfuzzle/response.rb @@ -0,0 +1,32 @@ +module Unfuzzle + + # = Response + # + # A simple wrapper around an HTTP response from the Unfuddle API + # + class Response + + # Create a new response from an HTTP response object + def initialize(http_response) + @http_response = http_response + end + + # Was there an error produced as part of the request? + def error? + !@http_response.is_a?(Net::HTTPSuccess) + end + + # Raw body of the HTTP response + def body + @http_response.body + end + + # Parsed JSON response body + def parse + if !error? + @parsed_data ||= JSON.parse(body) + end + end + + end +end \ No newline at end of file diff --git a/lib/unfuzzle/version.rb b/lib/unfuzzle/version.rb new file mode 100644 index 0000000..5a6d9a0 --- /dev/null +++ b/lib/unfuzzle/version.rb @@ -0,0 +1,13 @@ +module Unfuzzle + module Version + + MAJOR = 0 + MINOR = 1 + TINY = 0 + + def self.to_s # :nodoc: + [MAJOR, MINOR, TINY].join('.') + end + + end +end \ No newline at end of file diff --git a/test/fixtures/projects.json b/test/fixtures/projects.json new file mode 100644 index 0000000..38d7a5a --- /dev/null +++ b/test/fixtures/projects.json @@ -0,0 +1,32 @@ +[{ + "archived": false, + "short_name": "aa", + "created_at": "2008-07-28T16:57:10Z", + "title": "A Client", + "close_ticket_simultaneously_default": true, + "account_id": 12345, + "description": "A great project", + "enable_time_tracking": true, + "default_ticket_report_id": 0, + "updated_at": "2009-04-28T18:48:52Z", + "id": 1, + "theme": "teal", + "disk_usage": 1416, + "assignee_on_resolve": "none" +}, +{ + "archived": true, + "short_name": "zz", + "created_at": "2008-06-23T21:06:01Z", + "title": "Zee Client", + "close_ticket_simultaneously_default": false, + "account_id": 12345, + "description": null, + "enable_time_tracking": true, + "default_ticket_report_id": null, + "updated_at": "2009-05-20T23:15:24Z", + "id": 2, + "theme": "grey", + "disk_usage": 14008, + "assignee_on_resolve": "reporter" +}] \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..3170058 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,37 @@ +# http://sneaq.net/textmate-wtf +$:.reject! { |e| e.include? 'TextMate' } + +require 'rubygems' +require 'throat_punch' + +require File.dirname(__FILE__) + '/../lib/unfuzzle' + + +class Test::Unit::TestCase + + def self.read_fixture(method_name) + file = File.dirname(__FILE__) + "/fixtures/#{method_name}.json" + JSON.parse(File.read(file)) + end + + def read_fixture(method_name) + self.class.read_fixture(method_name) + end + + def self.when_populating(klass, options, &block) + # data = options[:from].is_a?(String) ? read_fixture(options[:from])[0] : options[:from].call + + context "with data populated for #{klass}" do + setup { @object = klass.new(read_fixture(options[:from])[0]) } + merge_block(&block) + end + + end + + def self.value_for(method_name, options) + should "have a value for :#{method_name}" do + @object.send(method_name).should == options[:is] + end + end + +end \ No newline at end of file diff --git a/test/unit/unfuzzle/project_test.rb b/test/unit/unfuzzle/project_test.rb new file mode 100644 index 0000000..09e2820 --- /dev/null +++ b/test/unit/unfuzzle/project_test.rb @@ -0,0 +1,76 @@ +require File.dirname(__FILE__) + '/../../test_helper' + +module Unfuzzle + class ProjectTest < Test::Unit::TestCase + + context "The Project class" do + + should "be able to return a list of all projects" do + data = read_fixture('projects') + response = stub(:parse => data) + + Unfuzzle::Request.expects(:get).with('/projects').returns(response) + + elements = Array.new + + data.each do |node| + element = stub() + Unfuzzle::Project.expects(:new).with(node).returns(element) + elements << element + end + + Project.all.should == elements + end + + end + + context "An instance of the Project class" do + + when_populating Project, :from => 'projects' do + + value_for :id, :is => 1 + value_for :archived, :is => false + value_for :slug, :is => 'aa' + value_for :name, :is => 'A Client' + value_for :description, :is => 'A great project' + value_for :created_timestamp, :is => '2008-07-28T16:57:10Z' + value_for :updated_timestamp, :is => '2009-04-28T18:48:52Z' + + end + + should "know that it's archived" do + project = Project.new(stub()) + project.stubs(:archived).with().returns(true) + + project.archived?.should be(true) + end + + should "know that it's not archived" do + project = Project.new(stub()) + project.stubs(:archived).with().returns(false) + + project.archived?.should be(false) + end + + should "have a create date" do + project = Project.new(stub()) + project.stubs(:created_timestamp).with().returns('2008-07-28T16:57:10Z') + + DateTime.expects(:parse).with('2008-07-28T16:57:10Z').returns('create_date') + + project.created_at.should == 'create_date' + end + + should "have an update date" do + project = Project.new(stub()) + project.stubs(:updated_timestamp).with().returns('2009-04-28T18:48:52Z') + + DateTime.expects(:parse).with('2009-04-28T18:48:52Z').returns('update_date') + + project.updated_at.should == 'update_date' + end + + end + + end +end \ No newline at end of file diff --git a/test/unit/unfuzzle/request_test.rb b/test/unit/unfuzzle/request_test.rb new file mode 100644 index 0000000..b1fdded --- /dev/null +++ b/test/unit/unfuzzle/request_test.rb @@ -0,0 +1,63 @@ +require File.dirname(__FILE__) + '/../../test_helper' + +module Unfuzzle + class RequestTest < Test::Unit::TestCase + + context "The Request class" do + + should "be able to perform a GET request" do + request = mock() {|r| r.expects(:get).with().returns('response') } + Unfuzzle::Request.expects(:new).with('/projects').returns(request) + + Unfuzzle::Request.get('/projects').should == 'response' + end + + end + + context "An instance of the Request class" do + + should "have an endpoint URI" do + Unfuzzle.stubs(:subdomain).with().returns('viget') + + request = Unfuzzle::Request.new('/projects') + request.endpoint_uri.should == URI.parse('http://viget.unfuddle.com/api/v1/projects.json') + end + + should "have a client" do + client = stub() + + request = Unfuzzle::Request.new('/projects') + request.stubs(:endpoint_uri).with().returns(URI.parse('http://example.com')) + + Net::HTTP.expects(:new).with('example.com').returns(client) + + request.client.should == client + end + + should "be able to perform a GET request" do + Unfuzzle.stubs(:username).with().returns('username') + Unfuzzle.stubs(:password).with().returns('password') + + request = Unfuzzle::Request.new('/projects') + request.stubs(:endpoint_uri).returns(URI.parse('http://example.com/projects')) + + get_request = mock() do |g| + g.expects(:basic_auth).with('username', 'password') + end + + client = mock() {|c| c.expects(:request).with(get_request).returns('response') } + + response = stub() + Unfuzzle::Response.expects(:new).with('response').returns(response) + + Net::HTTP::Get.expects(:new).with('/projects').returns(get_request) + + request.stubs(:client).with().returns(client) + + request.get.should == response + end + + end + + end +end \ No newline at end of file diff --git a/test/unit/unfuzzle/response_test.rb b/test/unit/unfuzzle/response_test.rb new file mode 100644 index 0000000..d1481a3 --- /dev/null +++ b/test/unit/unfuzzle/response_test.rb @@ -0,0 +1,65 @@ +require File.dirname(__FILE__) + '/../../test_helper' + +module Unfuzzle + class ResponseTest < Test::Unit::TestCase + + context "An instance of the Response class" do + + should "delegate to the HTTP response to determine the body" do + http_response = mock() {|r| r.expects(:body).with().returns('check mah boday') } + + response = Unfuzzle::Response.new(http_response) + + response.body.should == 'check mah boday' + end + + should "know that there are no errors" do + http_response = mock() do |r| + r.expects(:is_a?).with(Net::HTTPSuccess).returns(true) + end + + response = Unfuzzle::Response.new(http_response) + response.error?.should be(false) + end + + should "know if there are errors" do + http_response = mock() do |r| + r.expects(:is_a?).with(Net::HTTPSuccess).returns(false) + end + + response = Unfuzzle::Response.new(http_response) + response.error?.should be(true) + end + + should "be able to parse the response" do + response = Unfuzzle::Response.new(stub()) + response.expects(:body).with().returns('json') + response.stubs(:error?).with().returns(false) + + JSON.expects(:parse).with('json').returns('data') + + response.parse.should == 'data' + end + + should "cache the parsed data from the response" do + response = Unfuzzle::Response.new(stub()) + response.stubs(:body).with().returns('json') + response.stubs(:error?).with().returns(false) + + JSON.stubs(:parse).with('json').once.returns('data') + + 2.times { response.parse } + end + + should "return nil when parsing data if there are errors in the response" do + response = Unfuzzle::Response.new(stub()) + response.expects(:error?).with().returns(true) + + response.parse.should be(nil) + + end + + end + + end +end \ No newline at end of file diff --git a/test/unit/unfuzzle_test.rb b/test/unit/unfuzzle_test.rb new file mode 100644 index 0000000..9dea310 --- /dev/null +++ b/test/unit/unfuzzle_test.rb @@ -0,0 +1,24 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class UnfuzzleTest < Test::Unit::TestCase + + context "The Unfuzzle module" do + + should "be able to set the subdomain" do + Unfuzzle.subdomain = 'viget' + Unfuzzle.subdomain.should == 'viget' + end + + should "be able to set the username" do + Unfuzzle.username = 'username' + Unfuzzle.username.should == 'username' + end + + should "be able to set the password" do + Unfuzzle.password = 'password' + Unfuzzle.password.should == 'password' + end + + end + +end \ No newline at end of file