ruby-freshbooks 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,8 @@
1
+ == 0.2.0
2
+ * added support for OAuth authentication
3
+
4
+ == 0.1.2
5
+ * allow users to either require 'ruby-freshbooks' or require 'freshbooks'
6
+
7
+ == 0.1.1
8
+ * initial release
data/README.md CHANGED
@@ -4,8 +4,8 @@ This is a Ruby wrapper for the [FreshBooks](http://www.freshbooks.com) API. This
4
4
 
5
5
  For example,
6
6
 
7
- conn = FreshBooks::Connection.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
8
- conn.client.get :client_id => 2
7
+ c = FreshBooks::Client.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
8
+ c.client.get :client_id => 2
9
9
 
10
10
  generates the XML:
11
11
 
@@ -20,8 +20,8 @@ purely based on the request arguments. This library doesn't actually know anythi
20
20
 
21
21
  The following call will generate and POST the invoice create XML shown in the [FreshBooks API Documentation](http://developers.freshbooks.com/api/view/invoices/):
22
22
 
23
- conn = FreshBooks::Connection.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
24
- conn.invoice.create(:invoice => {
23
+ c = FreshBooks::Client.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
24
+ c.invoice.create(:invoice => {
25
25
  :client_id => 13,
26
26
  :number => 'FB00004',
27
27
  :status => 'draft',
@@ -59,11 +59,25 @@ The following call will generate and POST the invoice create XML shown in the [F
59
59
 
60
60
  ## Examples
61
61
 
62
- You can call any `#{namespace}.#{method_name}` method chain against a `FreshBooks::Connection` instance and it will POST a request to the corresponding FreshBooks API method. i.e.
62
+ You can call any `#{namespace}.#{method_name}` method chain against a `FreshBooks::Client` instance and it will POST a request with the corresponding FreshBooks API method. i.e.
63
63
 
64
- conn = FreshBooks::Connection.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
65
- conn.client.get :client_id => 37
66
- conn.invoice.list :client_id => 37, :page => 2, :per_page => 10
64
+ c = FreshBooks::Client.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
65
+ c.client.get :client_id => 37
66
+ c.invoice.list :client_id => 37, :page => 2, :per_page => 10
67
+
68
+ ## Authentication
69
+
70
+ You can authenticate using either API tokens or [OAuth](http://oauth.net/). The `FreshBooks::Client.new` constructor will create a client instance of the appropriate type depending on the arguments you pass in. so
71
+
72
+ c = FreshBooks::Client.new('youraccount.freshbooks.com', 'yourfreshbooksapitoken')
73
+
74
+ will return a `FreshBooks::TokenClient` instance, and
75
+
76
+ c = FreshBooks::Client.new('youraccount.freshbooks.com', 'your_consumer_key', 'your_consumer_secret', 'your_access_token', 'your_access_token_secret')
77
+
78
+ will return a `FreshBooks::OAuthClient` instance. both client classes work identically aside from how they authenticate requests.
79
+
80
+ *note:* this library provides no suppport for obtaining OAuth request or access tokens. for help with that take a look at [FreshBooks' OAuth Documentation](http://developers.freshbooks.com/api/oauth/) and the [oauth gem](http://oauth.rubyforge.org/).
67
81
 
68
82
  ## Goals
69
83
 
@@ -74,10 +88,10 @@ You can call any `#{namespace}.#{method_name}` method chain against a `FreshBook
74
88
 
75
89
  * seamless integration with FreshBooks API via an object interface. i.e.
76
90
 
77
- <pre><code>clients = FreshBooks::Client.list
78
- client = clients.first
79
- client.first_name = 'Swenson'
80
- client.update
91
+ <pre><code>invoices = FreshBooks::Invoice.list
92
+ invoice = invoices.first
93
+ invoice.amount = 500.00
94
+ invoice.update
81
95
  </code></pre>
82
96
 
83
97
  if you want this sort of thing, please use [freshbooks.rb](http://github.com/bcurren/freshbooks.rb) instead
@@ -86,7 +100,7 @@ if you want this sort of thing, please use [freshbooks.rb](http://github.com/bcu
86
100
 
87
101
  Maybe you should. It depends on what you want to do. I've used freshbooks.rb before but there were a few things that didn't work for me:
88
102
 
89
- * global connection. I've had the need to connect to multiple FreshBooks accounts within the same program to do things like sync or migrate data. you can't do this with freshbooks.rb because the global connection is owned by `FreshBooks::Base` which is the superclass of `Client`, `Invoice`, etc.
103
+ * global connection. I've had the need to connect to multiple FreshBooks accounts within the same program to do things like sync or migrate data. you can't do this with freshbooks.rb because the global connection is owned by `FreshBooks::Base` which is the superclass of `Item`, `Invoice`, etc.
90
104
  * requiring a library update every time the FreshBooks API changes. although this doesn't happen very often, it's a little annoying to have to manually patch freshbooks.rb when it does.
91
105
  * having to convert everything to and from the business objects the library provides. because the freshbooks.rb API is nice and abstract, it's easy to play around with invoices, clients, etc. as business objects. however, this is less convenient for mass import/export type programs because your data has to be pushed through that object interface instead of just transformed into YAML, CSV, etc. it's also less than desirable when your integration makes use of an alternate model class (i.e. an `ActiveRecord` subclass that you're using to save your FreshBooks data into a database).
92
106
  * data transparency. if you're just exploring the FreshBooks API, you might not know all of the attributes that some data types exposes. if you are getting back nicely packaged objects, you'll need to read through the documentation (or source code of freshbooks.rb if you're sure some property ought to be there but isn't and you suspect it's missing from the mapping) to see what you have access to.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.2.0
data/lib/freshbooks.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'httparty'
2
2
  require 'builder'
3
+ require 'openssl'
4
+ require 'cgi'
3
5
 
4
6
  module FreshBooks
5
7
  API_VERSION = '2.1'
@@ -22,15 +24,32 @@ module FreshBooks
22
24
  end
23
25
  end
24
26
 
25
- # FreshBooks API connection. instances are FreshBooks account
26
- # specific so you can, e.g. setup two connections and copy/
27
+ class Connection # :nodoc:
28
+ # <b>DEPRECATED:</b> Please use <tt>FreshBooks::Client.new</tt> instead.
29
+ def self.new(*args)
30
+ warn "[DEPRECATED] `FreshBooks::Connection` is deprecated. Please use `FreshBooks::Client` instead."
31
+ Client.new(*args)
32
+ end
33
+ end
34
+
35
+ # FreshBooks API client. instances are FreshBooks account
36
+ # specific so you can, e.g. setup two clients and copy/
27
37
  # sync data between them
28
- class Connection
38
+ module Client
29
39
  include HTTParty
30
40
 
31
- def initialize(domain, token)
32
- @domain = domain
33
- @auth = { :username => token, :password => 'X' }
41
+ # :call-seq:
42
+ # new(domain, api_token) => FreshBooks::TokenClient
43
+ # new(domain, consumer_key, consumer_secret, token, token_secret) => FreshBooks::OAuthClient
44
+ #
45
+ # creates a new FreshBooks API client. returns the appropriate client
46
+ # type based on the authorization arguments provided
47
+ def self.new(*args)
48
+ case args.size
49
+ when 2 then TokenClient.new(*args)
50
+ when 5 then OAuthClient.new(*args)
51
+ else raise ArgumentError
52
+ end
34
53
  end
35
54
 
36
55
  def api_url # :nodoc:
@@ -44,9 +63,9 @@ module FreshBooks
44
63
  # note: we only need to provide a #post method because the
45
64
  # FreshBooks API is POST only
46
65
  def post(method, params={}) # :nodoc:
47
- Response.new self.class.post(api_url,
48
- :basic_auth => @auth,
49
- :body => self.class.xml_body(method, params))
66
+ Response.new Client.post(api_url,
67
+ :headers => auth,
68
+ :body => Client.xml_body(method, params))
50
69
  end
51
70
 
52
71
  # takes nested Hash/Array combos and generates isomorphic
@@ -69,15 +88,15 @@ module FreshBooks
69
88
  # be used in a context where some other library hasn't
70
89
  # already defined #to_xml on Hash...
71
90
  case obj
72
- when Hash : obj.each { |k,v| xml.tag!(k) { build_xml(v, xml) } }
73
- when Array : obj.each { |e| build_xml(e ,xml) }
91
+ when Hash then obj.each { |k,v| xml.tag!(k) { build_xml(v, xml) } }
92
+ when Array then obj.each { |e| build_xml(e ,xml) }
74
93
  else xml.text! obj.to_s
75
94
  end
76
95
  xml.target!
77
96
  end
78
97
 
79
98
  # infer API methods based on 2-deep method chains sent to
80
- # connections. this allows us to provide a simple interface
99
+ # clients. this allows us to provide a simple interface
81
100
  # without actually knowing anything about the supported API
82
101
  # methods (and hence trusting users to read the official
83
102
  # FreshBooks API documentation)
@@ -86,10 +105,68 @@ module FreshBooks
86
105
  end
87
106
 
88
107
  # nothing to see here...
89
- class NamespaceProxy < Struct.new(:conn, :namespace) # :nodoc:
108
+ class NamespaceProxy < Struct.new(:client, :namespace) # :nodoc:
90
109
  def method_missing(sym, *args)
91
- conn.post "#{namespace}.#{sym}", *args
110
+ client.post "#{namespace}.#{sym}", *args
92
111
  end
93
112
  end
94
113
  end
114
+
115
+ # Basic Auth client. uses an account's API token.
116
+ class TokenClient
117
+ include Client
118
+
119
+ def initialize(domain, api_token)
120
+ @domain = domain
121
+ @username = api_token
122
+ @password = 'X'
123
+ end
124
+
125
+ def auth
126
+ { 'Authorization' =>
127
+ # taken from lib/net/http.rb
128
+ 'Basic ' + ["#{@username}:#{@password}"].pack('m').delete("\r\n") }
129
+ end
130
+ end
131
+
132
+ # OAuth 1.0 client. access token and secret must be obtained elsewhere.
133
+ # cf. the {oauth gem}[http://oauth.rubyforge.org/]
134
+ class OAuthClient
135
+ include Client
136
+
137
+ def initialize(domain, consumer_key, consumer_secret, token, token_secret)
138
+ @domain = domain
139
+ @consumer_key = consumer_key
140
+ @consumer_secret = consumer_secret
141
+ @token = token
142
+ @token_secret = token_secret
143
+ end
144
+
145
+ def auth
146
+ data = {
147
+ :realm => '',
148
+ :oauth_version => '1.0',
149
+ :oauth_consumer_key => @consumer_key,
150
+ :oauth_token => @token,
151
+ :oauth_timestamp => timestamp,
152
+ :oauth_nonce => nonce,
153
+ :oauth_signature_method => 'PLAINTEXT',
154
+ :oauth_signature => signature,
155
+ }.map { |k,v| %Q[#{k}="#{v}"] }.join(',')
156
+
157
+ { 'Authorization' => "OAuth #{data}" }
158
+ end
159
+
160
+ def signature
161
+ CGI.escape("#{@consumer_secret}&#{@token_secret}")
162
+ end
163
+
164
+ def nonce
165
+ [OpenSSL::Random.random_bytes(10)].pack('m').gsub(/\W/, '')
166
+ end
167
+
168
+ def timestamp
169
+ Time.now.to_i
170
+ end
171
+ end
95
172
  end
@@ -5,9 +5,9 @@ Gem::Specification.new do |s|
5
5
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
6
6
  s.authors = ["Justin Giancola"]
7
7
  s.date = %q{2010-04-25}
8
- s.description = %q{simple FreshBooks API wrapper}
8
+ s.description = %q{simple FreshBooks API wrapper. supports both OAuth and API token authentication}
9
9
  s.email = %q{elucid@gmail.com}
10
- s.files = ["README.md", "LICENSE", "VERSION", "ruby-freshbooks.gemspec", "lib/freshbooks.rb", "lib/ruby-freshbooks.rb", "spec/freshbooks_spec.rb"]
10
+ s.files = ["README.md", "LICENSE", "VERSION", "CHANGELOG", "ruby-freshbooks.gemspec", "lib/freshbooks.rb", "lib/ruby-freshbooks.rb", "spec/freshbooks_spec.rb"]
11
11
  s.has_rdoc = false
12
12
  s.homepage = %q{http://github.com/elucid/ruby-freshbooks}
13
13
  s.require_paths = ["lib"]
@@ -1,7 +1,7 @@
1
1
  require 'freshbooks'
2
2
 
3
3
  def build_xml(data)
4
- FreshBooks::Connection.build_xml data
4
+ FreshBooks::Client.build_xml data
5
5
  end
6
6
 
7
7
  describe "XML generation:" do
@@ -54,3 +54,10 @@ describe "XML generation:" do
54
54
  end
55
55
  end
56
56
  end
57
+
58
+ describe "FreshBooks Client instantiation" do
59
+ it "should create a TokenClient instance when Connection.new is called" do
60
+ c = FreshBooks::Connection.new('foo.freshbooks.com', 'abcdefghijklm')
61
+ c.should be_a(FreshBooks::TokenClient)
62
+ end
63
+ end
metadata CHANGED
@@ -1,12 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-freshbooks
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 1
8
- - 2
9
- version: 0.1.2
4
+ version: 0.2.0
10
5
  platform: ruby
11
6
  authors:
12
7
  - Justin Giancola
@@ -19,47 +14,35 @@ default_executable:
19
14
  dependencies:
20
15
  - !ruby/object:Gem::Dependency
21
16
  name: httparty
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
24
20
  requirements:
25
21
  - - ">="
26
22
  - !ruby/object:Gem::Version
27
- segments:
28
- - 0
29
- - 5
30
- - 0
31
23
  version: 0.5.0
32
- type: :runtime
33
- version_requirements: *id001
24
+ version:
34
25
  - !ruby/object:Gem::Dependency
35
26
  name: builder
36
- prerelease: false
37
- requirement: &id002 !ruby/object:Gem::Requirement
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
38
30
  requirements:
39
31
  - - ">="
40
32
  - !ruby/object:Gem::Version
41
- segments:
42
- - 2
43
- - 1
44
- - 2
45
33
  version: 2.1.2
46
- type: :runtime
47
- version_requirements: *id002
34
+ version:
48
35
  - !ruby/object:Gem::Dependency
49
36
  name: rspec
50
- prerelease: false
51
- requirement: &id003 !ruby/object:Gem::Requirement
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
52
40
  requirements:
53
41
  - - ">="
54
42
  - !ruby/object:Gem::Version
55
- segments:
56
- - 1
57
- - 3
58
- - 0
59
43
  version: 1.3.0
60
- type: :development
61
- version_requirements: *id003
62
- description: simple FreshBooks API wrapper
44
+ version:
45
+ description: simple FreshBooks API wrapper. supports both OAuth and API token authentication
63
46
  email: elucid@gmail.com
64
47
  executables: []
65
48
 
@@ -71,6 +54,7 @@ files:
71
54
  - README.md
72
55
  - LICENSE
73
56
  - VERSION
57
+ - CHANGELOG
74
58
  - ruby-freshbooks.gemspec
75
59
  - lib/freshbooks.rb
76
60
  - lib/ruby-freshbooks.rb
@@ -88,23 +72,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
72
  requirements:
89
73
  - - ">="
90
74
  - !ruby/object:Gem::Version
91
- segments:
92
- - 0
93
75
  version: "0"
76
+ version:
94
77
  required_rubygems_version: !ruby/object:Gem::Requirement
95
78
  requirements:
96
79
  - - ">="
97
80
  - !ruby/object:Gem::Version
98
- segments:
99
- - 1
100
- - 2
101
81
  version: "1.2"
82
+ version:
102
83
  requirements: []
103
84
 
104
85
  rubyforge_project:
105
- rubygems_version: 1.3.6
86
+ rubygems_version: 1.3.5
106
87
  signing_key:
107
88
  specification_version: 3
108
- summary: simple FreshBooks API wrapper
89
+ summary: simple FreshBooks API wrapper. supports both OAuth and API token authentication
109
90
  test_files: []
110
91