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 +8 -0
- data/README.md +27 -13
- data/VERSION +1 -1
- data/lib/freshbooks.rb +91 -14
- data/ruby-freshbooks.gemspec +2 -2
- data/spec/freshbooks_spec.rb +8 -1
- metadata +19 -38
data/CHANGELOG
ADDED
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
|
-
|
8
|
-
|
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
|
-
|
24
|
-
|
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::
|
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
|
-
|
65
|
-
|
66
|
-
|
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>
|
78
|
-
|
79
|
-
|
80
|
-
|
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 `
|
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
|
+
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
|
-
#
|
26
|
-
|
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
|
-
|
38
|
+
module Client
|
29
39
|
include HTTParty
|
30
40
|
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
48
|
-
|
49
|
-
|
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
|
73
|
-
when Array
|
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
|
-
#
|
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(:
|
108
|
+
class NamespaceProxy < Struct.new(:client, :namespace) # :nodoc:
|
90
109
|
def method_missing(sym, *args)
|
91
|
-
|
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
|
data/ruby-freshbooks.gemspec
CHANGED
@@ -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"]
|
data/spec/freshbooks_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'freshbooks'
|
2
2
|
|
3
3
|
def build_xml(data)
|
4
|
-
FreshBooks::
|
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
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
33
|
-
version_requirements: *id001
|
24
|
+
version:
|
34
25
|
- !ruby/object:Gem::Dependency
|
35
26
|
name: builder
|
36
|
-
|
37
|
-
|
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
|
-
|
47
|
-
version_requirements: *id002
|
34
|
+
version:
|
48
35
|
- !ruby/object:Gem::Dependency
|
49
36
|
name: rspec
|
50
|
-
|
51
|
-
|
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
|
-
|
61
|
-
|
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.
|
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
|
|