jpablobr-freshbooks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/History.txt +8 -0
  3. data/LICENSE +10 -0
  4. data/Manifest.txt +44 -0
  5. data/README +65 -0
  6. data/Rakefile +19 -0
  7. data/lib/freshbooks.rb +94 -0
  8. data/lib/freshbooks/base.rb +168 -0
  9. data/lib/freshbooks/category.rb +11 -0
  10. data/lib/freshbooks/client.rb +20 -0
  11. data/lib/freshbooks/connection.rb +112 -0
  12. data/lib/freshbooks/estimate.rb +23 -0
  13. data/lib/freshbooks/expense.rb +12 -0
  14. data/lib/freshbooks/invoice.rb +25 -0
  15. data/lib/freshbooks/item.rb +11 -0
  16. data/lib/freshbooks/line.rb +10 -0
  17. data/lib/freshbooks/links.rb +7 -0
  18. data/lib/freshbooks/list_proxy.rb +70 -0
  19. data/lib/freshbooks/payment.rb +12 -0
  20. data/lib/freshbooks/project.rb +12 -0
  21. data/lib/freshbooks/recurring.rb +15 -0
  22. data/lib/freshbooks/response.rb +27 -0
  23. data/lib/freshbooks/schema/definition.rb +20 -0
  24. data/lib/freshbooks/schema/mixin.rb +40 -0
  25. data/lib/freshbooks/staff.rb +13 -0
  26. data/lib/freshbooks/task.rb +12 -0
  27. data/lib/freshbooks/time_entry.rb +12 -0
  28. data/lib/freshbooks/xml_serializer.rb +17 -0
  29. data/lib/freshbooks/xml_serializer/serializers.rb +107 -0
  30. data/script/console +10 -0
  31. data/script/destroy +14 -0
  32. data/script/generate +14 -0
  33. data/test/fixtures/invoice_create_response.xml +4 -0
  34. data/test/fixtures/invoice_get_response.xml +51 -0
  35. data/test/fixtures/invoice_list_response.xml +27 -0
  36. data/test/fixtures/success_response.xml +2 -0
  37. data/test/mock_connection.rb +13 -0
  38. data/test/schema/test_definition.rb +36 -0
  39. data/test/schema/test_mixin.rb +39 -0
  40. data/test/test_base.rb +97 -0
  41. data/test/test_connection.rb +83 -0
  42. data/test/test_helper.rb +32 -0
  43. data/test/test_invoice.rb +122 -0
  44. data/test/test_list_proxy.rb +41 -0
  45. data/test/test_page.rb +50 -0
  46. metadata +115 -0
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/xml_serializer/serializers'
2
+
3
+ module FreshBooks
4
+ module XmlSerializer
5
+ def self.to_value(node, type)
6
+ create_serializer(type).to_value(node)
7
+ end
8
+
9
+ def self.to_node(member_name, value, type)
10
+ create_serializer(type).to_node(member_name, value)
11
+ end
12
+
13
+ def self.create_serializer(type)
14
+ "FreshBooks::XmlSerializer::#{type.to_s.classify}Serializer".constantize
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,107 @@
1
+ module FreshBooks
2
+ module XmlSerializer
3
+ class FixnumSerializer
4
+ def self.to_node(member_name, value)
5
+ element = REXML::Element.new(member_name)
6
+ element.text = value.to_s
7
+ element
8
+ end
9
+
10
+ def self.to_value(xml_val)
11
+ xml_val.text.to_i
12
+ end
13
+ end
14
+
15
+ class FloatSerializer
16
+ def self.to_node(member_name, value)
17
+ element = REXML::Element.new(member_name)
18
+ element.text = value.to_s
19
+ element
20
+ end
21
+
22
+ def self.to_value(xml_val)
23
+ xml_val.text.to_f
24
+ end
25
+ end
26
+
27
+ class DateSerializer
28
+ def self.to_node(member_name, value)
29
+ element = REXML::Element.new(member_name)
30
+ element.text = value.to_s
31
+ element
32
+ end
33
+
34
+ def self.to_value(xml_val)
35
+ begin
36
+ Date.parse(xml_val.text.to_s)
37
+ rescue ArgumentError => e
38
+ # Sometimes freshbooks gives dates that look like this 0000-00-00 00:00:00
39
+ # just default to todays date, you have any other suggestions?
40
+ Date.new
41
+ end
42
+ end
43
+ end
44
+
45
+ class StringSerializer
46
+ def self.to_node(member_name, value)
47
+ element = REXML::Element.new(member_name)
48
+ element.text = value.to_s
49
+ element
50
+ end
51
+
52
+ def self.to_value(xml_val)
53
+ xml_val.text.to_s
54
+ end
55
+ end
56
+
57
+ class BooleanSerializer
58
+ def self.to_node(member_name, value)
59
+ element = REXML::Element.new(member_name)
60
+ element.text = value ? '1' : '0'
61
+ element
62
+ end
63
+
64
+ def self.to_value(xml_val)
65
+ xml_val.text.to_s == "1"
66
+ end
67
+ end
68
+
69
+ class ObjectSerializer
70
+ def self.to_node(member_name, value)
71
+ REXML::Document.new(value.to_xml(member_name))
72
+ end
73
+
74
+ def self.to_value(xml_val)
75
+ FreshBooks::const_get(xml_val.name.camelize)::new_from_xml(xml_val)
76
+ end
77
+ end
78
+
79
+ class ArraySerializer
80
+ def self.to_node(member_name, value)
81
+ element = REXML::Element.new(member_name)
82
+ value.each { |array_elem|
83
+ element.add_element(REXML::Document.new(array_elem.to_xml))
84
+ }
85
+ element
86
+ end
87
+
88
+ def self.to_value(xml_val)
89
+ xml_val.elements.map { |elem|
90
+ FreshBooks::const_get(elem.name.camelize)::new_from_xml(elem)
91
+ }
92
+ end
93
+ end
94
+
95
+ class DateTimeSerializer
96
+ def self.to_node(member_name, value)
97
+ element = REXML::Element.new(member_name)
98
+ element.text = value.to_s(:db)
99
+ element
100
+ end
101
+
102
+ def self.to_value(xml_val)
103
+ DateTime.parse(xml_val.text.to_s)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/freshbooks.rb'}"
9
+ puts "Loading freshbooks.rb gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok">
3
+ <invoice_id>1</invoice_id>
4
+ </response>
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0"?>
2
+ <response status="ok">
3
+ <invoice>
4
+ <invoice_id>1</invoice_id>
5
+ <client_id>1</client_id>
6
+ <number>number1</number>
7
+ <recurring_id>1</recurring_id>
8
+ <organization>Organization 1</organization>
9
+ <status>draft</status>
10
+ <amount>100.00</amount>
11
+ <amount_outstanding>50.00</amount_outstanding>
12
+ <date>2009-02-01</date>
13
+
14
+ <po_number>1</po_number>
15
+ <discount>1</discount>
16
+ <notes>notes1</notes>
17
+ <terms>terms1</terms>
18
+
19
+ <first_name>first_name1</first_name>
20
+ <last_name>last_name1</last_name>
21
+ <p_street1>p_street11</p_street1>
22
+ <p_street2>p_street21</p_street2>
23
+ <p_city>p_city1</p_city>
24
+ <p_state>p_state1</p_state>
25
+ <p_country>p_country1</p_country>
26
+ <p_code>p_code1</p_code>
27
+
28
+ <links>
29
+ <client_view>client_view1</client_view>
30
+ <view>view1</view>
31
+ <edit>edit1</edit>
32
+ </links>
33
+
34
+ <lines>
35
+ <line>
36
+ <amount>1</amount>
37
+ <name>name1</name>
38
+ <description>description1</description>
39
+ <unit_cost>1</unit_cost>
40
+ <quantity>1</quantity>
41
+ <tax1_name>tax1_name1</tax1_name>
42
+ <tax2_name>tax2_name1</tax2_name>
43
+ <tax1_percent>1</tax1_percent>
44
+ <tax2_percent>1</tax2_percent>
45
+ </line>
46
+ </lines>
47
+
48
+ <url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b</url>
49
+ <auth_url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b-z</auth_url>
50
+ </invoice>
51
+ </response>
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok">
3
+ <invoices page="1" per_page="2" pages="2" total="3">
4
+ <invoice>
5
+ <invoice_id>1</invoice_id>
6
+ <number>number1</number>
7
+ <client_id>1</client_id>
8
+ <recurring_id>1</recurring_id>
9
+ <organization>Organization 1</organization>
10
+ <status>draft</status>
11
+ <amount>100.00</amount>
12
+ <amount_outstanding>50.00</amount_outstanding>
13
+ <date>2009-02-01</date>
14
+ </invoice>
15
+ <invoice>
16
+ <invoice_id>2</invoice_id>
17
+ <number>number2</number>
18
+ <client_id>2</client_id>
19
+ <recurring_id>2</recurring_id>
20
+ <organization>Organization 2</organization>
21
+ <status>draft</status>
22
+ <amount>200.00</amount>
23
+ <amount_outstanding>100.00</amount_outstanding>
24
+ <date>2009-02-02</date>
25
+ </invoice>
26
+ </invoices>
27
+ </response>
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok"/>
@@ -0,0 +1,13 @@
1
+ require 'freshbooks/connection'
2
+
3
+ class MockConnection < FreshBooks::Connection
4
+ def initialize(response_body)
5
+ @response_body = response_body
6
+ end
7
+
8
+ protected
9
+
10
+ def post(request_body)
11
+ @response_body
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestDefinition < Test::Unit::TestCase
5
+ def setup
6
+ @definition = FreshBooks::Schema::Definition.new
7
+ end
8
+
9
+ def test_method_missing
10
+ # Empty
11
+ assert_equal 0, @definition.members.size
12
+
13
+ # One type
14
+ @definition.string :name
15
+ assert_equal 1, @definition.members.size
16
+ assert_equal({ :type => :string, :read_only => false }, @definition.members["name"])
17
+
18
+ # Multiple attributes
19
+ @definition.fixnum :version, :po_number
20
+ assert_equal 3, @definition.members.size
21
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["version"])
22
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["po_number"])
23
+
24
+ # Multiple times
25
+ @definition.fixnum :lock_number
26
+ assert_equal 4, @definition.members.size
27
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["lock_number"])
28
+ end
29
+
30
+ def test_method_missing_extra_options
31
+ @definition.fixnum :version, :po_number, :read_only => true
32
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["version"])
33
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["po_number"])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestMixin < Test::Unit::TestCase
5
+ def test_define_schema__unique_definition_per_class
6
+ assert MyItem.schema_definition.members.include?("name")
7
+ assert !MyItem.schema_definition.members.include?("name2")
8
+
9
+ assert MyItem2.schema_definition.members.include?("name2")
10
+ assert !MyItem2.schema_definition.members.include?("name")
11
+ end
12
+
13
+ def test_define_schema__creates_attr_accessors
14
+ assert MyItem.public_method_defined?("name")
15
+ assert MyItem.public_method_defined?("name=")
16
+
17
+ assert MyItem2.public_method_defined?("name2")
18
+ assert MyItem2.public_method_defined?("name2=")
19
+ end
20
+
21
+ def test_define_schema__creates_read_only_attr_accessors
22
+ assert MyItem.public_method_defined?("read_only_name")
23
+ assert MyItem.protected_method_defined?("read_only_name=")
24
+ end
25
+ end
26
+
27
+ class MyItem < FreshBooks::Base
28
+ define_schema do |s|
29
+ s.string :name
30
+ s.string :read_only_name, :read_only => true
31
+ end
32
+ end
33
+
34
+ class MyItem2 < FreshBooks::Base
35
+ define_schema do |s|
36
+ s.string :name2
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestBase < Test::Unit::TestCase
4
+ def test_establish_connection
5
+ assert_nil FreshBooks::Base.connection
6
+
7
+ FreshBooks::Base.establish_connection("company.freshbooks.com", "auth_token")
8
+ connection = FreshBooks::Base.connection
9
+ assert_not_nil connection
10
+ assert_equal "company.freshbooks.com", connection.account_url
11
+ assert_equal "auth_token", connection.auth_token
12
+
13
+ FreshBooks::Base.establish_connection("company2.freshbooks.com", "auth_token2")
14
+ connection = FreshBooks::Base.connection
15
+ assert_not_nil connection
16
+ assert_equal "company2.freshbooks.com", connection.account_url
17
+ assert_equal "auth_token2", connection.auth_token
18
+ end
19
+
20
+ def test_new_from_xml_to_xml__round_trip
21
+ xml = <<-EOS
22
+ <my_item>
23
+ <name>name1</name>
24
+ <amount>4.5</amount>
25
+ <read_only_number>5</read_only_number>
26
+ <number>6</number>
27
+ <visible>1</visible>
28
+ <date>2008-02-01</date>
29
+ <created_at>2008-10-22 13:57:00</created_at>
30
+ <my_address>
31
+ <street1>street1</street1>
32
+ </my_address>
33
+ <my_lines>
34
+ <my_line>
35
+ <description>description</description>
36
+ </my_line>
37
+ </my_lines>
38
+ </my_item>
39
+ EOS
40
+ doc = REXML::Document.new(xml)
41
+
42
+ item = FreshBooks::MyItem.new_from_xml(doc.root)
43
+ assert_equal "name1", item.name
44
+ assert_equal 5, item.read_only_number
45
+ assert_equal 6, item.number
46
+ assert_equal 4.5, item.amount
47
+ assert_equal true, item.visible
48
+ assert_equal Date.new(2008, 2, 1), item.date
49
+ assert_equal DateTime.new(2008, 10, 22, 13, 57, 0), item.created_at
50
+ assert_equal "street1", item.my_address.street1
51
+ assert_equal 1, item.my_lines.size
52
+ assert_equal "description", item.my_lines.first.description
53
+
54
+ # If someone knows a good way to compare xml docs where ordering doesn't matter
55
+ # let me know. This can be refactored and improved greatly.
56
+ xml_out = item.to_xml.to_s.strip
57
+ assert_equal "<my_item>", xml_out.first(9)
58
+ assert xml_out.include?("<name>name1</name>")
59
+ assert xml_out.include?("<amount>4.5</amount>")
60
+ assert xml_out.include?("<visible>1</visible>")
61
+ assert !xml_out.include?("<read_only_number>5</read_only_number>") # this is read only
62
+ assert xml_out.include?("<number>6</number>")
63
+ assert xml_out.include?("<date>2008-02-01</date>")
64
+ assert xml_out.include?("<created_at>2008-10-22 13:57:00</created_at>")
65
+ assert xml_out.include?("<my_address><street1>street1</street1></my_address>")
66
+ assert xml_out.include?("<my_lines><my_line><description>description</description></my_line></my_lines>")
67
+ end
68
+
69
+ end
70
+
71
+ module FreshBooks
72
+ class MyItem < FreshBooks::Base
73
+ define_schema do |s|
74
+ s.string :name
75
+ s.fixnum :read_only_number, :read_only => true
76
+ s.fixnum :number
77
+ s.float :amount
78
+ s.boolean :visible
79
+ s.date :date
80
+ s.date_time :created_at
81
+ s.object :my_address
82
+ s.array :my_lines
83
+ end
84
+ end
85
+
86
+ class MyAddress < FreshBooks::Base
87
+ define_schema do |s|
88
+ s.string :street1
89
+ end
90
+ end
91
+
92
+ class MyLine < FreshBooks::Base
93
+ define_schema do |s|
94
+ s.string :description
95
+ end
96
+ end
97
+ end