ib-ruby 0.7.6 → 0.7.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/HISTORY +8 -0
  2. data/Rakefile +8 -0
  3. data/VERSION +1 -1
  4. data/bin/fundamental_data +6 -9
  5. data/lib/ib-ruby/connection.rb +16 -19
  6. data/lib/ib-ruby/constants.rb +3 -1
  7. data/lib/ib-ruby/extensions.rb +5 -0
  8. data/lib/ib-ruby/messages/incoming/contract_data.rb +46 -45
  9. data/lib/ib-ruby/messages/incoming/delta_neutral_validation.rb +8 -5
  10. data/lib/ib-ruby/messages/incoming/execution_data.rb +2 -2
  11. data/lib/ib-ruby/messages/incoming/next_valid_id.rb +18 -0
  12. data/lib/ib-ruby/messages/incoming/open_order.rb +23 -16
  13. data/lib/ib-ruby/messages/incoming/order_status.rb +5 -3
  14. data/lib/ib-ruby/messages/incoming/scanner_data.rb +15 -11
  15. data/lib/ib-ruby/messages/incoming.rb +1 -5
  16. data/lib/ib-ruby/messages/outgoing/abstract_message.rb +2 -1
  17. data/lib/ib-ruby/messages/outgoing/place_order.rb +1 -1
  18. data/lib/ib-ruby/messages/outgoing.rb +1 -1
  19. data/lib/ib-ruby/models/bag.rb +59 -0
  20. data/lib/ib-ruby/models/combo_leg.rb +10 -6
  21. data/lib/ib-ruby/models/contract.rb +278 -0
  22. data/lib/ib-ruby/models/contract_detail.rb +70 -0
  23. data/lib/ib-ruby/models/execution.rb +22 -16
  24. data/lib/ib-ruby/models/model.rb +75 -17
  25. data/lib/ib-ruby/models/model_properties.rb +40 -26
  26. data/lib/ib-ruby/models/option.rb +62 -0
  27. data/lib/ib-ruby/models/order.rb +122 -86
  28. data/lib/ib-ruby/models/order_state.rb +11 -12
  29. data/lib/ib-ruby/models/underlying.rb +36 -0
  30. data/lib/ib-ruby/models.rb +1 -4
  31. data/spec/account_helper.rb +2 -1
  32. data/spec/db.rb +1 -1
  33. data/spec/db_helper.rb +105 -0
  34. data/spec/ib-ruby/connection_spec.rb +3 -3
  35. data/spec/ib-ruby/messages/incoming/open_order_spec.rb +5 -5
  36. data/spec/ib-ruby/messages/incoming/order_status_spec.rb +3 -3
  37. data/spec/ib-ruby/models/bag_spec.rb +15 -23
  38. data/spec/ib-ruby/models/bar_spec.rb +0 -5
  39. data/spec/ib-ruby/models/combo_leg_spec.rb +18 -25
  40. data/spec/ib-ruby/models/contract_detail_spec.rb +54 -0
  41. data/spec/ib-ruby/models/contract_spec.rb +25 -37
  42. data/spec/ib-ruby/models/execution_spec.rb +64 -19
  43. data/spec/ib-ruby/models/option_spec.rb +12 -34
  44. data/spec/ib-ruby/models/order_spec.rb +107 -45
  45. data/spec/ib-ruby/models/order_state_spec.rb +12 -12
  46. data/spec/ib-ruby/models/underlying_spec.rb +36 -0
  47. data/spec/integration/contract_info_spec.rb +65 -55
  48. data/spec/integration/fundamental_data_spec.rb +2 -2
  49. data/spec/integration/orders/attached_spec.rb +3 -3
  50. data/spec/integration/orders/combo_spec.rb +3 -3
  51. data/spec/integration/orders/placement_spec.rb +8 -8
  52. data/spec/integration/orders/{execution_spec.rb → trades_spec.rb} +8 -12
  53. data/spec/integration/orders/valid_ids_spec.rb +3 -3
  54. data/spec/message_helper.rb +1 -1
  55. data/spec/model_helper.rb +150 -85
  56. data/spec/order_helper.rb +35 -18
  57. metadata +18 -10
  58. data/lib/ib-ruby/models/contracts/bag.rb +0 -62
  59. data/lib/ib-ruby/models/contracts/contract.rb +0 -320
  60. data/lib/ib-ruby/models/contracts/option.rb +0 -66
  61. data/lib/ib-ruby/models/contracts.rb +0 -27
@@ -0,0 +1,278 @@
1
+ require 'ib-ruby/models/contract_detail'
2
+ require 'ib-ruby/models/underlying'
3
+
4
+ module IB
5
+ module Models
6
+ class Contract < Model.for(:contract)
7
+ include ModelProperties
8
+
9
+ # Fields are Strings unless noted otherwise
10
+ prop :con_id, # int: The unique contract identifier.
11
+ :currency, # Only needed if there is an ambiguity, e.g. when SMART exchange
12
+ # and IBM is being requested (IBM can trade in GBP or USD).
13
+
14
+ :legs_description, # received in OpenOrder for all combos
15
+
16
+ :sec_type, # Security type. Valid values are: SECURITY_TYPES
17
+
18
+ :sec_id, # Unique identifier of the given secIdType.
19
+
20
+ :sec_id_type => :sup, # Security identifier, when querying contract details or
21
+ # when placing orders. Supported identifiers are:
22
+ # - ISIN (Example: Apple: US0378331005)
23
+ # - CUSIP (Example: Apple: 037833100)
24
+ # - SEDOL (6-AN + check digit. Example: BAE: 0263494)
25
+ # - RIC (exchange-independent RIC Root and exchange-
26
+ # identifying suffix. Ex: AAPL.O for Apple on NASDAQ.)
27
+
28
+ :symbol => :s, # This is the symbol of the underlying asset.
29
+
30
+ :local_symbol => :s, # Local exchange symbol of the underlying asset
31
+
32
+ # Future/option contract multiplier (only needed when multiple possibilities exist)
33
+ :multiplier => {:set => :i},
34
+
35
+ :strike => :f, # double: The strike price.
36
+ :expiry => :s, # The expiration date. Use the format YYYYMM or YYYYMMDD
37
+ :exchange => :sup, # The order destination, such as Smart.
38
+ :primary_exchange => :sup, # Non-SMART exchange where the contract trades.
39
+ :include_expired => :bool, # When true, contract details requests and historical
40
+ # data queries can be performed pertaining to expired contracts.
41
+ # Note: Historical data queries on expired contracts are
42
+ # limited to the last year of the contracts life, and are
43
+ # only supported for expired futures contracts.
44
+ # This field can NOT be set to true for orders.
45
+
46
+
47
+ # Specifies a Put or Call. Valid input values are: P, PUT, C, CALL
48
+ :right =>
49
+ {:set => proc { |val|
50
+ self[:right] =
51
+ case val.to_s.upcase
52
+ when 'NONE', '', '0', '?'
53
+ ''
54
+ when 'PUT', 'P'
55
+ 'P'
56
+ when 'CALL', 'C'
57
+ 'C'
58
+ else
59
+ val
60
+ end },
61
+ :validate => {:format => {:with => /^put$|^call$|^none$/,
62
+ :message => "should be put, call or none"}}
63
+ }
64
+
65
+ attr_accessor :description # NB: local to ib-ruby, not part of TWS.
66
+
67
+ ### Associations
68
+
69
+ has_one :contract_detail
70
+
71
+ has_one :underlying # for Delta-Neutral Combo contracts only!
72
+ alias under_comp underlying
73
+ alias under_comp= underlying=
74
+
75
+ has_many :combo_legs
76
+ alias legs combo_legs
77
+ alias legs= combo_legs=
78
+ alias combo_legs_description legs_description
79
+ alias combo_legs_description= legs_description=
80
+
81
+
82
+ ### Extra validations
83
+ validates_inclusion_of :sec_type, :in => CODES[:sec_type].keys,
84
+ :message => "should be valid security type"
85
+
86
+ validates_format_of :expiry, :with => /^\d{6}$|^\d{8}$|^$/,
87
+ :message => "should be YYYYMM or YYYYMMDD"
88
+
89
+ validates_format_of :primary_exchange, :without => /SMART/,
90
+ :message => "should not be SMART"
91
+
92
+ validates_format_of :sec_id_type, :with => /ISIN|SEDOL|CUSIP|RIC|^$/,
93
+ :message => "should be valid security identifier"
94
+
95
+ validates_numericality_of :multiplier, :strike, :allow_nil => true
96
+
97
+ def default_attributes
98
+ {:con_id => 0,
99
+ :strike => 0.0,
100
+ :right => :none, # Not an option
101
+ :exchange => 'SMART',
102
+ :include_expired => false, }.merge super
103
+ end
104
+
105
+ # This returns an Array of data from the given contract.
106
+ # Different messages serialize contracts differently. Go figure.
107
+ # Note that it does NOT include the combo legs.
108
+ # serialize [:option, :con_id, :include_expired, :sec_id]
109
+ def serialize *fields
110
+ [(fields.include?(:con_id) ? [con_id] : []),
111
+ symbol,
112
+ self[:sec_type],
113
+ (fields.include?(:option) ?
114
+ [expiry,
115
+ strike,
116
+ self[:right],
117
+ multiplier] : []),
118
+ exchange,
119
+ (fields.include?(:primary_exchange) ? [primary_exchange] : []),
120
+ currency,
121
+ local_symbol,
122
+ (fields.include?(:sec_id) ? [sec_id_type, sec_id] : []),
123
+ (fields.include?(:include_expired) ? [include_expired] : []),
124
+ ].flatten
125
+ end
126
+
127
+ def serialize_long *fields
128
+ serialize :option, :primary_exchange, *fields
129
+ end
130
+
131
+ def serialize_short *fields
132
+ serialize :option, *fields
133
+ end
134
+
135
+ # Serialize under_comp parameters: EClientSocket.java, line 471
136
+ def serialize_under_comp *args
137
+ under_comp ? under_comp.serialize : [false]
138
+ end
139
+
140
+ # Defined in Contract, not BAG subclass to keep code DRY
141
+ def serialize_legs *fields
142
+ case
143
+ when !bag?
144
+ []
145
+ when legs.empty?
146
+ [0]
147
+ else
148
+ [legs.size, legs.map { |leg| leg.serialize *fields }].flatten
149
+ end
150
+ end
151
+
152
+ # This produces a string uniquely identifying this contract, in the format used
153
+ # for command line arguments in the IB-Ruby examples. The format is:
154
+ #
155
+ # symbol:sec_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
156
+ #
157
+ # Fields not needed for a particular security should be left blank
158
+ # (e.g. strike and right are only relevant for options.)
159
+ #
160
+ # For example, to query the British pound futures contract trading on Globex
161
+ # expiring in September, 2008, the string is:
162
+ #
163
+ # GBP:FUT:200809:::62500:GLOBEX::USD:
164
+ def serialize_ib_ruby
165
+ serialize_long.join(":")
166
+ end
167
+
168
+ # Contract comparison
169
+ def == other
170
+ return false unless other.is_a?(self.class)
171
+
172
+ # Different sec_id_type
173
+ return false if sec_id_type && other.sec_id_type && sec_id_type != other.sec_id_type
174
+
175
+ # Different sec_id
176
+ return false if sec_id && other.sec_id && sec_id != other.sec_id
177
+
178
+ # Different symbols
179
+ return false if symbol && other.symbol && symbol != other.symbol
180
+
181
+ # Different currency
182
+ return false if currency && other.currency && currency != other.currency
183
+
184
+ # Same con_id for all Bags, but unknown for new Contracts...
185
+ # 0 or nil con_id matches any
186
+ return false if con_id != 0 && other.con_id != 0 &&
187
+ con_id && other.con_id && con_id != other.con_id
188
+
189
+ # SMART or nil exchange matches any
190
+ return false if exchange != 'SMART' && other.exchange != 'SMART' &&
191
+ exchange && other.exchange && exchange != other.exchange
192
+
193
+ # Comparison for Bonds and Options
194
+ if bond? || option?
195
+ return false if right != other.right || strike != other.strike
196
+ return false if multiplier && other.multiplier &&
197
+ multiplier != other.multiplier
198
+ return false if expiry && expiry[0..5] != other.expiry[0..5]
199
+ return false unless expiry && (expiry[6..7] == other.expiry[6..7] ||
200
+ expiry[6..7].empty? || other.expiry[6..7].empty?)
201
+ end
202
+
203
+ # All else being equal...
204
+ sec_type == other.sec_type
205
+ end
206
+
207
+ def to_s
208
+ "<Contract: " + instance_variables.map do |key|
209
+ value = send(key[1..-1])
210
+ " #{key}=#{value}" unless value.nil? || value == '' || value == 0
211
+ end.compact.join(',') + " >"
212
+ end
213
+
214
+ def to_human
215
+ "<Contract: " +
216
+ [symbol,
217
+ sec_type,
218
+ (expiry == '' ? nil : expiry),
219
+ (right == :none ? nil : right),
220
+ (strike == 0 ? nil : strike),
221
+ exchange,
222
+ currency
223
+ ].compact.join(" ") + ">"
224
+ end
225
+
226
+ def to_short
227
+ "#{symbol}#{expiry}#{strike}#{right}#{exchange}#{currency}"
228
+ end
229
+
230
+ # Testing for type of contract:
231
+
232
+ def bag?
233
+ self[:sec_type] == 'BAG'
234
+ end
235
+
236
+ def bond?
237
+ self[:sec_type] == 'BOND'
238
+ end
239
+
240
+ def stock?
241
+ self[:sec_type] == 'STK'
242
+ end
243
+
244
+ def option?
245
+ self[:sec_type] == 'OPT'
246
+ end
247
+
248
+ end # class Contract
249
+
250
+
251
+ ### Now let's deal with Contract subclasses
252
+
253
+ require 'ib-ruby/models/option'
254
+ require 'ib-ruby/models/bag'
255
+
256
+ class Contract
257
+ # Specialized Contract subclasses representing different security types
258
+ Subclasses = Hash.new(Contract)
259
+ Subclasses[:bag] = IB::Models::Bag
260
+ Subclasses[:option] = IB::Models::Option
261
+
262
+ # This returns a Contract initialized from the serialize_ib_ruby format string.
263
+ def self.build opts = {}
264
+ subclass = VALUES[:sec_type][opts[:sec_type]] || opts[:sec_type].to_sym
265
+ Contract::Subclasses[subclass].new opts
266
+ end
267
+
268
+ # This returns a Contract initialized from the serialize_ib_ruby format string.
269
+ def self.from_ib_ruby string
270
+ keys = [:symbol, :sec_type, :expiry, :strike, :right, :multiplier,
271
+ :exchange, :primary_exchange, :currency, :local_symbol]
272
+ props = Hash[keys.zip(string.split(":"))]
273
+ props.delete_if { |k, v| v.nil? || v.empty? }
274
+ Contract.build props
275
+ end
276
+ end # class Contract
277
+ end # module Models
278
+ end # module IB
@@ -0,0 +1,70 @@
1
+ module IB
2
+ module Models
3
+
4
+ # Additional Contract properties (volatile, therefore extracted)
5
+ class ContractDetail < Model.for(:contract_detail)
6
+ include ModelProperties
7
+
8
+ belongs_to :contract
9
+ alias summary contract
10
+ alias summary= contract=
11
+
12
+ # All fields Strings, unless specified otherwise:
13
+ prop :market_name, # The market name for this contract.
14
+ :trading_class, # The trading class name for this contract.
15
+ :min_tick, # double: The minimum price tick.
16
+ :price_magnifier, # int: Allows execution and strike prices to be
17
+ # reported consistently with market data, historical data and the
18
+ # order price: Z on LIFFE is reported in index points, not GBP.
19
+
20
+ :order_types, # The list of valid order types for this contract.
21
+ :valid_exchanges, # The list of exchanges this contract is traded on.
22
+ :under_con_id, # int: The underlying contract ID.
23
+ :long_name, # Descriptive name of the asset.
24
+ :contract_month, # The contract month of the underlying futures contract.
25
+
26
+ # The industry classification of the underlying/product:
27
+ :industry, # Wide industry. For example, Financial.
28
+ :category, # Industry category. For example, InvestmentSvc.
29
+ :subcategory, # Subcategory. For example, Brokerage.
30
+ [:time_zone, :time_zone_id], # Time zone for the trading hours (e.g. EST)
31
+ :trading_hours, # The trading hours of the product. For example:
32
+ # 20090507:0700-1830,1830-2330;20090508:CLOSED.
33
+ :liquid_hours, # The liquid trading hours of the product. For example,
34
+ # 20090507:0930-1600;20090508:CLOSED.
35
+
36
+ # BOND values:
37
+ :cusip, # The nine-character bond CUSIP or the 12-character SEDOL.
38
+ :ratings, # Credit rating of the issuer. Higher rating is less risky investment.
39
+ # Bond ratings are from Moody's and S&P respectively.
40
+ :desc_append, # Additional descriptive information about the bond.
41
+ :bond_type, # The type of bond, such as "CORP."
42
+ :coupon_type, # The type of bond coupon.
43
+ :coupon, # double: The interest rate used to calculate the amount you
44
+ # will receive in interest payments over the year. default 0
45
+ :maturity, # The date on which the issuer must repay bond face value
46
+ :issue_date, # The date the bond was issued.
47
+ :next_option_date, # only if bond has embedded options.
48
+ :next_option_type, # only if bond has embedded options.
49
+ :notes, # Additional notes, if populated for the bond in IB's database
50
+ :callable => :bool, # Can be called by the issuer under certain conditions.
51
+ :puttable => :bool, # Can be sold back to the issuer under certain conditions
52
+ :convertible => :bool, # Can be converted to stock under certain conditions.
53
+ :next_option_partial => :bool # # only if bond has embedded options.
54
+
55
+ # Extra validations
56
+ validates_format_of :time_zone, :with => /^\w{3}$/, :message => 'should be XXX'
57
+
58
+ def default_attributes
59
+ {:coupon => 0.0,
60
+ :under_con_id => 0,
61
+ :min_tick => 0,
62
+ :callable => false,
63
+ :puttable => false,
64
+ :convertible => false,
65
+ :next_option_partial => false, }.merge super
66
+ end
67
+
68
+ end # class ContractDetail
69
+ end # module Models
70
+ end # module IB
@@ -5,7 +5,9 @@ module IB
5
5
  class Execution < Model.for(:execution)
6
6
  include ModelProperties
7
7
 
8
- prop :order_id, # int: order id. TWS orders have a fixed order id of 0.
8
+ belongs_to :order
9
+
10
+ prop [:local_id, :order_id], # int: order id. TWS orders have a fixed order id of 0.
9
11
  :client_id, # int: client id. TWS orders have a fixed client id of 0.
10
12
  :perm_id, # int: TWS id used to identify orders over TWS sessions
11
13
  :exec_id, # String: Unique order execution id over TWS sessions.
@@ -13,29 +15,33 @@ module IB
13
15
  # String: The order execution time.
14
16
  :exchange, # String: Exchange that executed the order.
15
17
  :order_ref, # int: Same order_ref as in corresponding Order
16
- [:account_name, :account_number], # String: The customer account number.
17
18
  :price, # double: The order execution price.
18
19
  :average_price, # double: Average price. Used in regular trades, combo
19
20
  # trades and legs of the combo.
20
- :shares, # int: The number of shares filled.
21
+ [:quantity, :shares], # int: The number of shares filled.
21
22
  :cumulative_quantity, # int: Cumulative quantity. Used in regular
22
23
  # trades, combo trades and legs of the combo
23
- :liquidation => :bool, # int: This position is liquidated last should the need arise.
24
- [:side, :action] => PROPS[:side] # String: Was the transaction a buy or a sale: BOT|SLD
24
+ :liquidation => :bool, # This position is liquidated last should the need arise.
25
+ [:account_name, :account_number] => :s, # The customer account number.
26
+ [:side, :action] => PROPS[:side] # Was the transaction a buy or a sale: BOT|SLD
25
27
 
26
28
  # Extra validations
27
- validates_numericality_of :shares, :cumulative_quantity, :price, :average_price
28
-
29
- DEFAULT_PROPS = {:order_id => 0,
30
- :client_id => 0,
31
- :shares => 0,
32
- :price => 0,
33
- :perm_id => 0,
34
- :liquidation => 0, }
29
+ validates_numericality_of :quantity, :cumulative_quantity, :price, :average_price
30
+ validates_numericality_of :local_id, :client_id, :perm_id, :only_integer => true
31
+
32
+ def default_attributes
33
+ {:local_id => 0,
34
+ :client_id => 0,
35
+ :quantity => 0,
36
+ :price => 0,
37
+ :perm_id => 0,
38
+ :liquidation => false, }.merge super
39
+ end
40
+
35
41
  # Comparison
36
42
  def == other
37
43
  perm_id == other.perm_id &&
38
- order_id == other.order_id && # ((p __LINE__)||true) &&
44
+ local_id == other.local_id && # ((p __LINE__)||true) &&
39
45
  client_id == other.client_id &&
40
46
  exec_id == other.exec_id &&
41
47
  time == other.time &&
@@ -46,9 +52,9 @@ module IB
46
52
  end
47
53
 
48
54
  def to_human
49
- "<Execution: #{time} #{side} #{shares} at #{price} on #{exchange}, " +
55
+ "<Execution: #{time} #{side} #{quantity} at #{price} on #{exchange}, " +
50
56
  "cumulative #{cumulative_quantity} at #{average_price}, " +
51
- "ids #{order_id}/#{perm_id}/#{exec_id}>"
57
+ "ids #{local_id}/#{perm_id}/#{exec_id}>"
52
58
  end
53
59
 
54
60
  alias to_s to_human
@@ -1,43 +1,101 @@
1
1
  module IB
2
2
  module Models
3
3
 
4
- # Base IB data Model class, in future it will be developed into ActiveModel
4
+ # Base class for tableless IB data Models extends ActiveModel API
5
5
  class Model
6
+ extend ActiveModel::Naming
7
+ extend ActiveModel::Callbacks
8
+ include ActiveModel::Validations
9
+ include ActiveModel::Serialization
10
+ include ActiveModel::Serializers::Xml
11
+ include ActiveModel::Serializers::JSON
6
12
 
7
13
  # IB Models can be either database-backed, or not
8
- # require 'ib-ruby/db' # to make IB models database-backed
14
+ # require 'ib-ruby/db' # to make all IB models database-backed
15
+ # If you plan to persist only specific Models, select those subclasses here:
9
16
  def self.for subclass
10
- if DB
11
- case subclass
12
- when :execution, :bar, :order_state
13
- # Just a couple of AR models introduced for now...
14
- ActiveRecord::Base
15
- else
16
- Model
17
- end
17
+ if DB # && [:contract, :order, :order_state].include? subclass
18
+ ActiveRecord::Base
18
19
  else
19
20
  Model
20
21
  end
21
22
  end
22
23
 
24
+ attr_accessor :created_at, :updated_at, :attributes
25
+
23
26
  # If a opts hash is given, keys are taken as attribute names, values as data.
24
27
  # The model instance fields are then set automatically from the opts Hash.
25
- def initialize(opts={})
26
- error "Argument must be a Hash", :args unless opts.is_a?(Hash)
28
+ def initialize opts={}
29
+ run_callbacks :initialize do
30
+ error "Argument must be a Hash", :args unless opts.is_a?(Hash)
31
+
32
+ attrs = default_attributes.merge(opts)
33
+ attrs.keys.each { |key| self.send("#{key}=", attrs[key]) }
34
+ end
35
+ end
27
36
 
28
- props = self.class::DEFAULT_PROPS.merge(opts)
29
- props.keys.each { |key| self.send("#{key}=", props[key]) }
37
+ # ActiveModel API (for serialization)
38
+
39
+ def attributes
40
+ @attributes ||= HashWithIndifferentAccess.new
30
41
  end
31
42
 
32
- # ActiveModel-style attribute accessors
43
+ # ActiveModel-style read/write_attribute accessors
33
44
  def [] key
34
- instance_variable_get "@#{key}".to_sym
45
+ attributes[key.to_sym]
35
46
  end
36
47
 
37
48
  def []= key, val
38
- instance_variable_set "@#{key}".to_sym, val
49
+ attributes[key.to_sym] = val
50
+ end
51
+
52
+ def to_model
53
+ self
54
+ end
55
+
56
+ def new_record?
57
+ true
58
+ end
59
+
60
+ def save
61
+ valid?
62
+ end
63
+
64
+ alias save! save
65
+
66
+ ### ActiveRecord::Base association API mocks
67
+
68
+ def self.belongs_to model, *args
69
+ attr_accessor model
39
70
  end
40
71
 
72
+ def self.has_one model, *args
73
+ attr_accessor model
74
+ end
75
+
76
+ def self.has_many models, *args
77
+ attr_accessor models
78
+
79
+ define_method(models) do
80
+ self.instance_variable_get("@#{models}") ||
81
+ self.instance_variable_set("@#{models}", [])
82
+ end
83
+ end
84
+
85
+ def self.find *args
86
+ []
87
+ end
88
+
89
+ ### ActiveRecord::Base callback API mocks
90
+
91
+ define_model_callbacks :initialize, :only => :after
92
+
93
+ ### ActiveRecord::Base misc
94
+
95
+ def self.serialize *properties
96
+ end
97
+
98
+
41
99
  end # Model
42
100
  end # module Models
43
101
  end # module IB
@@ -1,5 +1,6 @@
1
1
  require 'active_model'
2
2
  require 'active_support/concern'
3
+ require 'active_support/hash_with_indifferent_access'
3
4
 
4
5
  module IB
5
6
  module Models
@@ -8,25 +9,50 @@ module IB
8
9
  module ModelProperties
9
10
  extend ActiveSupport::Concern
10
11
 
11
- DEFAULT_PROPS = {}
12
+ def default_attributes
13
+ {:created_at => Time.now,
14
+ :updated_at => Time.now,
15
+ }
16
+ end
12
17
 
13
18
  ### Instance methods
14
19
 
15
- attr_accessor :created_at
20
+ # Default presentation
21
+ def to_human
22
+ "<#{self.class.to_s.demodulize}: " + attributes.map do |attr, value|
23
+ "#{attr}: #{value}" unless value.nil?
24
+ end.compact.sort.join(' ') + ">"
25
+ end
26
+
27
+ # Comparison support
28
+ def content_attributes
29
+ HashWithIndifferentAccess[attributes.reject do |(attr, _)|
30
+ attr.to_s =~ /(_id|_count)$/ ||
31
+ [:created_at, :updated_at, :type, :id].include?(attr.to_sym)
32
+ end]
33
+ end
16
34
 
17
- def initialize opts={}
18
- @created_at = Time.now
19
- super self.class::DEFAULT_PROPS.merge(opts)
35
+ # Default Model comparison
36
+ def == other
37
+ content_attributes.inject(true) { |res, (attr, value)| res && other.send(attr) == value } &&
38
+ other.content_attributes.inject(true) { |res, (attr, value)| res && send(attr) == value }
20
39
  end
21
40
 
22
41
  included do
23
42
 
43
+ # Extending AR-backed Model class with attribute defaults
44
+ if defined?(ActiveRecord::Base) && ancestors.include?(ActiveRecord::Base)
45
+ def initialize opts={}
46
+ super default_attributes.merge(opts)
47
+ end
48
+ end
49
+
24
50
  ### Class macros
25
51
 
26
52
  def self.prop *properties
27
53
  prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
28
54
 
29
- properties.each { |names| define_property names, '' }
55
+ properties.each { |names| define_property names, nil }
30
56
  prop_hash.each { |names, type| define_property names, type }
31
57
  end
32
58
 
@@ -57,7 +83,6 @@ module IB
57
83
  :validate => body[2]
58
84
 
59
85
  when Hash # recursion base case
60
-
61
86
  getter = case # Define getter
62
87
  when body[:get].respond_to?(:call)
63
88
  body[:get]
@@ -65,10 +90,14 @@ module IB
65
90
  proc { self[name].send "to_#{body[:get]}" }
66
91
  when VALUES[name] # property is encoded
67
92
  proc { VALUES[name][self[name]] }
93
+ #when respond_to?(:column_names) && column_names.include?(name.to_s)
94
+ # # noop, ActiveRecord will take care of it...
95
+ # p "#{name} => get noop"
96
+ # p respond_to?(:column_names) && column_names
68
97
  else
69
98
  proc { self[name] }
70
99
  end
71
- define_method name, &getter
100
+ define_method name, &getter if getter
72
101
 
73
102
  setter = case # Define setter
74
103
  when body[:set].respond_to?(:call)
@@ -78,9 +107,9 @@ module IB
78
107
  when CODES[name] # property is encoded
79
108
  proc { |value| self[name] = CODES[name][value] || value }
80
109
  else
81
- proc { |value| self[name] = value }
110
+ proc { |value| self[name] = value } # p name, value;
82
111
  end
83
- define_method "#{name}=", &setter
112
+ define_method "#{name}=", &setter if setter
84
113
 
85
114
  # Define validator(s)
86
115
  [body[:validate]].flatten.compact.each do |validator|
@@ -95,22 +124,7 @@ module IB
95
124
  # TODO define self[:name] accessors for :virtual and :flag properties
96
125
 
97
126
  else # setter given
98
- define_property_methods name, :set => body
99
- end
100
- end
101
-
102
- # Extending lighweight (not DB-backed) Model class to mimic AR::Base
103
- unless ancestors.include? ActiveModel::Validations
104
- include ActiveModel::Validations
105
-
106
- def save
107
- false
108
- end
109
-
110
- alias save! save
111
-
112
- def self.find *args
113
- []
127
+ define_property_methods name, :set => body, :get => body
114
128
  end
115
129
  end
116
130