activerecord 1.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -1,15 +1,21 @@
1
1
  module ActiveRecord
2
+ # = Active Record Aggregations
2
3
  module Aggregations # :nodoc:
3
- def self.append_features(base)
4
- super
5
- base.extend(ClassMethods)
4
+ extend ActiveSupport::Concern
5
+
6
+ def clear_aggregation_cache #:nodoc:
7
+ self.class.reflect_on_all_aggregations.to_a.each do |assoc|
8
+ instance_variable_set "@#{assoc.name}", nil
9
+ end unless self.new_record?
6
10
  end
7
11
 
8
- # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
9
- # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
10
- # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
11
- # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
12
- # and how it can be turned back into attributes (when the entity is saved to the database). Example:
12
+ # Active Record implements aggregation through a macro-like class method called +composed_of+
13
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
14
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
15
+ # to the macro adds a description of how the value objects are created from the attributes of
16
+ # the entity object (when the entity is initialized either as a new object or from finding an
17
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
18
+ # the database).
13
19
  #
14
20
  # class Customer < ActiveRecord::Base
15
21
  # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
@@ -25,10 +31,10 @@ module ActiveRecord
25
31
  # class Money
26
32
  # include Comparable
27
33
  # attr_reader :amount, :currency
28
- # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
29
- #
30
- # def initialize(amount, currency = "USD")
31
- # @amount, @currency = amount, currency
34
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
35
+ #
36
+ # def initialize(amount, currency = "USD")
37
+ # @amount, @currency = amount, currency
32
38
  # end
33
39
  #
34
40
  # def exchange_to(other_currency)
@@ -42,7 +48,7 @@ module ActiveRecord
42
48
  #
43
49
  # def <=>(other_money)
44
50
  # if currency == other_money.currency
45
- # among <=> amount
51
+ # amount <=> amount
46
52
  # else
47
53
  # amount <=> other_money.exchange_to(currency).amount
48
54
  # end
@@ -51,114 +57,220 @@ module ActiveRecord
51
57
  #
52
58
  # class Address
53
59
  # attr_reader :street, :city
54
- # def initialize(street, city)
55
- # @street, @city = street, city
60
+ # def initialize(street, city)
61
+ # @street, @city = street, city
56
62
  # end
57
63
  #
58
- # def close_to?(other_address)
59
- # city == other_address.city
64
+ # def close_to?(other_address)
65
+ # city == other_address.city
60
66
  # end
61
67
  #
62
68
  # def ==(other_address)
63
69
  # city == other_address.city && street == other_address.street
64
70
  # end
65
71
  # end
66
- #
67
- # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
68
- # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
69
- # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
72
+ #
73
+ # Now it's possible to access attributes from the database through the value objects instead. If
74
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
75
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
76
+ # objects just like you would any other attribute, though:
70
77
  #
71
78
  # customer.balance = Money.new(20) # sets the Money value object and the attribute
72
79
  # customer.balance # => Money value object
73
- # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
80
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
74
81
  # customer.balance > Money.new(10) # => true
75
82
  # customer.balance == Money.new(20) # => true
76
83
  # customer.balance < Money.new(5) # => false
77
84
  #
78
- # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
79
- # determine the order of the parameters. Example:
85
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
86
+ # of the mappings will determine the order of the parameters.
80
87
  #
81
88
  # customer.address_street = "Hyancintvej"
82
89
  # customer.address_city = "Copenhagen"
83
90
  # customer.address # => Address.new("Hyancintvej", "Copenhagen")
84
91
  # customer.address = Address.new("May Street", "Chicago")
85
- # customer.address_street # => "May Street"
86
- # customer.address_city # => "Chicago"
92
+ # customer.address_street # => "May Street"
93
+ # customer.address_city # => "Chicago"
87
94
  #
88
95
  # == Writing value objects
89
96
  #
90
- # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
91
- # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
92
- # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
93
- # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
94
- # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
95
- #
96
- # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
97
- # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
98
- # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
99
- # changed through other means than the writer method.
100
- #
101
- # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
102
- # change it afterwards will result in a TypeError.
103
- #
104
- # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
105
- # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
97
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
98
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
99
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
100
+ # unlike entity objects where equality is determined by identity. An entity class such as Customer can
101
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
102
+ # determined by object or relational unique identifiers (such as primary keys). Normal
103
+ # ActiveRecord::Base classes are entity objects.
104
+ #
105
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
106
+ # its amount changed after creation. Create a new Money object with the new value instead. This
107
+ # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
108
+ # its own values. Active Record won't persist value objects that have been changed through means
109
+ # other than the writer method.
110
+ #
111
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
112
+ # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
113
+ #
114
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
115
+ # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
116
+ #
117
+ # == Custom constructors and converters
118
+ #
119
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
120
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
121
+ # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
122
+ # a custom constructor to be specified.
123
+ #
124
+ # When a new value is assigned to the value object the default assumption is that the new value
125
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
126
+ # converted to an instance of value class if necessary.
127
+ #
128
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
129
+ # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
130
+ # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
131
+ # values can be assigned to the value object using either another NetAddr::CIDR object, a string
132
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
133
+ # these requirements:
134
+ #
135
+ # class NetworkResource < ActiveRecord::Base
136
+ # composed_of :cidr,
137
+ # :class_name => 'NetAddr::CIDR',
138
+ # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
139
+ # :allow_nil => true,
140
+ # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
141
+ # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
142
+ # end
143
+ #
144
+ # # This calls the :constructor
145
+ # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
146
+ #
147
+ # # These assignments will both use the :converter
148
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
149
+ # network_resource.cidr = '192.168.0.1/24'
150
+ #
151
+ # # This assignment won't use the :converter as the value is already an instance of the value class
152
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
153
+ #
154
+ # # Saving and then reloading will use the :constructor on reload
155
+ # network_resource.save
156
+ # network_resource.reload
157
+ #
158
+ # == Finding records by a value object
159
+ #
160
+ # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
161
+ # by specifying an instance of the value object in the conditions hash. The following example
162
+ # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
163
+ #
164
+ # Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
165
+ #
106
166
  module ClassMethods
107
- # Adds the a reader and writer method for manipulating a value object, so
108
- # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
167
+ # Adds reader and writer methods for manipulating a value object:
168
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
109
169
  #
110
170
  # Options are:
111
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
112
- # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
113
- # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
114
- # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
115
- # to a constructor parameter on the value class.
171
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
172
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
173
+ # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
174
+ # with this option.
175
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
176
+ # object. Each mapping is represented as an array where the first item is the name of the
177
+ # entity attribute and the second item is the name the attribute in the value object. The
178
+ # order in which mappings are defined determine the order in which attributes are sent to the
179
+ # value class constructor.
180
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
181
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
182
+ # mapped attributes.
183
+ # This defaults to +false+.
184
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
185
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
186
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
187
+ # to instantiate a <tt>:class_name</tt> object.
188
+ # The default is <tt>:new</tt>.
189
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
190
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
191
+ # passed the single value that is used in the assignment and is only called if the new value is
192
+ # not an instance of <tt>:class_name</tt>.
116
193
  #
117
194
  # Option examples:
118
195
  # composed_of :temperature, :mapping => %w(reading celsius)
119
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
196
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
120
197
  # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
198
+ # composed_of :gps_location
199
+ # composed_of :gps_location, :allow_nil => true
200
+ # composed_of :ip_address,
201
+ # :class_name => 'IPAddr',
202
+ # :mapping => %w(ip to_i),
203
+ # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
204
+ # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
205
+ #
121
206
  def composed_of(part_id, options = {})
122
- validate_options([ :class_name, :mapping ], options.keys)
207
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
123
208
 
124
209
  name = part_id.id2name
125
- class_name = options[:class_name] || name_to_class_name(name)
126
- mapping = options[:mapping]
210
+ class_name = options[:class_name] || name.camelize
211
+ mapping = options[:mapping] || [ name, name ]
212
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
213
+ allow_nil = options[:allow_nil] || false
214
+ constructor = options[:constructor] || :new
215
+ converter = options[:converter]
216
+
217
+ reader_method(name, class_name, mapping, allow_nil, constructor)
218
+ writer_method(name, class_name, mapping, allow_nil, converter)
127
219
 
128
- reader_method(name, class_name, mapping)
129
- writer_method(name, class_name, mapping)
220
+ create_reflection(:composed_of, part_id, options, self)
130
221
  end
131
222
 
132
223
  private
133
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
134
- def validate_options(valid_option_keys, supplied_option_keys)
135
- unknown_option_keys = supplied_option_keys - valid_option_keys
136
- raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
137
- end
224
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
225
+ module_eval do
226
+ define_method(name) do |*args|
227
+ force_reload = args.first || false
138
228
 
139
- def name_to_class_name(name)
140
- name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
141
- end
142
-
143
- def reader_method(name, class_name, mapping)
144
- module_eval <<-end_eval
145
- def #{name}(force_reload = false)
146
- if @#{name}.nil? || force_reload
147
- @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
229
+ unless instance_variable_defined?("@#{name}")
230
+ instance_variable_set("@#{name}", nil)
148
231
  end
149
-
150
- return @#{name}
232
+
233
+ if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
234
+ attrs = mapping.collect {|pair| read_attribute(pair.first)}
235
+ object = case constructor
236
+ when Symbol
237
+ class_name.constantize.send(constructor, *attrs)
238
+ when Proc, Method
239
+ constructor.call(*attrs)
240
+ else
241
+ raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
242
+ end
243
+ instance_variable_set("@#{name}", object)
244
+ end
245
+ instance_variable_get("@#{name}")
151
246
  end
152
- end_eval
153
- end
154
-
155
- def writer_method(name, class_name, mapping)
156
- module_eval <<-end_eval
157
- def #{name}=(part)
158
- @#{name} = part.freeze
159
- #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
247
+ end
248
+
249
+ end
250
+
251
+ def writer_method(name, class_name, mapping, allow_nil, converter)
252
+ module_eval do
253
+ define_method("#{name}=") do |part|
254
+ if part.nil? && allow_nil
255
+ mapping.each { |pair| self[pair.first] = nil }
256
+ instance_variable_set("@#{name}", nil)
257
+ else
258
+ unless part.is_a?(class_name.constantize) || converter.nil?
259
+ part = case converter
260
+ when Symbol
261
+ class_name.constantize.send(converter, part)
262
+ when Proc, Method
263
+ converter.call(part)
264
+ else
265
+ raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
266
+ end
267
+ end
268
+
269
+ mapping.each { |pair| self[pair.first] = part.send(pair.last) }
270
+ instance_variable_set("@#{name}", part.freeze)
271
+ end
160
272
  end
161
- end_eval
273
+ end
162
274
  end
163
275
  end
164
276
  end