activerecord 3.1.10 → 4.2.11

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.

Potentially problematic release.


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

Files changed (237) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +1837 -338
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +39 -43
  5. data/examples/performance.rb +51 -20
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +57 -43
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +47 -39
  10. data/lib/active_record/associations/association.rb +71 -85
  11. data/lib/active_record/associations/association_scope.rb +138 -89
  12. data/lib/active_record/associations/belongs_to_association.rb +65 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +9 -3
  14. data/lib/active_record/associations/builder/association.rb +125 -29
  15. data/lib/active_record/associations/builder/belongs_to.rb +91 -60
  16. data/lib/active_record/associations/builder/collection_association.rb +69 -49
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +113 -42
  18. data/lib/active_record/associations/builder/has_many.rb +8 -64
  19. data/lib/active_record/associations/builder/has_one.rb +12 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +22 -29
  21. data/lib/active_record/associations/collection_association.rb +294 -187
  22. data/lib/active_record/associations/collection_proxy.rb +961 -94
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +118 -23
  25. data/lib/active_record/associations/has_many_through_association.rb +115 -45
  26. data/lib/active_record/associations/has_one_association.rb +57 -24
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +76 -102
  29. data/lib/active_record/associations/join_dependency/join_base.rb +7 -9
  30. data/lib/active_record/associations/join_dependency/join_part.rb +30 -37
  31. data/lib/active_record/associations/join_dependency.rb +230 -156
  32. data/lib/active_record/associations/preloader/association.rb +96 -55
  33. data/lib/active_record/associations/preloader/collection_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.rb +1 -1
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +61 -32
  38. data/lib/active_record/associations/preloader.rb +113 -87
  39. data/lib/active_record/associations/singular_association.rb +29 -13
  40. data/lib/active_record/associations/through_association.rb +37 -19
  41. data/lib/active_record/associations.rb +505 -371
  42. data/lib/active_record/attribute.rb +163 -0
  43. data/lib/active_record/attribute_assignment.rb +212 -0
  44. data/lib/active_record/attribute_decorators.rb +66 -0
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  46. data/lib/active_record/attribute_methods/dirty.rb +141 -51
  47. data/lib/active_record/attribute_methods/primary_key.rb +87 -36
  48. data/lib/active_record/attribute_methods/query.rb +5 -4
  49. data/lib/active_record/attribute_methods/read.rb +74 -117
  50. data/lib/active_record/attribute_methods/serialization.rb +70 -0
  51. data/lib/active_record/attribute_methods/time_zone_conversion.rb +49 -47
  52. data/lib/active_record/attribute_methods/write.rb +60 -21
  53. data/lib/active_record/attribute_methods.rb +409 -48
  54. data/lib/active_record/attribute_set/builder.rb +106 -0
  55. data/lib/active_record/attribute_set.rb +81 -0
  56. data/lib/active_record/attributes.rb +147 -0
  57. data/lib/active_record/autosave_association.rb +279 -232
  58. data/lib/active_record/base.rb +84 -1969
  59. data/lib/active_record/callbacks.rb +66 -28
  60. data/lib/active_record/coders/json.rb +13 -0
  61. data/lib/active_record/coders/yaml_column.rb +18 -21
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +422 -243
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +170 -194
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +32 -19
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -57
  67. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +125 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +273 -170
  70. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +50 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +731 -254
  72. data/lib/active_record/connection_adapters/abstract/transaction.rb +215 -0
  73. data/lib/active_record/connection_adapters/abstract_adapter.rb +339 -95
  74. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +946 -0
  75. data/lib/active_record/connection_adapters/column.rb +33 -221
  76. data/lib/active_record/connection_adapters/connection_specification.rb +275 -0
  77. data/lib/active_record/connection_adapters/mysql2_adapter.rb +140 -602
  78. data/lib/active_record/connection_adapters/mysql_adapter.rb +254 -756
  79. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +93 -0
  80. data/lib/active_record/connection_adapters/postgresql/column.rb +20 -0
  81. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +232 -0
  82. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +100 -0
  83. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  84. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +46 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +11 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +36 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/float.rb +21 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +13 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/integer.rb +11 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +35 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +43 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +79 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +19 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/time.rb +11 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid.rb +36 -0
  108. data/lib/active_record/connection_adapters/postgresql/quoting.rb +108 -0
  109. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  110. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +152 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +596 -0
  112. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  113. data/lib/active_record/connection_adapters/postgresql_adapter.rb +445 -902
  114. data/lib/active_record/connection_adapters/schema_cache.rb +94 -0
  115. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +578 -25
  116. data/lib/active_record/connection_handling.rb +132 -0
  117. data/lib/active_record/core.rb +579 -0
  118. data/lib/active_record/counter_cache.rb +159 -102
  119. data/lib/active_record/dynamic_matchers.rb +140 -0
  120. data/lib/active_record/enum.rb +197 -0
  121. data/lib/active_record/errors.rb +102 -34
  122. data/lib/active_record/explain.rb +38 -0
  123. data/lib/active_record/explain_registry.rb +30 -0
  124. data/lib/active_record/explain_subscriber.rb +29 -0
  125. data/lib/active_record/fixture_set/file.rb +56 -0
  126. data/lib/active_record/fixtures.rb +318 -260
  127. data/lib/active_record/gem_version.rb +15 -0
  128. data/lib/active_record/inheritance.rb +247 -0
  129. data/lib/active_record/integration.rb +113 -0
  130. data/lib/active_record/legacy_yaml_adapter.rb +30 -0
  131. data/lib/active_record/locale/en.yml +8 -1
  132. data/lib/active_record/locking/optimistic.rb +80 -52
  133. data/lib/active_record/locking/pessimistic.rb +27 -5
  134. data/lib/active_record/log_subscriber.rb +25 -18
  135. data/lib/active_record/migration/command_recorder.rb +130 -38
  136. data/lib/active_record/migration/join_table.rb +15 -0
  137. data/lib/active_record/migration.rb +532 -201
  138. data/lib/active_record/model_schema.rb +342 -0
  139. data/lib/active_record/nested_attributes.rb +229 -139
  140. data/lib/active_record/no_touching.rb +52 -0
  141. data/lib/active_record/null_relation.rb +81 -0
  142. data/lib/active_record/persistence.rb +304 -99
  143. data/lib/active_record/query_cache.rb +25 -43
  144. data/lib/active_record/querying.rb +68 -0
  145. data/lib/active_record/railtie.rb +86 -45
  146. data/lib/active_record/railties/console_sandbox.rb +3 -4
  147. data/lib/active_record/railties/controller_runtime.rb +7 -4
  148. data/lib/active_record/railties/databases.rake +198 -377
  149. data/lib/active_record/railties/jdbcmysql_error.rb +2 -2
  150. data/lib/active_record/readonly_attributes.rb +23 -0
  151. data/lib/active_record/reflection.rb +516 -165
  152. data/lib/active_record/relation/batches.rb +96 -45
  153. data/lib/active_record/relation/calculations.rb +221 -144
  154. data/lib/active_record/relation/delegation.rb +140 -0
  155. data/lib/active_record/relation/finder_methods.rb +362 -243
  156. data/lib/active_record/relation/merger.rb +193 -0
  157. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  158. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  159. data/lib/active_record/relation/predicate_builder.rb +135 -41
  160. data/lib/active_record/relation/query_methods.rb +982 -155
  161. data/lib/active_record/relation/spawn_methods.rb +50 -110
  162. data/lib/active_record/relation.rb +371 -180
  163. data/lib/active_record/result.rb +109 -12
  164. data/lib/active_record/runtime_registry.rb +22 -0
  165. data/lib/active_record/sanitization.rb +191 -0
  166. data/lib/active_record/schema.rb +19 -13
  167. data/lib/active_record/schema_dumper.rb +111 -61
  168. data/lib/active_record/schema_migration.rb +53 -0
  169. data/lib/active_record/scoping/default.rb +135 -0
  170. data/lib/active_record/scoping/named.rb +164 -0
  171. data/lib/active_record/scoping.rb +87 -0
  172. data/lib/active_record/serialization.rb +7 -45
  173. data/lib/active_record/serializers/xml_serializer.rb +14 -65
  174. data/lib/active_record/statement_cache.rb +111 -0
  175. data/lib/active_record/store.rb +205 -0
  176. data/lib/active_record/tasks/database_tasks.rb +299 -0
  177. data/lib/active_record/tasks/mysql_database_tasks.rb +159 -0
  178. data/lib/active_record/tasks/postgresql_database_tasks.rb +101 -0
  179. data/lib/active_record/tasks/sqlite_database_tasks.rb +55 -0
  180. data/lib/active_record/timestamp.rb +35 -14
  181. data/lib/active_record/transactions.rb +141 -74
  182. data/lib/active_record/translation.rb +22 -0
  183. data/lib/active_record/type/big_integer.rb +13 -0
  184. data/lib/active_record/type/binary.rb +50 -0
  185. data/lib/active_record/type/boolean.rb +31 -0
  186. data/lib/active_record/type/date.rb +50 -0
  187. data/lib/active_record/type/date_time.rb +54 -0
  188. data/lib/active_record/type/decimal.rb +64 -0
  189. data/lib/active_record/type/decimal_without_scale.rb +11 -0
  190. data/lib/active_record/type/decorator.rb +14 -0
  191. data/lib/active_record/type/float.rb +19 -0
  192. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  193. data/lib/active_record/type/integer.rb +59 -0
  194. data/lib/active_record/type/mutable.rb +16 -0
  195. data/lib/active_record/type/numeric.rb +36 -0
  196. data/lib/active_record/type/serialized.rb +62 -0
  197. data/lib/active_record/type/string.rb +40 -0
  198. data/lib/active_record/type/text.rb +11 -0
  199. data/lib/active_record/type/time.rb +26 -0
  200. data/lib/active_record/type/time_value.rb +38 -0
  201. data/lib/active_record/type/type_map.rb +64 -0
  202. data/lib/active_record/type/unsigned_integer.rb +15 -0
  203. data/lib/active_record/type/value.rb +110 -0
  204. data/lib/active_record/type.rb +23 -0
  205. data/lib/active_record/validations/associated.rb +27 -18
  206. data/lib/active_record/validations/presence.rb +67 -0
  207. data/lib/active_record/validations/uniqueness.rb +125 -66
  208. data/lib/active_record/validations.rb +37 -30
  209. data/lib/active_record/version.rb +5 -7
  210. data/lib/active_record.rb +80 -25
  211. data/lib/rails/generators/active_record/migration/migration_generator.rb +54 -9
  212. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  213. data/lib/rails/generators/active_record/migration/templates/migration.rb +25 -11
  214. data/lib/rails/generators/active_record/migration.rb +11 -8
  215. data/lib/rails/generators/active_record/model/model_generator.rb +17 -4
  216. data/lib/rails/generators/active_record/model/templates/model.rb +5 -2
  217. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  218. data/lib/rails/generators/active_record.rb +3 -11
  219. metadata +132 -53
  220. data/examples/associations.png +0 -0
  221. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -62
  222. data/lib/active_record/associations/join_helper.rb +0 -55
  223. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  224. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -135
  225. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -556
  226. data/lib/active_record/dynamic_finder_match.rb +0 -56
  227. data/lib/active_record/dynamic_scope_match.rb +0 -23
  228. data/lib/active_record/identity_map.rb +0 -163
  229. data/lib/active_record/named_scope.rb +0 -200
  230. data/lib/active_record/observer.rb +0 -121
  231. data/lib/active_record/session_store.rb +0 -358
  232. data/lib/active_record/test_case.rb +0 -69
  233. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -17
  234. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  235. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  236. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  237. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -0,0 +1,163 @@
1
+ module ActiveRecord
2
+ class Attribute # :nodoc:
3
+ class << self
4
+ def from_database(name, value, type)
5
+ FromDatabase.new(name, value, type)
6
+ end
7
+
8
+ def from_user(name, value, type)
9
+ FromUser.new(name, value, type)
10
+ end
11
+
12
+ def with_cast_value(name, value, type)
13
+ WithCastValue.new(name, value, type)
14
+ end
15
+
16
+ def null(name)
17
+ Null.new(name)
18
+ end
19
+
20
+ def uninitialized(name, type)
21
+ Uninitialized.new(name, type)
22
+ end
23
+ end
24
+
25
+ attr_reader :name, :value_before_type_cast, :type
26
+
27
+ # This method should not be called directly.
28
+ # Use #from_database or #from_user
29
+ def initialize(name, value_before_type_cast, type)
30
+ @name = name
31
+ @value_before_type_cast = value_before_type_cast
32
+ @type = type
33
+ end
34
+
35
+ def value
36
+ # `defined?` is cheaper than `||=` when we get back falsy values
37
+ @value = original_value unless defined?(@value)
38
+ @value
39
+ end
40
+
41
+ def original_value
42
+ type_cast(value_before_type_cast)
43
+ end
44
+
45
+ def value_for_database
46
+ type.type_cast_for_database(value)
47
+ end
48
+
49
+ def changed_from?(old_value)
50
+ type.changed?(old_value, value, value_before_type_cast)
51
+ end
52
+
53
+ def changed_in_place_from?(old_value)
54
+ has_been_read? && type.changed_in_place?(old_value, value)
55
+ end
56
+
57
+ def with_value_from_user(value)
58
+ self.class.from_user(name, value, type)
59
+ end
60
+
61
+ def with_value_from_database(value)
62
+ self.class.from_database(name, value, type)
63
+ end
64
+
65
+ def with_cast_value(value)
66
+ self.class.with_cast_value(name, value, type)
67
+ end
68
+
69
+ def type_cast(*)
70
+ raise NotImplementedError
71
+ end
72
+
73
+ def initialized?
74
+ true
75
+ end
76
+
77
+ def came_from_user?
78
+ false
79
+ end
80
+
81
+ def ==(other)
82
+ self.class == other.class &&
83
+ name == other.name &&
84
+ value_before_type_cast == other.value_before_type_cast &&
85
+ type == other.type
86
+ end
87
+
88
+ protected
89
+
90
+ def initialize_dup(other)
91
+ if defined?(@value) && @value.duplicable?
92
+ @value = @value.dup
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def has_been_read?
99
+ defined?(@value)
100
+ end
101
+
102
+ class FromDatabase < Attribute # :nodoc:
103
+ def type_cast(value)
104
+ type.type_cast_from_database(value)
105
+ end
106
+ end
107
+
108
+ class FromUser < Attribute # :nodoc:
109
+ def type_cast(value)
110
+ type.type_cast_from_user(value)
111
+ end
112
+
113
+ def came_from_user?
114
+ true
115
+ end
116
+ end
117
+
118
+ class WithCastValue < Attribute # :nodoc:
119
+ def type_cast(value)
120
+ value
121
+ end
122
+
123
+ def changed_in_place_from?(old_value)
124
+ false
125
+ end
126
+ end
127
+
128
+ class Null < Attribute # :nodoc:
129
+ def initialize(name)
130
+ super(name, nil, Type::Value.new)
131
+ end
132
+
133
+ def value
134
+ nil
135
+ end
136
+
137
+ def with_value_from_database(value)
138
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
139
+ end
140
+ alias_method :with_value_from_user, :with_value_from_database
141
+ end
142
+
143
+ class Uninitialized < Attribute # :nodoc:
144
+ def initialize(name, type)
145
+ super(name, nil, type)
146
+ end
147
+
148
+ def value
149
+ if block_given?
150
+ yield name
151
+ end
152
+ end
153
+
154
+ def value_for_database
155
+ end
156
+
157
+ def initialized?
158
+ false
159
+ end
160
+ end
161
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
162
+ end
163
+ end
@@ -0,0 +1,212 @@
1
+ require 'active_model/forbidden_attributes_protection'
2
+
3
+ module ActiveRecord
4
+ module AttributeAssignment
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::ForbiddenAttributesProtection
7
+
8
+ # Allows you to set all the attributes by passing in a hash of attributes with
9
+ # keys matching the attribute names (which again matches the column names).
10
+ #
11
+ # If the passed hash responds to <tt>permitted?</tt> method and the return value
12
+ # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
13
+ # exception is raised.
14
+ #
15
+ # cat = Cat.new(name: "Gorby", status: "yawning")
16
+ # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
17
+ # cat.assign_attributes(status: "sleeping")
18
+ # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
19
+ #
20
+ # New attributes will be persisted in the database when the object is saved.
21
+ #
22
+ # Aliased to <tt>attributes=</tt>.
23
+ def assign_attributes(new_attributes)
24
+ if !new_attributes.respond_to?(:stringify_keys)
25
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
26
+ end
27
+ return if new_attributes.blank?
28
+
29
+ attributes = new_attributes.stringify_keys
30
+ multi_parameter_attributes = []
31
+ nested_parameter_attributes = []
32
+
33
+ attributes = sanitize_for_mass_assignment(attributes)
34
+
35
+ attributes.each do |k, v|
36
+ if k.include?("(")
37
+ multi_parameter_attributes << [ k, v ]
38
+ elsif v.is_a?(Hash)
39
+ nested_parameter_attributes << [ k, v ]
40
+ else
41
+ _assign_attribute(k, v)
42
+ end
43
+ end
44
+
45
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
46
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
47
+ end
48
+
49
+ alias attributes= assign_attributes
50
+
51
+ private
52
+
53
+ def _assign_attribute(k, v)
54
+ public_send("#{k}=", v)
55
+ rescue NoMethodError, NameError
56
+ if respond_to?("#{k}=")
57
+ raise
58
+ else
59
+ raise UnknownAttributeError.new(self, k)
60
+ end
61
+ end
62
+
63
+ # Assign any deferred nested attributes after the base attributes have been set.
64
+ def assign_nested_parameter_attributes(pairs)
65
+ pairs.each { |k, v| _assign_attribute(k, v) }
66
+ end
67
+
68
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
69
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
70
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
71
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
72
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
73
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
74
+ def assign_multiparameter_attributes(pairs)
75
+ execute_callstack_for_multiparameter_attributes(
76
+ extract_callstack_for_multiparameter_attributes(pairs)
77
+ )
78
+ end
79
+
80
+ def execute_callstack_for_multiparameter_attributes(callstack)
81
+ errors = []
82
+ callstack.each do |name, values_with_empty_parameters|
83
+ begin
84
+ send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
85
+ rescue => ex
86
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
87
+ end
88
+ end
89
+ unless errors.empty?
90
+ error_descriptions = errors.map { |ex| ex.message }.join(",")
91
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
92
+ end
93
+ end
94
+
95
+ def extract_callstack_for_multiparameter_attributes(pairs)
96
+ attributes = {}
97
+
98
+ pairs.each do |(multiparameter_name, value)|
99
+ attribute_name = multiparameter_name.split("(").first
100
+ attributes[attribute_name] ||= {}
101
+
102
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
103
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
104
+ end
105
+
106
+ attributes
107
+ end
108
+
109
+ def type_cast_attribute_value(multiparameter_name, value)
110
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
111
+ end
112
+
113
+ def find_parameter_position(multiparameter_name)
114
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
115
+ end
116
+
117
+ class MultiparameterAttribute #:nodoc:
118
+ attr_reader :object, :name, :values, :cast_type
119
+
120
+ def initialize(object, name, values)
121
+ @object = object
122
+ @name = name
123
+ @values = values
124
+ end
125
+
126
+ def read_value
127
+ return if values.values.compact.empty?
128
+
129
+ @cast_type = object.type_for_attribute(name)
130
+ klass = cast_type.klass
131
+
132
+ if klass == Time
133
+ read_time
134
+ elsif klass == Date
135
+ read_date
136
+ else
137
+ read_other
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def instantiate_time_object(set_values)
144
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
145
+ Time.zone.local(*set_values)
146
+ else
147
+ Time.send(object.class.default_timezone, *set_values)
148
+ end
149
+ end
150
+
151
+ def read_time
152
+ # If column is a :time (and not :date or :datetime) there is no need to validate if
153
+ # there are year/month/day fields
154
+ if cast_type.type == :time
155
+ # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
156
+ { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
157
+ values[key] ||= value
158
+ end
159
+ else
160
+ # else column is a timestamp, so if Date bits were not provided, error
161
+ validate_required_parameters!([1,2,3])
162
+
163
+ # If Date bits were provided but blank, then return nil
164
+ return if blank_date_parameter?
165
+ end
166
+
167
+ max_position = extract_max_param(6)
168
+ set_values = values.values_at(*(1..max_position))
169
+ # If Time bits are not there, then default to 0
170
+ (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
171
+ instantiate_time_object(set_values)
172
+ end
173
+
174
+ def read_date
175
+ return if blank_date_parameter?
176
+ set_values = values.values_at(1,2,3)
177
+ begin
178
+ Date.new(*set_values)
179
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
180
+ instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
181
+ end
182
+ end
183
+
184
+ def read_other
185
+ max_position = extract_max_param
186
+ positions = (1..max_position)
187
+ validate_required_parameters!(positions)
188
+
189
+ values.slice(*positions)
190
+ end
191
+
192
+ # Checks whether some blank date parameter exists. Note that this is different
193
+ # than the validate_required_parameters! method, since it just checks for blank
194
+ # positions instead of missing ones, and does not raise in case one blank position
195
+ # exists. The caller is responsible to handle the case of this returning true.
196
+ def blank_date_parameter?
197
+ (1..3).any? { |position| values[position].blank? }
198
+ end
199
+
200
+ # If some position is not provided, it errors out a missing parameter exception.
201
+ def validate_required_parameters!(positions)
202
+ if missing_parameter = positions.detect { |position| !values.key?(position) }
203
+ raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
204
+ end
205
+ end
206
+
207
+ def extract_max_param(upper_cap = 100)
208
+ [values.keys.max, upper_cap].min
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,66 @@
1
+ module ActiveRecord
2
+ module AttributeDecorators # :nodoc:
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
7
+ self.attribute_type_decorations = TypeDecorator.new
8
+ end
9
+
10
+ module ClassMethods # :nodoc:
11
+ def decorate_attribute_type(column_name, decorator_name, &block)
12
+ matcher = ->(name, _) { name == column_name.to_s }
13
+ key = "_#{column_name}_#{decorator_name}"
14
+ decorate_matching_attribute_types(matcher, key, &block)
15
+ end
16
+
17
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
18
+ clear_caches_calculated_from_columns
19
+ decorator_name = decorator_name.to_s
20
+
21
+ # Create new hashes so we don't modify parent classes
22
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
23
+ end
24
+
25
+ private
26
+
27
+ def add_user_provided_columns(*)
28
+ super.map do |column|
29
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
30
+ column.with_type(decorated_type)
31
+ end
32
+ end
33
+ end
34
+
35
+ class TypeDecorator # :nodoc:
36
+ delegate :clear, to: :@decorations
37
+
38
+ def initialize(decorations = {})
39
+ @decorations = decorations
40
+ end
41
+
42
+ def merge(*args)
43
+ TypeDecorator.new(@decorations.merge(*args))
44
+ end
45
+
46
+ def apply(name, type)
47
+ decorations = decorators_for(name, type)
48
+ decorations.inject(type) do |new_type, block|
49
+ block.call(new_type)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def decorators_for(name, type)
56
+ matching(name, type).map(&:last)
57
+ end
58
+
59
+ def matching(name, type)
60
+ @decorations.values.select do |(matcher, _)|
61
+ matcher.call(name, type)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,30 +1,75 @@
1
1
  module ActiveRecord
2
2
  module AttributeMethods
3
+ # = Active Record Attribute Methods Before Type Cast
4
+ #
5
+ # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
6
+ # read the value of the attributes before typecasting and deserialization.
7
+ #
8
+ # class Task < ActiveRecord::Base
9
+ # end
10
+ #
11
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
12
+ # task.id # => 1
13
+ # task.completed_on # => Sun, 21 Oct 2012
14
+ #
15
+ # task.attributes_before_type_cast
16
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
17
+ # task.read_attribute_before_type_cast('id') # => "1"
18
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
19
+ #
20
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
21
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
22
+ # suffix.
23
+ #
24
+ # task.id_before_type_cast # => "1"
25
+ # task.completed_on_before_type_cast # => "2012-10-21"
3
26
  module BeforeTypeCast
4
27
  extend ActiveSupport::Concern
5
28
 
6
29
  included do
7
30
  attribute_method_suffix "_before_type_cast"
31
+ attribute_method_suffix "_came_from_user?"
8
32
  end
9
33
 
34
+ # Returns the value of the attribute identified by +attr_name+ before
35
+ # typecasting and deserialization.
36
+ #
37
+ # class Task < ActiveRecord::Base
38
+ # end
39
+ #
40
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
41
+ # task.read_attribute('id') # => 1
42
+ # task.read_attribute_before_type_cast('id') # => '1'
43
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
44
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
45
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
10
46
  def read_attribute_before_type_cast(attr_name)
11
- @attributes[attr_name]
47
+ @attributes[attr_name.to_s].value_before_type_cast
12
48
  end
13
49
 
14
50
  # Returns a hash of attributes before typecasting and deserialization.
51
+ #
52
+ # class Task < ActiveRecord::Base
53
+ # end
54
+ #
55
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
56
+ # task.attributes
57
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
58
+ # task.attributes_before_type_cast
59
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
15
60
  def attributes_before_type_cast
16
- @attributes
61
+ @attributes.values_before_type_cast
17
62
  end
18
63
 
19
64
  private
20
65
 
21
66
  # Handle *_before_type_cast for method_missing.
22
67
  def attribute_before_type_cast(attribute_name)
23
- if attribute_name == 'id'
24
- read_attribute_before_type_cast(self.class.primary_key)
25
- else
26
- read_attribute_before_type_cast(attribute_name)
27
- end
68
+ read_attribute_before_type_cast(attribute_name)
69
+ end
70
+
71
+ def attribute_came_from_user?(attribute_name)
72
+ @attributes[attribute_name].came_from_user?
28
73
  end
29
74
  end
30
75
  end