ib-ruby 0.7.4 → 0.7.6

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 (93) hide show
  1. data/.gitignore +3 -0
  2. data/HISTORY +8 -0
  3. data/README.md +2 -2
  4. data/Rakefile +15 -0
  5. data/TODO +7 -2
  6. data/VERSION +1 -1
  7. data/bin/account_info +1 -1
  8. data/bin/cancel_orders +1 -1
  9. data/bin/contract_details +1 -1
  10. data/bin/depth_of_market +1 -1
  11. data/bin/fa_accounts +1 -1
  12. data/bin/fundamental_data +42 -0
  13. data/bin/historic_data +1 -1
  14. data/bin/historic_data_cli +1 -1
  15. data/bin/list_orders +1 -2
  16. data/bin/market_data +1 -1
  17. data/bin/option_data +1 -1
  18. data/bin/place_combo_order +1 -1
  19. data/bin/place_order +1 -1
  20. data/bin/template +1 -4
  21. data/bin/tick_data +2 -2
  22. data/bin/time_and_sales +1 -1
  23. data/lib/ib-ruby.rb +4 -0
  24. data/lib/ib-ruby/connection.rb +50 -34
  25. data/lib/ib-ruby/constants.rb +232 -37
  26. data/lib/ib-ruby/db.rb +25 -0
  27. data/lib/ib-ruby/extensions.rb +51 -1
  28. data/lib/ib-ruby/messages/abstract_message.rb +0 -8
  29. data/lib/ib-ruby/messages/incoming.rb +18 -493
  30. data/lib/ib-ruby/messages/incoming/abstract_message.rb +100 -0
  31. data/lib/ib-ruby/messages/incoming/alert.rb +34 -0
  32. data/lib/ib-ruby/messages/incoming/contract_data.rb +82 -0
  33. data/lib/ib-ruby/messages/incoming/delta_neutral_validation.rb +20 -0
  34. data/lib/ib-ruby/messages/incoming/execution_data.rb +59 -0
  35. data/lib/ib-ruby/messages/incoming/historical_data.rb +55 -0
  36. data/lib/ib-ruby/messages/incoming/market_depths.rb +44 -0
  37. data/lib/ib-ruby/messages/incoming/open_order.rb +32 -16
  38. data/lib/ib-ruby/messages/incoming/order_status.rb +67 -0
  39. data/lib/ib-ruby/messages/incoming/portfolio_value.rb +39 -0
  40. data/lib/ib-ruby/messages/incoming/real_time_bar.rb +32 -0
  41. data/lib/ib-ruby/messages/incoming/scanner_data.rb +49 -0
  42. data/lib/ib-ruby/messages/outgoing.rb +25 -223
  43. data/lib/ib-ruby/messages/outgoing/abstract_message.rb +61 -0
  44. data/lib/ib-ruby/messages/outgoing/bar_requests.rb +149 -0
  45. data/lib/ib-ruby/messages/outgoing/place_order.rb +24 -0
  46. data/lib/ib-ruby/models.rb +4 -0
  47. data/lib/ib-ruby/models/bar.rb +31 -14
  48. data/lib/ib-ruby/models/combo_leg.rb +48 -23
  49. data/lib/ib-ruby/models/contracts.rb +2 -2
  50. data/lib/ib-ruby/models/contracts/bag.rb +11 -7
  51. data/lib/ib-ruby/models/contracts/contract.rb +90 -66
  52. data/lib/ib-ruby/models/contracts/option.rb +16 -7
  53. data/lib/ib-ruby/models/execution.rb +34 -18
  54. data/lib/ib-ruby/models/model.rb +15 -7
  55. data/lib/ib-ruby/models/model_properties.rb +101 -44
  56. data/lib/ib-ruby/models/order.rb +176 -187
  57. data/lib/ib-ruby/models/order_state.rb +99 -0
  58. data/lib/ib-ruby/symbols/forex.rb +10 -10
  59. data/lib/ib-ruby/symbols/futures.rb +6 -6
  60. data/lib/ib-ruby/symbols/stocks.rb +3 -3
  61. data/spec/account_helper.rb +4 -5
  62. data/spec/combo_helper.rb +4 -4
  63. data/spec/db.rb +18 -0
  64. data/spec/ib-ruby/messages/{incoming_spec.rb → incoming/alert_spec.rb} +1 -0
  65. data/spec/ib-ruby/messages/incoming/open_order_spec.rb +100 -0
  66. data/spec/ib-ruby/messages/incoming/order_status_spec.rb +74 -0
  67. data/spec/ib-ruby/messages/{outgoing_spec.rb → outgoing/account_data_spec.rb} +0 -0
  68. data/spec/ib-ruby/messages/outgoing/market_data_type_spec.rb +44 -0
  69. data/spec/ib-ruby/models/bag_spec.rb +97 -0
  70. data/spec/ib-ruby/models/bar_spec.rb +45 -0
  71. data/spec/ib-ruby/models/combo_leg_spec.rb +56 -40
  72. data/spec/ib-ruby/models/contract_spec.rb +134 -170
  73. data/spec/ib-ruby/models/execution_spec.rb +35 -50
  74. data/spec/ib-ruby/models/option_spec.rb +127 -0
  75. data/spec/ib-ruby/models/order_spec.rb +89 -68
  76. data/spec/ib-ruby/models/order_state_spec.rb +55 -0
  77. data/spec/integration/contract_info_spec.rb +4 -6
  78. data/spec/integration/fundamental_data_spec.rb +41 -0
  79. data/spec/integration/historic_data_spec.rb +4 -4
  80. data/spec/integration/market_data_spec.rb +1 -3
  81. data/spec/integration/orders/attached_spec.rb +8 -10
  82. data/spec/integration/orders/combo_spec.rb +2 -2
  83. data/spec/integration/orders/execution_spec.rb +0 -1
  84. data/spec/integration/orders/placement_spec.rb +1 -3
  85. data/spec/integration/orders/valid_ids_spec.rb +1 -2
  86. data/spec/message_helper.rb +1 -1
  87. data/spec/model_helper.rb +211 -0
  88. data/spec/order_helper.rb +44 -37
  89. data/spec/spec_helper.rb +36 -23
  90. data/spec/v.rb +7 -0
  91. data/tasks/doc.rake +1 -1
  92. metadata +116 -12
  93. data/spec/integration/orders/open_order +0 -98
@@ -1,13 +1,12 @@
1
- require 'ib-ruby/models/model'
2
-
3
1
  module IB
4
2
  module Models
5
3
  module Contracts
6
- class Contract < Model
4
+ class Contract < Model.for(:contract)
5
+ include ModelProperties
7
6
 
8
7
  # This returns a Contract initialized from the serialize_ib_ruby format string.
9
8
  def self.build opts = {}
10
- Contracts::TYPES[opts[:sec_type]].new opts
9
+ Contracts::TYPES[VALUES[:sec_type][opts[:sec_type]]].new opts
11
10
  end
12
11
 
13
12
  # This returns a Contract initialized from the serialize_ib_ruby format string.
@@ -21,14 +20,11 @@ module IB
21
20
 
22
21
  # Fields are Strings unless noted otherwise
23
22
  prop :con_id, # int: The unique contract identifier.
24
- :symbol, # This is the symbol of the underlying asset.
25
- :sec_type, # Security type. Valid values are: SECURITY_TYPES
23
+ :sec_type,
26
24
  :strike, # double: The strike price.
27
- :exchange, # The order destination, such as Smart.
28
25
  :currency, # Only needed if there is an ambiguity, e.g. when SMART exchange
29
26
  # and IBM is being requested (IBM can trade in GBP or USD).
30
27
 
31
- :local_symbol, # Local exchange symbol of the underlying asset
32
28
  :include_expired, # When true, contract details requests and historical
33
29
  # data queries can be performed pertaining to expired contracts.
34
30
  # Note: Historical data queries on expired contracts are
@@ -45,57 +41,39 @@ module IB
45
41
  # identifying suffix. Ex: AAPL.O for Apple on NASDAQ.)
46
42
  :sec_id, # Unique identifier of the given secIdType.
47
43
 
48
- # COMBOS
49
44
  :legs_description, # received in OpenOrder for all combos
50
45
 
51
- :multiplier => :i,
46
+ :symbol => :s, # This is the symbol of the underlying asset.
47
+
48
+ :local_symbol => :s, # Local exchange symbol of the underlying asset
49
+
52
50
  # Future/option contract multiplier (only needed when multiple possibilities exist)
51
+ :multiplier => :i,
53
52
 
54
- :primary_exchange =>
55
- # non-aggregate (ie not the SMART) exchange that the contract trades on.
56
- proc { |val|
57
- val.upcase! if val.is_a?(String)
58
- error "Don't set primary_exchange to smart", :args if val == 'SMART'
59
- self[:primary_exchange] = val
60
- },
53
+ :expiry => :s, # The expiration date. Use the format YYYYMM or YYYYMMDD
54
+ :exchange => :sup, # The order destination, such as Smart.
55
+ :primary_exchange => :sup, # Non-SMART exchange where the contract trades.
61
56
 
62
- :right => # Specifies a Put or Call. Valid input values are: P, PUT, C, CALL
63
- proc { |val|
57
+ # Specifies a Put or Call. Valid input values are: P, PUT, C, CALL
58
+ :right =>
59
+ {:set => proc { |val|
64
60
  self[:right] =
65
61
  case val.to_s.upcase
66
- when '', '0', '?'
67
- nil
62
+ when 'NONE', '', '0', '?'
63
+ ''
68
64
  when 'PUT', 'P'
69
- 'PUT'
65
+ 'P'
70
66
  when 'CALL', 'C'
71
- 'CALL'
72
- else
73
- error "Right must be one of PUT, CALL, P, C - not '#{val}'", :args
74
- end
75
- },
76
-
77
- :expiry => # The expiration date. Use the format YYYYMM.
78
- proc { |val|
79
- self[:expiry] =
80
- case val.to_s
81
- when /\d{6,8}/
82
- val.to_s
83
- when nil, ''
84
- nil
67
+ 'C'
85
68
  else
86
- error "Invalid expiry '#{val}' (must be in format YYYYMM or YYYYMMDD)", :args
87
- end
88
- },
89
-
90
- :sec_type => # Security type. Valid values are: SECURITY_TYPES
91
- proc { |val|
92
- val = nil if !val.nil? && val.empty?
93
- unless val.nil? || SECURITY_TYPES.values.include?(val)
94
- error "Invalid security type '#{val}' (must be one of #{SECURITY_TYPES.values}", :args
95
- end
96
- self[:sec_type] = val
69
+ val
70
+ end },
71
+ :validate => {:format => {:with => /^put$|^call$|^none$/,
72
+ :message => "should be put, call or none"}}
97
73
  }
98
74
 
75
+ # Security type. Valid values are: SECURITY_TYPES
76
+
99
77
  # ContractDetails fields are bundled into Contract proper, as it should be
100
78
  # All fields Strings, unless specified otherwise:
101
79
  prop :market_name, # The market name for this contract.
@@ -128,17 +106,17 @@ module IB
128
106
  :desc_append, # Additional descriptive information about the bond.
129
107
  :bond_type, # The type of bond, such as "CORP."
130
108
  :coupon_type, # The type of bond coupon.
131
- :callable, # bool: Can be called by the issuer under certain conditions.
132
- :puttable, # bool: Can be sold back to the issuer under certain conditions
133
109
  :coupon, # double: The interest rate used to calculate the amount you
134
110
  # will receive in interest payments over the year. default 0
135
- :convertible, # bool: Can be converted to stock under certain conditions.
136
111
  :maturity, # The date on which the issuer must repay bond face value
137
112
  :issue_date, # The date the bond was issued.
138
113
  :next_option_date, # only if bond has embedded options.
139
114
  :next_option_type, # only if bond has embedded options.
140
- :next_option_partial, # bool: # only if bond has embedded options.
141
- :notes # Additional notes, if populated for the bond in IB's database
115
+ :notes, # Additional notes, if populated for the bond in IB's database
116
+ :callable => :bool, # Can be called by the issuer under certain conditions.
117
+ :puttable => :bool, # Can be sold back to the issuer under certain conditions
118
+ :convertible => :bool, # Can be converted to stock under certain conditions.
119
+ :next_option_partial => :bool # # only if bond has embedded options.
142
120
 
143
121
  # Used for Delta-Neutral Combo contracts only!
144
122
  # UnderComp fields are bundled into Contract proper, as it should be.
@@ -156,18 +134,27 @@ module IB
156
134
 
157
135
  attr_accessor :description # NB: local to ib-ruby, not part of TWS.
158
136
 
137
+ # Extra validations
138
+ validates_inclusion_of :sec_type, :in => CODES[:sec_type].keys,
139
+ :message => "should be valid security type"
140
+
141
+ validates_format_of :expiry, :with => /^\d{6}$|^\d{8}$|^$/,
142
+ :message => "should be YYYYMM or YYYYMMDD"
143
+
144
+ validates_format_of :primary_exchange, :without => /SMART/,
145
+ :message => "should not be SMART"
146
+
159
147
  DEFAULT_PROPS = {:con_id => 0,
160
- :strike => 0,
148
+ :strike => 0.0,
149
+ :right => :none, # Not an option
161
150
  :exchange => 'SMART',
162
- :include_expired => false,
163
-
164
- # These properties are from ContractDetails
165
151
  :under_con_id => 0,
166
152
  :min_tick => 0,
153
+ :coupon => 0,
167
154
  :callable => false,
168
155
  :puttable => false,
169
- :coupon => 0,
170
156
  :convertible => false,
157
+ :include_expired => false,
171
158
  :next_option_partial => false, }
172
159
 
173
160
  # NB: ContractDetails reference - to self!
@@ -181,8 +168,12 @@ module IB
181
168
  def serialize *fields
182
169
  [(fields.include?(:con_id) ? [con_id] : []),
183
170
  symbol,
184
- sec_type,
185
- (fields.include?(:option) ? [expiry, strike, right, multiplier] : []),
171
+ self[:sec_type],
172
+ (fields.include?(:option) ?
173
+ [expiry,
174
+ strike,
175
+ self[:right],
176
+ multiplier] : []),
186
177
  exchange,
187
178
  (fields.include?(:primary_exchange) ? [primary_exchange] : []),
188
179
  currency,
@@ -213,9 +204,16 @@ module IB
213
204
  end
214
205
  end
215
206
 
216
- # Redefined in BAG subclass
207
+ # Defined in Contract, not BAG subclass to keep code DRY
217
208
  def serialize_legs *fields
218
- []
209
+ case
210
+ when !bag?
211
+ []
212
+ when legs.empty?
213
+ [0]
214
+ else
215
+ [legs.size, legs.map { |leg| leg.serialize *fields }].flatten
216
+ end
219
217
  end
220
218
 
221
219
  # This produces a string uniquely identifying this contract, in the format used
@@ -263,12 +261,12 @@ module IB
263
261
  exchange && other.exchange && exchange != other.exchange
264
262
 
265
263
  # Comparison for Bonds and Options
266
- if sec_type == SECURITY_TYPES[:bond] || sec_type == SECURITY_TYPES[:option]
264
+ if bond? || option?
267
265
  return false if right != other.right || strike != other.strike
268
266
  return false if multiplier && other.multiplier && multiplier != other.multiplier
269
- return false if expiry[0..5] != other.expiry[0..5]
270
- return false unless expiry[6..7] == other.expiry[6..7] ||
271
- expiry[6..7].empty? || other.expiry[6..7].empty?
267
+ return false if expiry && expiry[0..5] != other.expiry[0..5]
268
+ return false unless expiry && (expiry[6..7] == other.expiry[6..7] ||
269
+ expiry[6..7].empty? || other.expiry[6..7].empty?)
272
270
  end
273
271
 
274
272
  # All else being equal...
@@ -283,13 +281,39 @@ module IB
283
281
  end
284
282
 
285
283
  def to_human
286
- "<Contract: " + [symbol, sec_type, expiry, strike, right, exchange, currency].join("-") + ">"
284
+ "<Contract: " +
285
+ [symbol,
286
+ sec_type,
287
+ (expiry == '' ? nil : expiry),
288
+ (right == :none ? nil : right),
289
+ (strike == 0 ? nil : strike),
290
+ exchange,
291
+ currency
292
+ ].compact.join(" ") + ">"
287
293
  end
288
294
 
289
295
  def to_short
290
296
  "#{symbol}#{expiry}#{strike}#{right}#{exchange}#{currency}"
291
297
  end
292
298
 
299
+ # Testing for type of contract:
300
+
301
+ def bag?
302
+ self[:sec_type] == 'BAG'
303
+ end
304
+
305
+ def bond?
306
+ self[:sec_type] == 'BOND'
307
+ end
308
+
309
+ def stock?
310
+ self[:sec_type] == 'STK'
311
+ end
312
+
313
+ def option?
314
+ self[:sec_type] == 'OPT'
315
+ end
316
+
293
317
  end # class Contract
294
318
  end # module Contracts
295
319
  end # module Models
@@ -5,6 +5,14 @@ module IB
5
5
  module Contracts
6
6
  class Option < Contract
7
7
 
8
+ validates_numericality_of :strike, :greater_than => 0
9
+ validates_format_of :sec_type, :with => /^option$/,
10
+ :message => "should be an option"
11
+ validates_format_of :local_symbol, :with => /^\w+\s*\d{15}$|^$/,
12
+ :message => "invalid OSI code"
13
+ validates_format_of :right, :with => /^put$|^call$/,
14
+ :message => "should be put or call"
15
+
8
16
  # For Options, this is contract's OSI (Option Symbology Initiative) name/code
9
17
  alias osi local_symbol
10
18
 
@@ -16,8 +24,8 @@ module IB
16
24
  # Make valid IB Contract definition from OSI (Option Symbology Initiative) code.
17
25
  # NB: Simply making a new Contract with *local_symbol* (osi) property set to a
18
26
  # valid OSI code works just as well, just do NOT set *expiry*, *right* or
19
- # *strike* properties at the same time.
20
- # This class method provided as a backup, to show how to analyse OSI codes.
27
+ # *strike* properties in this case.
28
+ # This class method provided as a backup and shows how to analyse OSI codes.
21
29
  def self.from_osi osi
22
30
 
23
31
  # Parse contract's OSI (OCC Option Symbology Initiative) code
@@ -32,25 +40,26 @@ module IB
32
40
 
33
41
  # Set correct expiry date - IB expiry date differs from OSI if expiry date
34
42
  # falls on Saturday (see https://github.com/arvicco/option_mower/issues/4)
35
- expiry_date = Time.new(year, month, day)
36
- expiry_date = Time.new(year, month, day-1) if expiry_date.saturday?
43
+ expiry_date = Time.utc(year, month, day)
44
+ expiry_date = Time.utc(year, month, day-1) if expiry_date.wday == 6
37
45
 
38
46
  new :symbol => symbol,
39
47
  :exchange => "SMART",
40
- :expiry => expiry_date.to_ib,
48
+ :expiry => expiry_date.to_ib[2..7], # YYMMDD
41
49
  :right => right,
42
50
  :strike => strike
43
51
  end
44
52
 
45
53
  def initialize opts = {}
46
54
  super opts
47
- self[:sec_type] = IB::SECURITY_TYPES[:option]
55
+ self.sec_type = 'OPT'
48
56
  self[:description] ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}"
49
57
  end
50
58
 
51
59
  def to_human
52
- "<Option: " + [symbol, expiry, right, strike, exchange, currency].join("-") + ">"
60
+ "<Option: " + [symbol, expiry, right, strike, exchange, currency].join(" ") + ">"
53
61
  end
62
+
54
63
  end # class Option
55
64
  end # class Contract
56
65
  end # module Models
@@ -1,29 +1,30 @@
1
- require 'ib-ruby/models/model'
2
-
3
1
  module IB
4
2
  module Models
5
3
  # This is IB Order execution report.
6
4
  # Instantiate with a Hash of attributes, to be auto-set via initialize in Model.
7
- class Execution < Model
5
+ class Execution < Model.for(:execution)
6
+ include ModelProperties
7
+
8
8
  prop :order_id, # int: order id. TWS orders have a fixed order id of 0.
9
- :client_id, # int: id of the client that placed the order.
10
- # TWS orders have a fixed client id of 0.
11
- :perm_id, # int: TWS id used to identify orders, remains
12
- :exec_id, # String: Unique order execution id.
13
- # the same over TWS sessions.
14
- :time, # String: The order execution time.
9
+ :client_id, # int: client id. TWS orders have a fixed client id of 0.
10
+ :perm_id, # int: TWS id used to identify orders over TWS sessions
11
+ :exec_id, # String: Unique order execution id over TWS sessions.
12
+ :time, # # TODO: convert into Time object?
13
+ # String: The order execution time.
15
14
  :exchange, # String: Exchange that executed the order.
15
+ :order_ref, # int: Same order_ref as in corresponding Order
16
+ [:account_name, :account_number], # String: The customer account number.
16
17
  :price, # double: The order execution price.
17
18
  :average_price, # double: Average price. Used in regular trades, combo
18
19
  # trades and legs of the combo.
19
20
  :shares, # int: The number of shares filled.
20
21
  :cumulative_quantity, # int: Cumulative quantity. Used in regular
21
22
  # trades, combo trades and legs of the combo
22
- :liquidation, # int: This position is liquidated last should the need arise.
23
- :order_ref, # int: Same order_ref as in corresponding Order
24
- [:account_name, :account_number], # String: The customer account number.
25
- :side => # String: Was the transaction a buy or a sale: BOT|SLD
26
- {:set => proc { |val| self[:side] = val.to_s.upcase[0..0] == 'B' ? :buy : :sell }}
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
25
+
26
+ # Extra validations
27
+ validates_numericality_of :shares, :cumulative_quantity, :price, :average_price
27
28
 
28
29
  DEFAULT_PROPS = {:order_id => 0,
29
30
  :client_id => 0,
@@ -31,12 +32,27 @@ module IB
31
32
  :price => 0,
32
33
  :perm_id => 0,
33
34
  :liquidation => 0, }
35
+ # Comparison
36
+ def == other
37
+ perm_id == other.perm_id &&
38
+ order_id == other.order_id && # ((p __LINE__)||true) &&
39
+ client_id == other.client_id &&
40
+ exec_id == other.exec_id &&
41
+ time == other.time &&
42
+ exchange == other.exchange &&
43
+ order_ref == other.order_ref &&
44
+ side == other.side
45
+ # TODO: || compare all attributes!
46
+ end
34
47
 
35
- def to_s
36
- "<Execution #{time}: #{side} #{shares} @ #{price} on #{exchange}, " +
37
- "cumulative: #{cumulative_quantity} @ #{average_price}, " +
38
- "order: #{order_id}/#{perm_id}#{order_ref}, exec: #{exec_id}>"
48
+ def to_human
49
+ "<Execution: #{time} #{side} #{shares} at #{price} on #{exchange}, " +
50
+ "cumulative #{cumulative_quantity} at #{average_price}, " +
51
+ "ids #{order_id}/#{perm_id}/#{exec_id}>"
39
52
  end
53
+
54
+ alias to_s to_human
55
+
40
56
  end # Execution
41
57
  end # module Models
42
58
  end # module IB
@@ -1,21 +1,29 @@
1
- require 'ib-ruby/models/model_properties'
2
-
3
1
  module IB
4
2
  module Models
5
3
 
6
4
  # Base IB data Model class, in future it will be developed into ActiveModel
7
5
  class Model
8
- extend ModelProperties
9
-
10
- attr_reader :created_at
11
6
 
12
- DEFAULT_PROPS = {}
7
+ # IB Models can be either database-backed, or not
8
+ # require 'ib-ruby/db' # to make IB models database-backed
9
+ 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
18
+ else
19
+ Model
20
+ end
21
+ end
13
22
 
14
23
  # If a opts hash is given, keys are taken as attribute names, values as data.
15
24
  # The model instance fields are then set automatically from the opts Hash.
16
25
  def initialize(opts={})
17
26
  error "Argument must be a Hash", :args unless opts.is_a?(Hash)
18
- @created_at = Time.now
19
27
 
20
28
  props = self.class::DEFAULT_PROPS.merge(opts)
21
29
  props.keys.each { |key| self.send("#{key}=", props[key]) }
@@ -1,63 +1,120 @@
1
+ require 'active_model'
2
+ require 'active_support/concern'
3
+
1
4
  module IB
2
5
  module Models
3
6
 
4
- # Module adding prop macro
7
+ # Module adds prop Macro and
5
8
  module ModelProperties
9
+ extend ActiveSupport::Concern
10
+
11
+ DEFAULT_PROPS = {}
6
12
 
7
- def prop *properties
8
- prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
13
+ ### Instance methods
9
14
 
10
- properties.each { |names| define_property names, '' }
11
- prop_hash.each { |names, type| define_property names, type }
15
+ attr_accessor :created_at
16
+
17
+ def initialize opts={}
18
+ @created_at = Time.now
19
+ super self.class::DEFAULT_PROPS.merge(opts)
12
20
  end
13
21
 
14
- def define_property names, body
15
- aliases = [names].flatten
16
- name = aliases.shift
17
- instance_eval do
22
+ included do
23
+
24
+ ### Class macros
25
+
26
+ def self.prop *properties
27
+ prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
28
+
29
+ properties.each { |names| define_property names, '' }
30
+ prop_hash.each { |names, type| define_property names, type }
31
+ end
32
+
33
+ def self.define_property names, body
34
+ aliases = [names].flatten
35
+ name = aliases.shift
36
+ instance_eval do
18
37
 
19
- define_property_methods name, body
38
+ define_property_methods name, body
20
39
 
21
- aliases.each do |ali|
22
- alias_method "#{ali}", name
23
- alias_method "#{ali}=", "#{name}="
40
+ aliases.each do |ali|
41
+ alias_method "#{ali}", name
42
+ alias_method "#{ali}=", "#{name}="
43
+ end
24
44
  end
25
45
  end
26
- end
27
46
 
28
- def define_property_methods name, body={}
29
- #p name, body
30
- case body
31
- when '' # default getter and setter
32
- define_property_methods name
33
- when Proc # setter
34
- define_property_methods name, :set => body
35
- when Array # [setter, getter, validators]
36
- define_property_methods name,
37
- :get => body[0],
38
- :set => body[1],
39
- :validate => body[2]
40
- when Hash # recursion ends HERE!
41
- define_method(name, &body[:get] || proc { self[name] })
42
-
43
- define_method("#{name}=", &body[:set] || proc { |value| self[name] = value })
44
-
45
- [body[:validate]].flatten.compact.each do |validator|
46
- case validator
47
- when Proc
48
- validates_each name, &validator
49
- when Hash
50
- validates name, validator
47
+ def self.define_property_methods name, body={}
48
+ #p name, body
49
+ case body
50
+ when '' # default getter and setter
51
+ define_property_methods name
52
+
53
+ when Array # [setter, getter, validators]
54
+ define_property_methods name,
55
+ :get => body[0],
56
+ :set => body[1],
57
+ :validate => body[2]
58
+
59
+ when Hash # recursion base case
60
+
61
+ getter = case # Define getter
62
+ when body[:get].respond_to?(:call)
63
+ body[:get]
64
+ when body[:get]
65
+ proc { self[name].send "to_#{body[:get]}" }
66
+ when VALUES[name] # property is encoded
67
+ proc { VALUES[name][self[name]] }
68
+ else
69
+ proc { self[name] }
70
+ end
71
+ define_method name, &getter
72
+
73
+ setter = case # Define setter
74
+ when body[:set].respond_to?(:call)
75
+ body[:set]
76
+ when body[:set]
77
+ proc { |value| self[name] = value.send "to_#{body[:set]}" }
78
+ when CODES[name] # property is encoded
79
+ proc { |value| self[name] = CODES[name][value] || value }
80
+ else
81
+ proc { |value| self[name] = value }
82
+ end
83
+ define_method "#{name}=", &setter
84
+
85
+ # Define validator(s)
86
+ [body[:validate]].flatten.compact.each do |validator|
87
+ case validator
88
+ when Proc
89
+ validates_each name, &validator
90
+ when Hash
91
+ validates name, validator.dup
92
+ end
51
93
  end
52
94
 
53
- end
54
- else
55
- define_property_methods name, :set =>
56
- proc { |value| self[name] = value.send "to_#{body}" }
95
+ # TODO define self[:name] accessors for :virtual and :flag properties
96
+
97
+ else # setter given
98
+ define_property_methods name, :set => body
99
+ end
57
100
  end
58
- end
59
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
+ []
114
+ end
115
+ end
116
+
117
+ end # included
60
118
  end # module ModelProperties
61
- end
119
+ end # module Models
62
120
  end
63
-