ib-ruby 0.5.21 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/HISTORY +8 -0
  2. data/README.md +46 -27
  3. data/TODO +13 -2
  4. data/VERSION +1 -1
  5. data/bin/generic_data.rb +26 -0
  6. data/bin/place_order +1 -1
  7. data/lib/ib-ruby/connection.rb +126 -65
  8. data/lib/ib-ruby/messages/incoming.rb +3 -3
  9. data/lib/ib-ruby/models/bar.rb +11 -11
  10. data/lib/ib-ruby/models/combo_leg.rb +23 -29
  11. data/lib/ib-ruby/models/contract/bag.rb +34 -2
  12. data/lib/ib-ruby/models/contract/option.rb +2 -2
  13. data/lib/ib-ruby/models/contract.rb +151 -197
  14. data/lib/ib-ruby/models/execution.rb +27 -45
  15. data/lib/ib-ruby/models/model.rb +10 -4
  16. data/lib/ib-ruby/models/model_properties.rb +63 -0
  17. data/lib/ib-ruby/models/order.rb +274 -320
  18. data/lib/ib-ruby/symbols/stocks.rb +11 -5
  19. data/spec/account_helper.rb +80 -0
  20. data/spec/ib-ruby/connection_spec.rb +195 -52
  21. data/spec/ib-ruby/messages/incoming_spec.rb +4 -4
  22. data/spec/ib-ruby/models/combo_leg_spec.rb +1 -0
  23. data/spec/ib-ruby/models/contract_spec.rb +1 -1
  24. data/spec/ib-ruby/models/execution_spec.rb +73 -0
  25. data/spec/integration/account_info_spec.rb +12 -59
  26. data/spec/integration/contract_info_spec.rb +23 -37
  27. data/spec/integration/depth_data_spec.rb +4 -4
  28. data/spec/integration/historic_data_spec.rb +15 -27
  29. data/spec/integration/market_data_spec.rb +74 -61
  30. data/spec/integration/option_data_spec.rb +5 -48
  31. data/spec/integration/orders/execution_spec.rb +26 -31
  32. data/spec/integration/orders/open_order +2 -0
  33. data/spec/integration/orders/placement_spec.rb +28 -28
  34. data/spec/integration/orders/valid_ids_spec.rb +11 -11
  35. data/spec/integration_helper.rb +46 -32
  36. data/spec/message_helper.rb +4 -38
  37. data/spec/spec_helper.rb +2 -3
  38. metadata +9 -2
@@ -27,179 +27,154 @@ module IB
27
27
  end
28
28
 
29
29
  # Fields are Strings unless noted otherwise
30
- attr_accessor :con_id, # int: The unique contract identifier.
31
- :symbol, # This is the symbol of the underlying asset.
32
- :sec_type, # Security type. Valid values are: SECURITY_TYPES
33
- :expiry, # The expiration date. Use the format YYYYMM.
34
- :strike, # double: The strike price.
35
- :right, # Specifies a Put or Call. Valid values are: P, PUT, C, CALL
36
- :multiplier, # Specifies a future or option contract multiplier
37
- # String? (only necessary when multiple possibilities exist)
38
-
39
- :exchange, # The order destination, such as Smart.
40
- :currency, # Ambiguities MAY require that currency field be specified,
41
- # for example, when SMART is the exchange and IBM is being
42
- # requested (IBM can trade in GBP or USD).
43
-
44
- :local_symbol, # Local exchange symbol of the underlying asset
45
- :primary_exchange, # pick a non-aggregate (ie not the SMART) exchange
46
- # that the contract trades on. DO NOT SET TO SMART.
47
-
48
- :include_expired, # When true, contract details requests and historical
49
- # data queries can be performed pertaining to expired contracts.
50
- # Note: Historical data queries on expired contracts are
51
- # limited to the last year of the contracts life, and are
52
- # only supported for expired futures contracts.
53
- # This field can NOT be set to true for orders.
54
-
55
- :sec_id_type, # Security identifier, when querying contract details or
56
- # when placing orders. Supported identifiers are:
57
- # - ISIN (Example: Apple: US0378331005)
58
- # - CUSIP (Example: Apple: 037833100)
59
- # - SEDOL (6-AN + check digit. Example: BAE: 0263494)
60
- # - RIC (exchange-independent RIC Root and exchange-
61
- # identifying suffix. Ex: AAPL.O for Apple on NASDAQ.)
62
- :sec_id, # Unique identifier of the given secIdType.
63
-
64
- # COMBOS
65
- :legs_description, # received in open order for all combos
66
- :legs # Dynamic memory structure used to store the leg
67
- # definitions for this contract.
30
+ prop :con_id, # int: The unique contract identifier.
31
+ :symbol, # This is the symbol of the underlying asset.
32
+ :sec_type, # Security type. Valid values are: SECURITY_TYPES
33
+ :strike, # double: The strike price.
34
+ :exchange, # The order destination, such as Smart.
35
+ :currency, # Only needed if there is an ambiguity, e.g. when SMART exchange
36
+ # and IBM is being requested (IBM can trade in GBP or USD).
37
+
38
+ :local_symbol, # Local exchange symbol of the underlying asset
39
+ :include_expired, # 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
+ :sec_id_type, # Security identifier, when querying contract details or
47
+ # when placing orders. Supported identifiers are:
48
+ # - ISIN (Example: Apple: US0378331005)
49
+ # - CUSIP (Example: Apple: 037833100)
50
+ # - SEDOL (6-AN + check digit. Example: BAE: 0263494)
51
+ # - RIC (exchange-independent RIC Root and exchange-
52
+ # identifying suffix. Ex: AAPL.O for Apple on NASDAQ.)
53
+ :sec_id, # Unique identifier of the given secIdType.
54
+
55
+ # COMBOS
56
+ :legs_description, # received in OpenOrder for all combos
57
+
58
+ :multiplier => :i,
59
+ # Future/option contract multiplier (only needed when multiple possibilities exist)
60
+
61
+ :primary_exchange =>
62
+ # non-aggregate (ie not the SMART) exchange that the contract trades on.
63
+ proc { |val|
64
+ val.upcase! if val.is_a?(String)
65
+ raise(ArgumentError.new("Don't set primary_exchange to smart")) if val == 'SMART'
66
+ self[:primary_exchange] = val
67
+ },
68
+
69
+ :right => # Specifies a Put or Call. Valid input values are: P, PUT, C, CALL
70
+ proc { |val|
71
+ self[:right] =
72
+ case val.to_s.upcase
73
+ when '', '0', '?'
74
+ nil
75
+ when 'PUT', 'P'
76
+ 'PUT'
77
+ when 'CALL', 'C'
78
+ 'CALL'
79
+ else
80
+ raise ArgumentError.new("Invalid right '#{val}' (must be one of PUT, CALL, P, C)")
81
+ end
82
+ },
83
+
84
+ :expiry => # The expiration date. Use the format YYYYMM.
85
+ proc { |val|
86
+ self[:expiry] =
87
+ case val.to_s
88
+ when /\d{6,8}/
89
+ val.to_s
90
+ when nil, ''
91
+ nil
92
+ else
93
+ raise ArgumentError.new("Invalid expiry '#{val}' (must be in format YYYYMM or YYYYMMDD)")
94
+ end
95
+ },
96
+
97
+ :sec_type => # Security type. Valid values are: SECURITY_TYPES
98
+ proc { |val|
99
+ val = nil if !val.nil? && val.empty?
100
+ unless val.nil? || SECURITY_TYPES.values.include?(val)
101
+ raise(ArgumentError.new("Invalid security type '#{val}' (must be one of #{SECURITY_TYPES.values}"))
102
+ end
103
+ self[:sec_type] = val
104
+ }
68
105
 
69
106
  # ContractDetails fields are bundled into Contract proper, as it should be
70
107
  # All fields Strings, unless specified otherwise:
71
- attr_accessor :summary, # NB: ContractDetails reference - to self!
72
- :market_name, # The market name for this contract.
73
- :trading_class, # The trading class name for this contract.
74
- :min_tick, # double: The minimum price tick.
75
- :price_magnifier, # int: Allows execution and strike prices to be
76
- # reported consistently with market data, historical data and the
77
- # order price: Z on LIFFE is reported in index points, not GBP.
78
-
79
- :order_types, # The list of valid order types for this contract.
80
- :valid_exchanges, # The list of exchanges this contract is traded on.
81
- :under_con_id, # int: The underlying contract ID.
82
- :long_name, # Descriptive name of the asset.
83
- :contract_month, # Typically the contract month of the underlying for
84
- # a futures contract.
85
-
86
- # The industry classification of the underlying/product:
87
- :industry, # Wide industry. For example, Financial.
88
- :category, # Industry category. For example, InvestmentSvc.
89
- :subcategory, # Subcategory. For example, Brokerage.
90
- :time_zone, # The ID of the time zone for the trading hours of the
91
- # product. For example, EST.
92
- :trading_hours, # The trading hours of the product. For example:
93
- # 20090507:0700-1830,1830-2330;20090508:CLOSED.
94
- :liquid_hours, # The liquid trading hours of the product. For example,
95
- # 20090507:0930-1600;20090508:CLOSED.
96
-
97
- # Bond values:
98
- :cusip, # The nine-character bond CUSIP or the 12-character SEDOL.
99
- :ratings, # Credit rating of the issuer. Higher credit rating generally
100
- # indicates a less risky investment. Bond ratings are from
101
- # Moody's and S&P respectively.
102
- :desc_append, # Additional descriptive information about the bond.
103
- :bond_type, # The type of bond, such as "CORP."
104
- :coupon_type, # The type of bond coupon.
105
- :callable, # bool: Can be called by the issuer under certain conditions.
106
- :puttable, # bool: Can be sold back to the issuer under certain conditions
107
- :coupon, # double: The interest rate used to calculate the amount you
108
- # will receive in interest payments over the year. default 0
109
- :convertible, # bool: Can be converted to stock under certain conditions.
110
- :maturity, # The date on which the issuer must repay bond face value
111
- :issue_date, # The date the bond was issued.
112
- :next_option_date, # only if bond has embedded options.
113
- :next_option_type, # only if bond has embedded options.
114
- :next_option_partial, # bool: # only if bond has embedded options.
115
- :notes # Additional notes, if populated for the bond in IB's database
108
+ prop :market_name, # The market name for this contract.
109
+ :trading_class, # The trading class name for this contract.
110
+ :min_tick, # double: The minimum price tick.
111
+ :price_magnifier, # int: Allows execution and strike prices to be
112
+ # reported consistently with market data, historical data and the
113
+ # order price: Z on LIFFE is reported in index points, not GBP.
114
+
115
+ :order_types, # The list of valid order types for this contract.
116
+ :valid_exchanges, # The list of exchanges this contract is traded on.
117
+ :under_con_id, # int: The underlying contract ID.
118
+ :long_name, # Descriptive name of the asset.
119
+ :contract_month, # The contract month of the underlying for a futures contract.
120
+
121
+ # The industry classification of the underlying/product:
122
+ :industry, # Wide industry. For example, Financial.
123
+ :category, # Industry category. For example, InvestmentSvc.
124
+ :subcategory, # Subcategory. For example, Brokerage.
125
+ :time_zone, # Time zone for the trading hours of the product. For example, EST.
126
+ :trading_hours, # The trading hours of the product. For example:
127
+ # 20090507:0700-1830,1830-2330;20090508:CLOSED.
128
+ :liquid_hours, # The liquid trading hours of the product. For example,
129
+ # 20090507:0930-1600;20090508:CLOSED.
130
+
131
+ # Bond values:
132
+ :cusip, # The nine-character bond CUSIP or the 12-character SEDOL.
133
+ :ratings, # Credit rating of the issuer. Higher rating is less risky investment.
134
+ # Bond ratings are from Moody's and S&P respectively.
135
+ :desc_append, # Additional descriptive information about the bond.
136
+ :bond_type, # The type of bond, such as "CORP."
137
+ :coupon_type, # The type of bond coupon.
138
+ :callable, # bool: Can be called by the issuer under certain conditions.
139
+ :puttable, # bool: Can be sold back to the issuer under certain conditions
140
+ :coupon, # double: The interest rate used to calculate the amount you
141
+ # will receive in interest payments over the year. default 0
142
+ :convertible, # bool: Can be converted to stock under certain conditions.
143
+ :maturity, # The date on which the issuer must repay bond face value
144
+ :issue_date, # The date the bond was issued.
145
+ :next_option_date, # only if bond has embedded options.
146
+ :next_option_type, # only if bond has embedded options.
147
+ :next_option_partial, # bool: # only if bond has embedded options.
148
+ :notes # Additional notes, if populated for the bond in IB's database
116
149
 
117
150
  # Used for Delta-Neutral Combo contracts only!
118
151
  # UnderComp fields are bundled into Contract proper, as it should be.
119
- attr_accessor :under_comp, # if not nil, attributes below are sent to server
120
- #:under_con_id is is already defined in ContractDetails section
121
- :under_delta, # double: The underlying stock or future delta.
122
- :under_price # double: The price of the underlying.
152
+ prop :under_comp, # if not nil, attributes below are sent to server
153
+ #:under_con_id is is already defined in ContractDetails section
154
+ :under_delta, # double: The underlying stock or future delta.
155
+ :under_price # double: The price of the underlying.
123
156
 
124
157
  attr_accessor :description # NB: local to ib-ruby, not part of TWS.
125
158
 
126
- alias combo_legs legs
127
- alias combo_legs= legs=
128
- alias combo_legs_description legs_description
129
- alias combo_legs_description= legs_description=
130
-
131
- def initialize opts = {}
132
- # Assign defaults to properties first!
133
- @con_id = 0
134
- @strike = 0
135
- @sec_type = ''
136
- @exchange = 'SMART'
137
- @include_expired = false
138
- @legs = Array.new
139
-
140
- # These properties are from ContractDetails
141
- @under_con_id = 0
142
- @min_tick = 0
143
- @callable = false
144
- @puttable = false
145
- @coupon = 0
146
- @convertible = false
147
- @next_option_partial = false
148
-
149
- super opts
150
- end
151
-
152
- # This property is from ContractDetails
159
+ DEFAULT_PROPS = {:con_id => 0,
160
+ :strike => 0,
161
+ :exchange => 'SMART',
162
+ :include_expired => false,
163
+
164
+ # These properties are from ContractDetails
165
+ :under_con_id => 0,
166
+ :min_tick => 0,
167
+ :callable => false,
168
+ :puttable => false,
169
+ :coupon => 0,
170
+ :convertible => false,
171
+ :next_option_partial => false, }
172
+
173
+ # NB: ContractDetails reference - to self!
153
174
  def summary
154
175
  self
155
176
  end
156
177
 
157
- # some protective filters
158
- def primary_exchange= x
159
- x.upcase! if x.is_a?(String)
160
-
161
- # per http://chuckcaplan.com/twsapi/index.php/Class%20Contract
162
- raise(ArgumentError.new("Don't set primary_exchange to smart")) if x == 'SMART'
163
-
164
- @primary_exchange = x
165
- end
166
-
167
- def right= x
168
- @right =
169
- case x.to_s.upcase
170
- when '', '0', '?'
171
- nil
172
- when 'PUT', 'P'
173
- 'PUT'
174
- when 'CALL', 'C'
175
- 'CALL'
176
- else
177
- raise ArgumentError.new("Invalid right '#{x}' (must be one of PUT, CALL, P, C)")
178
- end
179
- end
180
-
181
- def expiry= x
182
- @expiry =
183
- case x.to_s
184
- when /\d{6,8}/
185
- x.to_s
186
- when ''
187
- nil
188
- else
189
- raise ArgumentError.new("Invalid expiry '#{x}' (must be in format YYYYMM or YYYYMMDD)")
190
- end
191
- end
192
-
193
- def sec_type= x
194
- x = nil if !x.nil? && x.empty?
195
- raise(ArgumentError.new("Invalid security type '#{x}' (must be one of #{SECURITY_TYPES.values}")) unless x.nil? || SECURITY_TYPES.values.include?(x)
196
- @sec_type = x
197
- end
198
-
199
- def multiplier= x
200
- @multiplier = x.to_i
201
- end
202
-
203
178
  # This returns an Array of data from the given contract.
204
179
  # Different messages serialize contracts differently. Go figure.
205
180
  # Note that it does NOT include the combo legs.
@@ -225,22 +200,6 @@ module IB
225
200
  serialize :option, *fields
226
201
  end
227
202
 
228
- # This produces a string uniquely identifying this contract, in the format used
229
- # for command line arguments in the IB-Ruby examples. The format is:
230
- #
231
- # symbol:security_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
232
- #
233
- # Fields not needed for a particular security should be left blank
234
- # (e.g. strike and right are only relevant for options.)
235
- #
236
- # For example, to query the British pound futures contract trading on Globex
237
- # expiring in September, 2008, the string is:
238
- #
239
- # GBP:FUT:200809:::62500:GLOBEX::USD:
240
- def serialize_ib_ruby version
241
- serialize.join(":")
242
- end
243
-
244
203
  # Serialize under_comp parameters
245
204
  def serialize_under_comp *args
246
205
  # EClientSocket.java, line 471:
@@ -254,27 +213,25 @@ module IB
254
213
  end
255
214
  end
256
215
 
257
- ### Leg-related methods (better suited to BAG subclass?)
258
-
259
- # Some messages send open_close too, some don't. WTF.
260
- # "BAG" is not really a contract, but a combination (combo) of securities.
261
- # AKA basket or bag of securities. Individual securities in combo are represented
262
- # by ComboLeg objects.
216
+ # Redefined in BAG subclass
263
217
  def serialize_legs *fields
264
- return [] unless sec_type.upcase == "BAG"
265
- return [0] if legs.empty? || legs.nil?
266
- [legs.size, legs.map { |leg| leg.serialize *fields }]
267
- end
268
-
269
- # Check if two Contracts have same legs (maybe in different order)
270
- def same_legs? other
271
- legs == other.legs ||
272
- legs_description.split(',').sort == other.legs_description.split(',').sort
218
+ []
273
219
  end
274
220
 
275
- # IB-equivalent leg description. TODO: Rewrite with self[:legs_description]
276
- def legs_description
277
- @legs_description || legs.map { |leg| "#{leg.con_id}|#{leg.weight}" }.join(',')
221
+ # This produces a string uniquely identifying this contract, in the format used
222
+ # for command line arguments in the IB-Ruby examples. The format is:
223
+ #
224
+ # symbol:security_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
225
+ #
226
+ # Fields not needed for a particular security should be left blank
227
+ # (e.g. strike and right are only relevant for options.)
228
+ #
229
+ # For example, to query the British pound futures contract trading on Globex
230
+ # expiring in September, 2008, the string is:
231
+ #
232
+ # GBP:FUT:200809:::62500:GLOBEX::USD:
233
+ def serialize_ib_ruby version
234
+ serialize.join(":")
278
235
  end
279
236
 
280
237
  # Contract comparison
@@ -296,9 +253,6 @@ module IB
296
253
  # Different currency
297
254
  return false if currency && other.currency && currency != other.currency
298
255
 
299
- # Different legs
300
- return false unless same_legs? other
301
-
302
256
  # Same con_id for all Bags, but unknown for new Contracts...
303
257
  # 0 or nil con_id matches any
304
258
  return false if con_id != 0 && other.con_id != 0 &&
@@ -5,54 +5,36 @@ module IB
5
5
  # This is IB Order execution report.
6
6
  # Instantiate with a Hash of attributes, to be auto-set via initialize in Model.
7
7
  class Execution < Model
8
- attr_accessor :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
- :exec_id, # String: Unique order execution id.
12
- :time, # String: The order execution time.
13
- :account_name, #String: The customer account number.
14
- :exchange, # String: Exchange that executed the order.
15
- :side, # String: Was the transaction a buy or a sale: BOT|SLD
16
- :shares, # int: The number of shares filled.
17
- :price, # double: The order execution price.
18
- :perm_id, # int: TWS id used to identify orders, remains
19
- # the same over TWS sessions.
20
- :liquidation, # int: Identifies the position as one to be liquidated
21
- # last should the need arise.
22
- :cumulative_quantity, # int: Cumulative quantity. Used in regular
23
- # trades, combo trades and legs of the combo
24
- :average_price # double: Average price. Used in regular trades, combo
25
- # trades and legs of the combo.
26
- # Legacy
27
- alias account_number account_name
28
- alias account_number= account_name=
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.
15
+ :exchange, # String: Exchange that executed the order.
16
+ :price, # double: The order execution price.
17
+ :average_price, # double: Average price. Used in regular trades, combo
18
+ # trades and legs of the combo.
19
+ :shares, # int: The number of shares filled.
20
+ :cumulative_quantity, # int: Cumulative quantity. Used in regular
21
+ # trades, combo trades and legs of the combo
22
+ :liquidation, # int: This position is liquidated last should the need arise.
23
+ [:account_name, :account_number], # String: The customer account number.
24
+ :side => # String: Was the transaction a buy or a sale: BOT|SLD
25
+ {:set => proc { |val| self[:side] = val.to_s.upcase[0..0] == 'B' ? :buy : :sell }}
29
26
 
30
- def side= value
31
- @side = case value
32
- when 'BOT'
33
- :BUY
34
- when 'SLD'
35
- :SELL
36
- else
37
- value
38
- end
39
- end
40
-
41
- def initialize opts = {}
42
- @order_id = 0
43
- @client_id = 0
44
- @shares = 0
45
- @price = 0
46
- @perm_id = 0
47
- @liquidation = 0
48
-
49
- super opts
50
- end
27
+ DEFAULT_PROPS = {:order_id => 0,
28
+ :client_id => 0,
29
+ :shares => 0,
30
+ :price => 0,
31
+ :perm_id => 0,
32
+ :liquidation => 0, }
51
33
 
52
34
  def to_s
53
- "<Execution #{@time}: #{@side} #{@shares} @ #{@price} on #{@exchange}, " +
54
- "cumulative: #{@cumulative_quantity} @ #{@average_price}, " +
55
- "ids: #{@order_id} order, #{@perm_id} perm, #{@exec_id} exec>"
35
+ "<Execution #{time}: #{side} #{shares} @ #{price} on #{exchange}, " +
36
+ "cumulative: #{cumulative_quantity} @ #{average_price}, " +
37
+ "ids: #{order_id} order, #{perm_id} perm, #{exec_id} exec>"
56
38
  end
57
39
  end # Execution
58
40
  end # module Models
@@ -1,27 +1,33 @@
1
+ require 'ib-ruby/models/model_properties'
2
+
1
3
  module IB
2
4
  module Models
3
5
 
4
6
  # Base IB data Model class, in future it will be developed into ActiveModel
5
7
  class Model
8
+ extend ModelProperties
9
+
6
10
  attr_reader :created_at
7
11
 
12
+ DEFAULT_PROPS = {}
13
+
8
14
  # If a opts hash is given, keys are taken as attribute names, values as data.
9
15
  # The model instance fields are then set automatically from the opts Hash.
10
- #
11
16
  def initialize(opts={})
12
17
  raise ArgumentError.new("Argument must be a Hash") unless opts.is_a?(Hash)
13
18
  @created_at = Time.now
14
19
 
15
- opts.keys.each { |key| self.send("#{key}=", opts[key]) }
20
+ props = self.class::DEFAULT_PROPS.merge(opts)
21
+ props.keys.each { |key| self.send("#{key}=", props[key]) }
16
22
  end
17
23
 
18
24
  # ActiveModel-style attribute accessors
19
25
  def [] key
20
- self.send key
26
+ instance_variable_get "@#{key}".to_sym
21
27
  end
22
28
 
23
29
  def []= key, val
24
- self.send "#{key}=", val
30
+ instance_variable_set "@#{key}".to_sym, val
25
31
  end
26
32
 
27
33
  end # Model
@@ -0,0 +1,63 @@
1
+ module IB
2
+ module Models
3
+
4
+ # Module adding prop macro
5
+ module ModelProperties
6
+
7
+ def prop *properties
8
+ prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
9
+
10
+ properties.each { |names| define_property names, '' }
11
+ prop_hash.each { |names, type| define_property names, type }
12
+ end
13
+
14
+ def define_property names, body
15
+ aliases = [names].flatten
16
+ name = aliases.shift
17
+ instance_eval do
18
+
19
+ define_property_methods name, body
20
+
21
+ aliases.each do |ali|
22
+ alias_method "#{ali}", name
23
+ alias_method "#{ali}=", "#{name}="
24
+ end
25
+ end
26
+ end
27
+
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
51
+ end
52
+
53
+ end
54
+ else
55
+ define_property_methods name, :set =>
56
+ proc { |value| self[name] = value.send "to_#{body}" }
57
+ end
58
+ end
59
+
60
+ end # module ModelProperties
61
+ end
62
+ end
63
+