gmoney 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Manifest CHANGED
@@ -3,6 +3,7 @@ README.rdoc
3
3
  Rakefile
4
4
  gmoney.gemspec
5
5
  lib/extensions/fixnum.rb
6
+ lib/extensions/nil_class.rb
6
7
  lib/extensions/string.rb
7
8
  lib/gmoney.rb
8
9
  lib/gmoney/authentication_request.rb
@@ -21,6 +22,8 @@ spec/authentication_request_spec.rb
21
22
  spec/fixtures/cacert.pem
22
23
  spec/fixtures/default_portfolios_feed.xml
23
24
  spec/fixtures/empty_portfolio_feed.xml
25
+ spec/fixtures/new_portfolio_feed.xml
26
+ spec/fixtures/new_transaction_feed.xml
24
27
  spec/fixtures/portfolio_9_feed.xml
25
28
  spec/fixtures/portfolio_feed_with_returns.xml
26
29
  spec/fixtures/position_feed_for_9_GOOG.xml
@@ -29,7 +32,9 @@ spec/fixtures/positions_feed_for_portfolio_9.xml
29
32
  spec/fixtures/positions_feed_for_portfolio_9r.xml
30
33
  spec/fixtures/transaction_feed_for_GOOG_1.xml
31
34
  spec/fixtures/transactions_feed_for_GOOG.xml
35
+ spec/fixtures/updated_portfolio_feed.xml
32
36
  spec/gmoney_spec.rb
37
+ spec/nil_class_spec.rb
33
38
  spec/portfolio_feed_parser_spec.rb
34
39
  spec/portfolio_spec.rb
35
40
  spec/position_feed_parser_spec.rb
@@ -9,61 +9,88 @@ A gem for interacting with the Google Finance API
9
9
  == Usage
10
10
 
11
11
  Login
12
-
12
+ -----
13
13
 
14
14
  GMoney::GFSession.login('google username', 'password')
15
15
 
16
16
  Portfolios
17
-
17
+ --------
18
18
 
19
19
  Reading
20
- portfolios = GMoney::Portfolio.all #returns all of a users portfolios
21
- portfolio = GMoney::Portfolio.find(9) #returns a specific portfolio
20
+ > portfolios = GMoney::Portfolio.all #returns all of a users portfolios
21
+ > portfolio = GMoney::Portfolio.find(9) #returns a specific portfolio
22
+
23
+ > positions = portfolio.positions #returns an Array of the Positions held within a given portfolio
22
24
 
23
- positions = portfolio.positions #returns an Array of the Positions held within a given portfolio
25
+ Creating
26
+ > portfolio = GMoney::Portfolio.new
27
+ > portfolio.title = "My New Portfolio"
28
+ > portfolio.save #returns portfolio object
29
+
30
+ Updating
31
+ > portfolio = GMoney::Portfolio.find 9
32
+ > portfolio.title = "My Updated Portfolio Title"
33
+ > portfolio.save #returns portfolio object
24
34
 
25
35
  Deleting
26
- GMoney::Portfolio.delete 2
36
+ > GMoney::Portfolio.delete 2
27
37
 
28
38
  or
29
39
 
30
- portfolio = GMoney::Portfolio.find 2
31
- portfolio.delete #call delete on an instance of a portfolio
40
+ > portfolio = GMoney::Portfolio.find 2
41
+ > portfolio.delete #call delete on an instance of a portfolio
32
42
 
33
43
 
34
44
  Positions
35
-
45
+ --------
36
46
 
37
47
  Reading
38
- positions = GMoney::Position.find(9) #returns all of a users positions within a given portfolio, i.e. Portfolio "9"
39
- position = GMoney::Position.find("9/NASDAQ:GOOG") #returns a specific position within a given portfolio
48
+ > positions = GMoney::Position.find(9) #returns all of a users positions within a given portfolio, i.e. Portfolio "9"
49
+ > position = GMoney::Position.find("9/NASDAQ:GOOG") #returns a specific position within a given portfolio
50
+
51
+ > transactions = position.transactions #returns an Array of the Transactions within a given position
40
52
 
41
- transactions = position.transactions #returns an Array of the Transactions within a given position
53
+ Creating/Updating
54
+ Positions are created/updated via transactions
42
55
 
43
56
  Deleting
44
- GMoney::Position.delete '2/NASDAQ:GOOG'
57
+ > GMoney::Position.delete '2/NASDAQ:GOOG'
45
58
 
46
59
  or
47
60
 
48
- position = GMoney::Position.find '2/NASDAQ:GOOG'
49
- position.delete #call delete on an instance of a position
61
+ > position = GMoney::Position.find '2/NASDAQ:GOOG'
62
+ > position.delete #call delete on an instance of a position
50
63
 
51
- Transactions
52
64
 
65
+ Transactions
66
+ --------
53
67
 
54
68
  Reading
55
- transactions = GMoney::Transaction.find("9/NASDAQ:GOOG") #returns all of a users transactions within a given position
56
- transaction = GMoney::Transaction.find("9/NASDAQ:GOOG/2") #returns a specific transaction within a given position
69
+ > transactions = GMoney::Transaction.find("9/NASDAQ:GOOG") #returns all of a users transactions within a given position
70
+ > transaction = GMoney::Transaction.find("9/NASDAQ:GOOG/2") #returns a specific transaction within a given position
71
+
72
+ Creating
73
+ > transaction = GMoney::Transaction.new
74
+ > transaction.portfolio = 9 #Must be a valid portfolio id
75
+ > transaction.ticker = 'nyse:c' #Must be a valid ticker symbol
76
+ > transaction.type = 'Buy' #Must be one of the following: Buy, Sell, Sell Short, Buy to Cover
77
+ > transaction.save #returns transaction object
78
+
79
+ Updating
80
+ > transaction = GMoney::Transaction.find('9/NYSE:C/1')
81
+ > transaction.shares = 50
82
+ > transaction.price = 3.50
83
+ > transaction.save #returns transaction object
57
84
 
58
85
  Deleting
59
- GMoney::Transaction.delete '2/NASDAQ:GOOG/1'
86
+ > GMoney::Transaction.delete '2/NASDAQ:GOOG/1'
60
87
 
61
88
  or
62
89
 
63
- transaction = GMoney::Transaction.find '2/NASDAQ:GOOG/1'
64
- transaction.delete #call delete on an instance of a transaction
90
+ > transaction = GMoney::Transaction.find '2/NASDAQ:GOOG/1'
91
+ > transaction.delete #call delete on an instance of a transaction
65
92
 
66
-
93
+ ----------
67
94
 
68
95
  Options parameter
69
96
 
@@ -81,8 +108,11 @@ Options parameter
81
108
  all take a hash as an optional last parameter. Valid values for this hash are:
82
109
 
83
110
  * :returns - By default Google does not return a comprehensive list of returns data. Set this parameter to true to access this information.
84
- * :refresh - GMoney caches the data returned from Google by default. Set :refresh to true if you would like to get the latest data.
85
- * :eager - GMoney lazily loads children data (i.e. positions for a given Portfolio). If you would like to load all of this data at once set :eager to true
111
+ * :refresh - GMoney caches the data returned from Google by default. Set :refresh => true if you would like to get the latest data.
112
+ * :eager - GMoney lazily loads children data (i.e. positions for a given Portfolio). If you would like to load all of this data at once set :eager => true
113
+
114
+ ----------
115
+ There are still some rough edges. Feedback is appreciated.
86
116
 
87
117
  == License
88
118
 
@@ -2,17 +2,17 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{gmoney}
5
- s.version = "0.2.1"
5
+ s.version = "0.4.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Justin Spradlin"]
9
- s.date = %q{2009-11-14}
9
+ s.date = %q{2009-11-17}
10
10
  s.description = %q{A gem for interacting with the Google Finance API}
11
11
  s.email = %q{jspradlin@gmail.com}
12
- s.extra_rdoc_files = ["README.rdoc", "lib/extensions/fixnum.rb", "lib/extensions/string.rb", "lib/gmoney.rb", "lib/gmoney/authentication_request.rb", "lib/gmoney/feed_parser.rb", "lib/gmoney/gf_request.rb", "lib/gmoney/gf_response.rb", "lib/gmoney/gf_service.rb", "lib/gmoney/gf_session.rb", "lib/gmoney/portfolio.rb", "lib/gmoney/portfolio_feed_parser.rb", "lib/gmoney/position.rb", "lib/gmoney/position_feed_parser.rb", "lib/gmoney/transaction.rb", "lib/gmoney/transaction_feed_parser.rb"]
13
- s.files = ["Manifest", "README.rdoc", "Rakefile", "gmoney.gemspec", "lib/extensions/fixnum.rb", "lib/extensions/string.rb", "lib/gmoney.rb", "lib/gmoney/authentication_request.rb", "lib/gmoney/feed_parser.rb", "lib/gmoney/gf_request.rb", "lib/gmoney/gf_response.rb", "lib/gmoney/gf_service.rb", "lib/gmoney/gf_session.rb", "lib/gmoney/portfolio.rb", "lib/gmoney/portfolio_feed_parser.rb", "lib/gmoney/position.rb", "lib/gmoney/position_feed_parser.rb", "lib/gmoney/transaction.rb", "lib/gmoney/transaction_feed_parser.rb", "spec/authentication_request_spec.rb", "spec/fixtures/cacert.pem", "spec/fixtures/default_portfolios_feed.xml", "spec/fixtures/empty_portfolio_feed.xml", "spec/fixtures/portfolio_9_feed.xml", "spec/fixtures/portfolio_feed_with_returns.xml", "spec/fixtures/position_feed_for_9_GOOG.xml", "spec/fixtures/positions_feed_for_portfolio_14.xml", "spec/fixtures/positions_feed_for_portfolio_9.xml", "spec/fixtures/positions_feed_for_portfolio_9r.xml", "spec/fixtures/transaction_feed_for_GOOG_1.xml", "spec/fixtures/transactions_feed_for_GOOG.xml", "spec/gmoney_spec.rb", "spec/portfolio_feed_parser_spec.rb", "spec/portfolio_spec.rb", "spec/position_feed_parser_spec.rb", "spec/position_spec.rb", "spec/request_spec.rb", "spec/response_spec.rb", "spec/service_spec.rb", "spec/session_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/string_spec.rb", "spec/transaction_feed_parser_spec.rb", "spec/transaction_spec.rb"]
12
+ s.extra_rdoc_files = ["README.rdoc", "lib/extensions/fixnum.rb", "lib/extensions/nil_class.rb", "lib/extensions/string.rb", "lib/gmoney.rb", "lib/gmoney/authentication_request.rb", "lib/gmoney/feed_parser.rb", "lib/gmoney/gf_request.rb", "lib/gmoney/gf_response.rb", "lib/gmoney/gf_service.rb", "lib/gmoney/gf_session.rb", "lib/gmoney/portfolio.rb", "lib/gmoney/portfolio_feed_parser.rb", "lib/gmoney/position.rb", "lib/gmoney/position_feed_parser.rb", "lib/gmoney/transaction.rb", "lib/gmoney/transaction_feed_parser.rb"]
13
+ s.files = ["Manifest", "README.rdoc", "Rakefile", "gmoney.gemspec", "lib/extensions/fixnum.rb", "lib/extensions/nil_class.rb", "lib/extensions/string.rb", "lib/gmoney.rb", "lib/gmoney/authentication_request.rb", "lib/gmoney/feed_parser.rb", "lib/gmoney/gf_request.rb", "lib/gmoney/gf_response.rb", "lib/gmoney/gf_service.rb", "lib/gmoney/gf_session.rb", "lib/gmoney/portfolio.rb", "lib/gmoney/portfolio_feed_parser.rb", "lib/gmoney/position.rb", "lib/gmoney/position_feed_parser.rb", "lib/gmoney/transaction.rb", "lib/gmoney/transaction_feed_parser.rb", "spec/authentication_request_spec.rb", "spec/fixtures/cacert.pem", "spec/fixtures/default_portfolios_feed.xml", "spec/fixtures/empty_portfolio_feed.xml", "spec/fixtures/new_portfolio_feed.xml", "spec/fixtures/new_transaction_feed.xml", "spec/fixtures/portfolio_9_feed.xml", "spec/fixtures/portfolio_feed_with_returns.xml", "spec/fixtures/position_feed_for_9_GOOG.xml", "spec/fixtures/positions_feed_for_portfolio_14.xml", "spec/fixtures/positions_feed_for_portfolio_9.xml", "spec/fixtures/positions_feed_for_portfolio_9r.xml", "spec/fixtures/transaction_feed_for_GOOG_1.xml", "spec/fixtures/transactions_feed_for_GOOG.xml", "spec/fixtures/updated_portfolio_feed.xml", "spec/gmoney_spec.rb", "spec/nil_class_spec.rb", "spec/portfolio_feed_parser_spec.rb", "spec/portfolio_spec.rb", "spec/position_feed_parser_spec.rb", "spec/position_spec.rb", "spec/request_spec.rb", "spec/response_spec.rb", "spec/service_spec.rb", "spec/session_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/string_spec.rb", "spec/transaction_feed_parser_spec.rb", "spec/transaction_spec.rb"]
14
14
  s.homepage = %q{http://github.com/jspradlin/gmoney}
15
- s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Gmoney", "--main", "README.rdoc~"]
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Gmoney", "--main", "README.rdoc"]
16
16
  s.require_paths = ["lib"]
17
17
  s.rubyforge_project = %q{gmoney}
18
18
  s.rubygems_version = %q{1.3.4}
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+ end
@@ -67,4 +67,8 @@ class String
67
67
  raise TransactionParseError
68
68
  end
69
69
  end
70
+
71
+ def blank?
72
+ respond_to?(:empty?) ? self.strip.empty? : !self
73
+ end
70
74
  end
@@ -11,6 +11,7 @@ require 'net/https'
11
11
  require 'rexml/document'
12
12
 
13
13
  require 'extensions/fixnum'
14
+ require 'extensions/nil_class'
14
15
  require 'extensions/string'
15
16
 
16
17
  require 'gmoney/authentication_request'
@@ -27,7 +28,7 @@ require 'gmoney/transaction'
27
28
  require 'gmoney/transaction_feed_parser'
28
29
 
29
30
  module GMoney
30
- VERSION = '0.2.1'
31
+ VERSION = '0.4.0'
31
32
  GF_URL = "https://finance.google.com/finance"
32
33
  GF_FEED_URL = "#{GF_URL}/feeds/default"
33
34
  GF_PORTFOLIO_FEED_URL = "#{GF_FEED_URL}/portfolios"
@@ -14,9 +14,6 @@ module GMoney
14
14
  finance_object = feed_class.new
15
15
  finance_data = parsed_entry.elements["gf:#{feed_class_string}Data"]
16
16
 
17
- #TODO - have someone peer review this. Is it bad practice to use instance_variable_set because
18
- #it breaks encapsulation? (Even though it actually enhances the encapsulation in the "domain" classes
19
- #by not allowing users to set attributes that should be read only (i.e. id, updated, return1w))
20
17
  finance_object.instance_variable_set("@id", parsed_entry.elements['id'].text)
21
18
  finance_object.instance_variable_set("@title", parsed_entry.elements['title'].text)
22
19
  finance_object.instance_variable_set("@updated", DateTime.parse(parsed_entry.elements['updated'].text))
@@ -25,7 +22,6 @@ module GMoney
25
22
  finance_data.attributes.each { |attr_name, attr_value| set_ivar.call(finance_object, attr_name, attr_value) }
26
23
  parsed_entry.elements['gf:symbol'].each { |attr_name, attr_value| set_ivar.call(finance_object, attr_name, attr_value)} if options[:symbol]
27
24
 
28
- #TODO - This is only going to work for USD for now. Might need to updated to make a "Money" object to store amount and currency code.
29
25
  finance_data.elements.each do |cg|
30
26
  finance_object.instance_variable_set("@#{cg.name.camel_to_us}", cg.elements['gd:money'].attributes['amount'].to_f)
31
27
  end
@@ -2,6 +2,7 @@ module GMoney
2
2
  class Portfolio
3
3
  class PortfolioRequestError < StandardError;end
4
4
  class PortfolioDeleteError < StandardError;end
5
+ class PortfolioSaveError < StandardError;end
5
6
 
6
7
  attr_accessor :title, :currency_code
7
8
 
@@ -27,6 +28,10 @@ module GMoney
27
28
  @positions.is_a?(Array) ? @positions : [@positions]
28
29
  end
29
30
 
31
+ def save
32
+ save_portfolio
33
+ end
34
+
30
35
  def self.delete(id)
31
36
  delete_portfolio(id)
32
37
  end
@@ -56,16 +61,44 @@ module GMoney
56
61
 
57
62
  portfolios
58
63
  end
64
+
65
+ def save_portfolio
66
+ raise PortfolioSaveError, 'Portfolios must have a title' if @title.blank?
67
+
68
+ @currency_code ||= 'USD'
69
+
70
+ #Rcov hates multi-line strings and I hate red on my test coverage report.
71
+ atom_string = "<?xml version='1.0'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:gf='http://schemas.google.com/finance/2007' xmlns:gd='http://schemas.google.com/g/2005'><title type='text'>#{title}</title> <gf:portfolioData currencyCode='#{currency_code}'/></entry>"
72
+
73
+ url = @id ? @id : GF_PORTFOLIO_FEED_URL
74
+
75
+ #Some firewalls block HTTP PUT messages. To get around this, you can include a
76
+ #X-HTTP-Method-Override: PUT header in a POST request
77
+ headers = {"Authorization" => "GoogleLogin auth=#{GFSession.auth_token}", "Content-Type" => "application/atom+xml"}
78
+ headers["X-HTTP-Method-Override"] = "PUT" if @id #if there is already an @id defined then we are updating a portfolio
79
+
80
+ request = GFRequest.new(url, :method => :post, :body => atom_string, :headers => headers)
81
+
82
+ response = GFService.send_request(request)
83
+
84
+ if response.status_code == HTTPCreated || response.status_code == HTTPOK
85
+ portfolio = PortfolioFeedParser.parse_portfolio_feed(response.body)[0]
86
+ self.instance_variable_set("@id", portfolio.id) if response.status_code == HTTPCreated
87
+ else
88
+ raise PortfolioSaveError, response.body
89
+ end
90
+ portfolio
91
+ end
59
92
 
60
- #If you are working behind some firewalls HTTP DELETE request won't work.
61
- #To overcome this problem the google doc say to use a post request with
62
- #the X-HTTP-Method-Override set to "DELETE"
93
+ #Some firewalls block HTTP DELETE messages. To get around this, you can include a
94
+ #X-HTTP-Method-Override: DELETE header in a POST request
63
95
  def self.delete_portfolio(id)
64
96
  url = "#{GF_PORTFOLIO_FEED_URL}/#{id}"
65
97
  response = GFService.send_request(GFRequest.new(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=#{GFSession.auth_token}", "X-HTTP-Method-Override" => "DELETE"}))
66
98
  raise PortfolioDeleteError, response.body if response.status_code != HTTPOK
67
99
  end
68
100
 
101
+ private :save_portfolio
69
102
  private_class_method :retreive_portfolios, :delete_portfolio
70
103
  end
71
104
  end
@@ -2,15 +2,19 @@ module GMoney
2
2
  class Transaction
3
3
  class TransactionRequestError < StandardError; end
4
4
  class TransactionDeleteError < StandardError;end
5
+ class TransactionSaveError < StandardError;end
5
6
 
6
7
  attr_reader :id, :updated, :title
7
-
8
- attr_accessor :type, :date, :shares, :notes, :commission, :price
8
+ attr_accessor :type, :date, :shares, :notes, :commission, :price, :portfolio, :ticker, :currency_code
9
9
 
10
10
  def self.find(id, options={})
11
11
  find_by_url("#{GF_PORTFOLIO_FEED_URL}/#{id.portfolio_id}/positions/#{id.position_id}/transactions/#{id.transaction_id}", options)
12
12
  end
13
13
 
14
+ def save
15
+ save_transaction
16
+ end
17
+
14
18
  def self.delete(id)
15
19
  delete_transaction(id)
16
20
  end
@@ -34,7 +38,7 @@ module GMoney
34
38
  return transactions[0] if transactions.size == 1
35
39
 
36
40
  transactions
37
- end
41
+ end
38
42
 
39
43
  #If you are working behind some firewalls DELETE HTTP request won't work.
40
44
  #To overcome this problem the google doc say to use a post request with
@@ -45,6 +49,53 @@ module GMoney
45
49
  raise TransactionDeleteError, response.body if response.status_code != HTTPOK
46
50
  end
47
51
 
52
+ def save_transaction
53
+ raise TransactionSaveError, "You must include a portfolio id, ticker symbol, and transaction type ['Buy', 'Sell', 'Buy to Cover', 'Sell Short'] in order to create a transaction." if !is_valid_transaction?
54
+
55
+ @currency_code ||= 'USD'
56
+ @date ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S.000")
57
+ @shares ||= 0
58
+
59
+ atom_string = "<?xml version='1.0'?>
60
+ <entry xmlns='http://www.w3.org/2005/Atom'
61
+ xmlns:gf='http://schemas.google.com/finance/2007'
62
+ xmlns:gd='http://schemas.google.com/g/2005'>
63
+ <gf:transactionData date='#{@date}' shares='#{@shares}' type='#{@type}'>"
64
+
65
+ atom_string += "<gf:commission><gd:money amount='#{@commission}' currencyCode='#{@currency_code}'/></gf:commission>" if @commission
66
+ atom_string += "<gf:price><gd:money amount='#{@price}' currencyCode='#{@currency_code}'/></gf:price>" if @price
67
+ atom_string += "</gf:transactionData></entry>"
68
+
69
+ url = @id ? @id : "#{GF_PORTFOLIO_FEED_URL}/#{@portfolio}/positions/#{@ticker}/transactions"
70
+
71
+ #Some firewalls block HTTP PUT messages. To get around this, you can include a
72
+ #X-HTTP-Method-Override: PUT header in a POST request
73
+ headers = {"Authorization" => "GoogleLogin auth=#{GFSession.auth_token}", "Content-Type" => "application/atom+xml"}
74
+ headers["X-HTTP-Method-Override"] = "PUT" if @id #if there is already an @id defined then we are updating a transaction
75
+
76
+ request = GFRequest.new(url, :method => :post, :body => atom_string, :headers => headers)
77
+
78
+ response = GFService.send_request(request)
79
+
80
+ if response.status_code == HTTPCreated || response.status_code == HTTPOK
81
+ transaction = TransactionFeedParser.parse_transaction_feed(response.body)[0]
82
+ self.instance_variable_set("@id", transaction.id) if response.status_code == HTTPCreated
83
+ transaction.portfolio = @portfolio if response.status_code == HTTPCreated
84
+ else
85
+ raise TransactionSaveError, response.body
86
+ end
87
+ transaction
88
+ end
89
+
90
+ def is_valid_transaction?
91
+ !(@portfolio.to_s.blank? || @ticker.blank? || !is_valid_transaction_type?)
92
+ end
93
+
94
+ def is_valid_transaction_type?
95
+ ['Buy', 'Sell', 'Buy to Cover', 'Sell Short'].include?(@type)
96
+ end
97
+
98
+ private :save_transaction, :is_valid_transaction?, :is_valid_transaction_type?
48
99
  private_class_method :find_by_url, :delete_transaction
49
100
  end
50
101
  end
@@ -0,0 +1,11 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <entry xmlns='http://www.w3.org/2005/Atom' xmlns:gf='http://schemas.google.com/finance/2007' xmlns:gd='http://schemas.google.com/g/2005'>
3
+ <id>http://finance.google.com/finance/feeds/user@example.com/portfolios/38</id>
4
+ <updated>2009-11-14T21:52:46.000Z</updated>
5
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/finance/2007#portfolio' />
6
+ <title type='text'>New Portfolio</title>
7
+ <link rel='self' type='application/atom+xml' href='http://finance.google.com/finance/feeds/default/portfolios/38' />
8
+ <link rel='edit' type='application/atom+xml' href='http://finance.google.com/finance/feeds/default/portfolios/38' />
9
+ <gd:feedLink href='http://finance.google.com/finance/feeds/user@example.com/portfolios/38/positions' />
10
+ <gf:portfolioData currencyCode='USD' gainPercentage='0.0' return1w='0.0' return1y='0.0' return3m='0.0' return3y='0.0' return4w='0.0' return5y='0.0' returnOverall='0.0' returnYTD='0.0' />
11
+ </entry>
@@ -0,0 +1,17 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <entry xmlns='http://www.w3.org/2005/Atom' xmlns:gf='http://schemas.google.com/finance/2007' xmlns:gd='http://schemas.google.com/g/2005'>
3
+ <id>http://finance.google.com/finance/feeds/user@example.com/portfolios/9/positions/NASDAQ:GOOG/transactions/12</id>
4
+ <updated>2009-11-18T01:44:50.000Z</updated>
5
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/finance/2007#transaction' />
6
+ <title type='text'>12</title>
7
+ <link rel='self' type='application/atom+xml' href='http://finance.google.com/finance/feeds/default/portfolios/9/positions/NASDAQ%3AGOOG/transactions/12' />
8
+ <link rel='edit' type='application/atom+xml' href='http://finance.google.com/finance/feeds/default/portfolios/9/positions/NASDAQ%3AGOOG/transactions/12' />
9
+ <gf:transactionData date='2009-11-17T00:00:00.000' shares='50.0' type='Buy'>
10
+ <gf:commission>
11
+ <gd:money amount='20.0' currencyCode='USD' />
12
+ </gf:commission>
13
+ <gf:price>
14
+ <gd:money amount='450.0' currencyCode='USD' />
15
+ </gf:price>
16
+ </gf:transactionData>
17
+ </entry>
@@ -0,0 +1,13 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <entry xmlns='http://www.w3.org/2005/Atom' xmlns:gf='http://schemas.google.com/finance/2007' xmlns:gd='http://schemas.google.com/g/2005'>
3
+ <id>http://finance.google.com/finance/feeds/user@example.com/portfolios/38</id>
4
+ <updated>2009-11-14T22:12:15.000Z</updated>
5
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/finance/2007#portfolio' />
6
+ <title type='text'>Updated Title</title>
7
+ <link rel='self' type='application/atom+xml' href='http://finance.google.com/finance/feeds/user@example.com/portfolios/38' />
8
+ <link rel='edit' type='application/atom+xml' href='http://finance.google.com/finance/feeds/user@example.com/portfolios/38' />
9
+ <gd:feedLink href='http://finance.google.com/finance/feeds/user@example.com/portfolios/38/positions' />
10
+ <gf:portfolioData currencyCode='USD' gainPercentage='0.0' return1w='0.0' return1y='0.0' return3m='0.0' return3y='0.0' return4w='0.0' return5y='0.0' returnOverall='0.0' returnYTD='0.0' />
11
+ </entry>
12
+
13
+
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
2
+
3
+ describe NilClass do
4
+ it "should return true for a method call to blank?" do
5
+ nil.blank?.should be_true
6
+ end
7
+ end
@@ -5,7 +5,9 @@ describe GMoney::Portfolio do
5
5
  @default_feed = File.read('spec/fixtures/default_portfolios_feed.xml')
6
6
  @feed_with_returns = File.read('spec/fixtures/portfolio_feed_with_returns.xml')
7
7
  @empty_feed = File.read('spec/fixtures/empty_portfolio_feed.xml')
8
- @portfolio_9_feed = File.read('spec/fixtures/portfolio_9_feed.xml')
8
+ @portfolio_9_feed = File.read('spec/fixtures/portfolio_9_feed.xml')
9
+ @new_portfolio_feed = File.read('spec/fixtures/new_portfolio_feed.xml')
10
+ @updated_portfolio_feed = File.read('spec/fixtures/updated_portfolio_feed.xml')
9
11
  end
10
12
 
11
13
  before(:each) do
@@ -133,7 +135,58 @@ describe GMoney::Portfolio do
133
135
  portfolio_delete_helper("#{@url}/asdf")
134
136
 
135
137
  lambda { GMoney::Portfolio.delete("asdf") }.should raise_error(GMoney::Portfolio::PortfolioDeleteError, @gf_response.body)
136
- end
138
+ end
139
+
140
+ it "should save a portfolio" do
141
+ portfolio = GMoney::Portfolio.new
142
+ portfolio.title = "New Portfolio"
143
+
144
+ @gf_response.status_code = 200
145
+ @gf_response.body = @new_portfolio_feed
146
+
147
+ portfolio_save_helper(portfolio)
148
+
149
+ portfolio_return = portfolio.save
150
+
151
+ portfolio_return.id.should be_eql('http://finance.google.com/finance/feeds/user@example.com/portfolios/38')
152
+ portfolio_return.title.should be_eql("New Portfolio")
153
+ end
154
+
155
+ it "should update a portfolio when an @id is already set" do
156
+ portfolio = GMoney::Portfolio.new
157
+ portfolio.title = "New Portfolio"
158
+ portfolio.instance_variable_set("@id", "http://finance.google.com/finance/feeds/user@example.com/portfolios/38")
159
+
160
+ @gf_response.status_code = 201
161
+ @gf_response.body = @new_portfolio_feed
162
+
163
+ portfolio_save_helper(portfolio)
164
+
165
+ portfolio.save
166
+ end
167
+
168
+ it "should raise a PortfolioSaveError if the portfolio title is empty or nil" do
169
+ portfolio = GMoney::Portfolio.new
170
+ lambda { portfolio.save }.should raise_error(GMoney::Portfolio::PortfolioSaveError, 'Portfolios must have a title')
171
+
172
+ portfolio.title = ""
173
+ lambda { portfolio.save }.should raise_error(GMoney::Portfolio::PortfolioSaveError, 'Portfolios must have a title')
174
+
175
+ portfolio.title = " "
176
+ lambda { portfolio.save }.should raise_error(GMoney::Portfolio::PortfolioSaveError, 'Portfolios must have a title')
177
+ end
178
+
179
+ it "should raise a PortfolioSaveError if a portfolio with the same title already exists" do
180
+ portfolio = GMoney::Portfolio.new
181
+ portfolio.title = "New Portfolio" #already exists
182
+
183
+ @gf_response.status_code = 400
184
+ @gf_response.body = 'A Portfolio with this name already exists.'
185
+
186
+ portfolio_save_helper(portfolio)
187
+
188
+ lambda { portfolio.save }.should raise_error(GMoney::Portfolio::PortfolioSaveError, 'A Portfolio with this name already exists.')
189
+ end
137
190
 
138
191
  def portfolio_helper(url, id = nil, options = {})
139
192
  GMoney::GFSession.should_receive(:auth_token).and_return('toke')
@@ -158,5 +211,23 @@ describe GMoney::Portfolio do
158
211
  GMoney::GFRequest.should_receive(:new).with(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=toke", "X-HTTP-Method-Override" => "DELETE"}).and_return(@gf_request)
159
212
 
160
213
  GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
161
- end
214
+ end
215
+
216
+ def portfolio_save_helper(portfolio)
217
+ title = portfolio.title
218
+ currency_code = portfolio.currency_code ? portfolio.currency_code : 'USD'
219
+
220
+ atom_string = "<?xml version='1.0'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:gf='http://schemas.google.com/finance/2007' xmlns:gd='http://schemas.google.com/g/2005'><title type='text'>#{title}</title> <gf:portfolioData currencyCode='#{currency_code}'/></entry>"
221
+
222
+ url = portfolio.id ? portfolio.id : GMoney::GF_PORTFOLIO_FEED_URL
223
+
224
+ GMoney::GFSession.should_receive(:auth_token).and_return('toke')
225
+
226
+ headers = {"Authorization" => "GoogleLogin auth=toke", "Content-Type" => "application/atom+xml"}
227
+ headers["X-HTTP-Method-Override"] = "PUT" if portfolio.id
228
+
229
+ GMoney::GFRequest.should_receive(:new).with(url, :method => :post, :body => atom_string, :headers => headers).and_return(@gf_request)
230
+
231
+ GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
232
+ end
162
233
  end
@@ -81,4 +81,12 @@ describe String do
81
81
  lambda {"123/NASDAQ:GOOG/2134/23".transaction_id}.should raise_error(String::TransactionParseError)
82
82
  "123/NASDAQ:GOOG/23".transaction_id.should be_eql("23")
83
83
  end
84
+
85
+ it "should be able to detect blank strings" do
86
+ nil.blank?.should be_true
87
+ "".blank?.should be_true
88
+ " ".blank?.should be_true
89
+ " a ".blank?.should be_false
90
+ "234".blank?.should be_false
91
+ end
84
92
  end
@@ -4,6 +4,7 @@ describe GMoney::Transaction do
4
4
  before(:all) do
5
5
  @goog_feed = File.read('spec/fixtures/transactions_feed_for_GOOG.xml')
6
6
  @goog_feed_1 = File.read('spec/fixtures/transaction_feed_for_GOOG_1.xml')
7
+ @new_transaction_feed = File.read('spec/fixtures/new_transaction_feed.xml')
7
8
  end
8
9
 
9
10
  before(:each) do
@@ -85,6 +86,140 @@ describe GMoney::Transaction do
85
86
  lambda { GMoney::Transaction.delete("9/NASDAQ:GOOG/24") }.should raise_error(GMoney::Transaction::TransactionDeleteError, @gf_response.body)
86
87
  end
87
88
 
89
+ it "should create only valid transactions" do
90
+ trans = GMoney::Transaction.new
91
+
92
+ #Make is_valid_transaction? method public for testing purposes
93
+ def trans.public_is_valid_transaction?(*args)
94
+ is_valid_transaction?(*args)
95
+ end
96
+
97
+ trans.portfolio = 1
98
+ trans.ticker = "NYSE:GLD"
99
+ trans.type = 'Buy'
100
+ trans.public_is_valid_transaction?.should be_true
101
+
102
+ trans.type = 'Sell'
103
+ trans.public_is_valid_transaction?.should be_true
104
+
105
+ trans.type = 'Buy to Cover'
106
+ trans.public_is_valid_transaction?.should be_true
107
+
108
+ trans.type = 'Sell Short'
109
+ trans.public_is_valid_transaction?.should be_true
110
+
111
+ trans.portfolio = 1
112
+ trans.ticker = "NYSE:GLD"
113
+ trans.type = nil
114
+ trans.public_is_valid_transaction?.should be_false
115
+
116
+ trans.portfolio = 1
117
+ trans.ticker = "NYSE:GLD"
118
+ trans.type = 'buy'
119
+ trans.public_is_valid_transaction?.should be_false
120
+
121
+ trans.portfolio = 1
122
+ trans.ticker = " "
123
+ trans.type = 'Buy'
124
+ trans.public_is_valid_transaction?.should be_false
125
+
126
+ trans.portfolio = " "
127
+ trans.ticker = "NYSE:GLD"
128
+ trans.type = 'Buy'
129
+ trans.public_is_valid_transaction?.should be_false
130
+
131
+ trans.portfolio = nil
132
+ trans.ticker = nil
133
+ trans.type = nil
134
+ trans.public_is_valid_transaction?.should be_false
135
+
136
+ trans.portfolio = nil
137
+ trans.ticker = "NYSE:GLD"
138
+ trans.type = 'Buy'
139
+ trans.public_is_valid_transaction?.should be_false
140
+
141
+ trans.portfolio = "1"
142
+ trans.ticker = nil
143
+ trans.type = 'Buy'
144
+ trans.public_is_valid_transaction?.should be_false
145
+ end
146
+
147
+ it "should save a transaction" do
148
+ transaction = GMoney::Transaction.new
149
+ transaction.portfolio = 9
150
+ transaction.ticker = 'NASDAQ:GOOG'
151
+ transaction.type = 'Buy'
152
+ transaction.shares = 50
153
+ transaction.price = 450.0
154
+ transaction.commission = 20.0
155
+ transaction.date = '2009-11-17T00:00:00.000'
156
+
157
+ @gf_response.status_code = 200
158
+ @gf_response.body = @new_transaction_feed
159
+
160
+ transaction_save_helper(transaction)
161
+
162
+ transaction_return = transaction.save
163
+
164
+ transaction_return.id.should be_eql('http://finance.google.com/finance/feeds/user@example.com/portfolios/9/positions/NASDAQ:GOOG/transactions/12')
165
+ transaction_return.commission.should be_eql(20.0)
166
+ transaction_return.price.should be_eql(450.0)
167
+ transaction_return.type.should be_eql('Buy')
168
+ end
169
+
170
+ it "should update a transaction when an @id is already set" do
171
+ transaction = GMoney::Transaction.new
172
+ transaction.portfolio = 9
173
+ transaction.ticker = 'NASDAQ:GOOG'
174
+ transaction.type = 'Buy'
175
+ transaction.shares = 50
176
+ transaction.price = 450.0
177
+ transaction.commission = 20.0
178
+ transaction.date = '2009-11-17T00:00:00.000'
179
+ transaction.instance_variable_set("@id", "http://finance.google.com/finance/feeds/user@example.com/portfolios/9/positions/NASDAQ:GOOG/transactions/12")
180
+
181
+ @gf_response.status_code = 201
182
+ @gf_response.body = @new_transaction_feed
183
+
184
+ transaction_save_helper(transaction)
185
+
186
+ transaction.save
187
+ end
188
+
189
+ it "should raise a TransactionSaveError if the transaction type, ticker, or portfolio are not set" do
190
+ transaction = GMoney::Transaction.new
191
+
192
+ lambda { transaction.save }.should raise_error(GMoney::Transaction::TransactionSaveError, "You must include a portfolio id, ticker symbol, and transaction type ['Buy', 'Sell', 'Buy to Cover', 'Sell Short'] in order to create a transaction.")
193
+
194
+ transaction.portfolio = 9
195
+ transaction.type = 'Buy'
196
+ lambda { transaction.save }.should raise_error(GMoney::Transaction::TransactionSaveError, "You must include a portfolio id, ticker symbol, and transaction type ['Buy', 'Sell', 'Buy to Cover', 'Sell Short'] in order to create a transaction.")
197
+
198
+ transaction.portfolio = nil
199
+ transaction.ticker = 'NASDAQ:GOOG'
200
+ transaction.type = 'Buy'
201
+ lambda { transaction.save }.should raise_error(GMoney::Transaction::TransactionSaveError, "You must include a portfolio id, ticker symbol, and transaction type ['Buy', 'Sell', 'Buy to Cover', 'Sell Short'] in order to create a transaction.")
202
+ end
203
+
204
+ it "should give you a warning from Google if your transaction attributes are bad" do
205
+ transaction = GMoney::Transaction.new
206
+ transaction.portfolio = 9
207
+ transaction.ticker = 'asdfasd:asdfs' #invalid ticker
208
+ transaction.type = 'Buy'
209
+ transaction.shares = 50
210
+ transaction.price = 450.0
211
+ transaction.commission = 20.0
212
+ transaction.date = '2009-11-17T00:00:00.000'
213
+
214
+ @gf_response.status_code = 400
215
+ @gf_response.body = 'Some of the values submitted are not valid and have been ignored.'
216
+
217
+ transaction_save_helper(transaction)
218
+
219
+ lambda { transaction.save }.should raise_error(GMoney::Transaction::TransactionSaveError, "Some of the values submitted are not valid and have been ignored.")
220
+
221
+ end
222
+
88
223
  def transaction_helper(id, options={})
89
224
  GMoney::GFSession.should_receive(:auth_token).and_return('toke')
90
225
 
@@ -104,4 +239,29 @@ describe GMoney::Transaction do
104
239
 
105
240
  GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
106
241
  end
242
+
243
+ def transaction_save_helper(transaction)
244
+ currency_code = transaction.currency_code ? transaction.currency_code : 'USD'
245
+
246
+ atom_string = "<?xml version='1.0'?>
247
+ <entry xmlns='http://www.w3.org/2005/Atom'
248
+ xmlns:gf='http://schemas.google.com/finance/2007'
249
+ xmlns:gd='http://schemas.google.com/g/2005'>
250
+ <gf:transactionData date='#{transaction.date}' shares='#{transaction.shares}' type='#{transaction.type}'>"
251
+
252
+ atom_string += "<gf:commission><gd:money amount='#{transaction.commission}' currencyCode='#{currency_code}'/></gf:commission>" if transaction.commission
253
+ atom_string += "<gf:price><gd:money amount='#{transaction.price}' currencyCode='#{currency_code}'/></gf:price>" if transaction.price
254
+ atom_string += "</gf:transactionData></entry>"
255
+
256
+ url = transaction.id ? transaction.id : "#{GMoney::GF_PORTFOLIO_FEED_URL}/#{transaction.portfolio}/positions/#{transaction.ticker}/transactions"
257
+
258
+ GMoney::GFSession.should_receive(:auth_token).and_return('toke')
259
+
260
+ headers = {"Authorization" => "GoogleLogin auth=toke", "Content-Type" => "application/atom+xml"}
261
+ headers["X-HTTP-Method-Override"] = "PUT" if transaction.id
262
+
263
+ GMoney::GFRequest.should_receive(:new).with(url, :method => :post, :body => atom_string, :headers => headers).and_return(@gf_request)
264
+
265
+ GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
266
+ end
107
267
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gmoney
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Spradlin
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-14 00:00:00 -05:00
12
+ date: 2009-11-17 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,6 +22,7 @@ extensions: []
22
22
  extra_rdoc_files:
23
23
  - README.rdoc
24
24
  - lib/extensions/fixnum.rb
25
+ - lib/extensions/nil_class.rb
25
26
  - lib/extensions/string.rb
26
27
  - lib/gmoney.rb
27
28
  - lib/gmoney/authentication_request.rb
@@ -42,6 +43,7 @@ files:
42
43
  - Rakefile
43
44
  - gmoney.gemspec
44
45
  - lib/extensions/fixnum.rb
46
+ - lib/extensions/nil_class.rb
45
47
  - lib/extensions/string.rb
46
48
  - lib/gmoney.rb
47
49
  - lib/gmoney/authentication_request.rb
@@ -60,6 +62,8 @@ files:
60
62
  - spec/fixtures/cacert.pem
61
63
  - spec/fixtures/default_portfolios_feed.xml
62
64
  - spec/fixtures/empty_portfolio_feed.xml
65
+ - spec/fixtures/new_portfolio_feed.xml
66
+ - spec/fixtures/new_transaction_feed.xml
63
67
  - spec/fixtures/portfolio_9_feed.xml
64
68
  - spec/fixtures/portfolio_feed_with_returns.xml
65
69
  - spec/fixtures/position_feed_for_9_GOOG.xml
@@ -68,7 +72,9 @@ files:
68
72
  - spec/fixtures/positions_feed_for_portfolio_9r.xml
69
73
  - spec/fixtures/transaction_feed_for_GOOG_1.xml
70
74
  - spec/fixtures/transactions_feed_for_GOOG.xml
75
+ - spec/fixtures/updated_portfolio_feed.xml
71
76
  - spec/gmoney_spec.rb
77
+ - spec/nil_class_spec.rb
72
78
  - spec/portfolio_feed_parser_spec.rb
73
79
  - spec/portfolio_spec.rb
74
80
  - spec/position_feed_parser_spec.rb
@@ -93,7 +99,7 @@ rdoc_options:
93
99
  - --title
94
100
  - Gmoney
95
101
  - --main
96
- - README.rdoc~
102
+ - README.rdoc
97
103
  require_paths:
98
104
  - lib
99
105
  required_ruby_version: !ruby/object:Gem::Requirement