gmoney 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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