ib-ruby 0.7.4 → 0.7.6

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