ib-ruby 0.7.6 → 0.7.8

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.
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