tallty_duck_record 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +41 -0
  3. data/README.md +82 -0
  4. data/Rakefile +28 -0
  5. data/lib/core_ext/array_without_blank.rb +46 -0
  6. data/lib/duck_record.rb +65 -0
  7. data/lib/duck_record/associations.rb +130 -0
  8. data/lib/duck_record/associations/association.rb +271 -0
  9. data/lib/duck_record/associations/belongs_to_association.rb +71 -0
  10. data/lib/duck_record/associations/builder/association.rb +127 -0
  11. data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
  12. data/lib/duck_record/associations/builder/collection_association.rb +45 -0
  13. data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
  14. data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
  15. data/lib/duck_record/associations/builder/has_many.rb +11 -0
  16. data/lib/duck_record/associations/builder/has_one.rb +20 -0
  17. data/lib/duck_record/associations/builder/singular_association.rb +33 -0
  18. data/lib/duck_record/associations/collection_association.rb +476 -0
  19. data/lib/duck_record/associations/collection_proxy.rb +1160 -0
  20. data/lib/duck_record/associations/embeds_association.rb +92 -0
  21. data/lib/duck_record/associations/embeds_many_association.rb +203 -0
  22. data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
  23. data/lib/duck_record/associations/embeds_one_association.rb +48 -0
  24. data/lib/duck_record/associations/foreign_association.rb +11 -0
  25. data/lib/duck_record/associations/has_many_association.rb +17 -0
  26. data/lib/duck_record/associations/has_one_association.rb +39 -0
  27. data/lib/duck_record/associations/singular_association.rb +73 -0
  28. data/lib/duck_record/attribute.rb +213 -0
  29. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  30. data/lib/duck_record/attribute_assignment.rb +118 -0
  31. data/lib/duck_record/attribute_decorators.rb +89 -0
  32. data/lib/duck_record/attribute_methods.rb +325 -0
  33. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  34. data/lib/duck_record/attribute_methods/dirty.rb +107 -0
  35. data/lib/duck_record/attribute_methods/read.rb +78 -0
  36. data/lib/duck_record/attribute_methods/serialization.rb +66 -0
  37. data/lib/duck_record/attribute_methods/write.rb +70 -0
  38. data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
  39. data/lib/duck_record/attribute_set.rb +98 -0
  40. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  41. data/lib/duck_record/attributes.rb +262 -0
  42. data/lib/duck_record/base.rb +300 -0
  43. data/lib/duck_record/callbacks.rb +324 -0
  44. data/lib/duck_record/coders/json.rb +13 -0
  45. data/lib/duck_record/coders/yaml_column.rb +48 -0
  46. data/lib/duck_record/core.rb +262 -0
  47. data/lib/duck_record/define_callbacks.rb +23 -0
  48. data/lib/duck_record/enum.rb +139 -0
  49. data/lib/duck_record/errors.rb +71 -0
  50. data/lib/duck_record/inheritance.rb +130 -0
  51. data/lib/duck_record/locale/en.yml +46 -0
  52. data/lib/duck_record/model_schema.rb +71 -0
  53. data/lib/duck_record/nested_attributes.rb +555 -0
  54. data/lib/duck_record/nested_validate_association.rb +262 -0
  55. data/lib/duck_record/persistence.rb +39 -0
  56. data/lib/duck_record/readonly_attributes.rb +36 -0
  57. data/lib/duck_record/reflection.rb +650 -0
  58. data/lib/duck_record/serialization.rb +26 -0
  59. data/lib/duck_record/translation.rb +22 -0
  60. data/lib/duck_record/type.rb +77 -0
  61. data/lib/duck_record/type/array.rb +36 -0
  62. data/lib/duck_record/type/array_without_blank.rb +36 -0
  63. data/lib/duck_record/type/date.rb +7 -0
  64. data/lib/duck_record/type/date_time.rb +7 -0
  65. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  66. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  67. data/lib/duck_record/type/internal/timezone.rb +15 -0
  68. data/lib/duck_record/type/json.rb +6 -0
  69. data/lib/duck_record/type/registry.rb +97 -0
  70. data/lib/duck_record/type/serialized.rb +63 -0
  71. data/lib/duck_record/type/text.rb +9 -0
  72. data/lib/duck_record/type/time.rb +19 -0
  73. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  74. data/lib/duck_record/validations.rb +67 -0
  75. data/lib/duck_record/validations/subset.rb +74 -0
  76. data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
  77. data/lib/duck_record/version.rb +3 -0
  78. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  79. metadata +181 -0
@@ -0,0 +1,41 @@
1
+ module DuckRecord
2
+ class AttributeSet
3
+ # Attempts to do more intelligent YAML dumping of an
4
+ # DuckRecord::AttributeSet to reduce the size of the resulting string
5
+ class YAMLEncoder # :nodoc:
6
+ def initialize(default_types)
7
+ @default_types = default_types
8
+ end
9
+
10
+ def encode(attribute_set, coder)
11
+ coder["concise_attributes"] = attribute_set.each_value.map do |attr|
12
+ if attr.type.equal?(default_types[attr.name])
13
+ attr.with_type(nil)
14
+ else
15
+ attr
16
+ end
17
+ end
18
+ end
19
+
20
+ def decode(coder)
21
+ if coder["attributes"]
22
+ coder["attributes"]
23
+ else
24
+ attributes_hash = Hash[coder["concise_attributes"].map do |attr|
25
+ if attr.type.nil?
26
+ attr = attr.with_type(default_types[attr.name])
27
+ end
28
+ [attr.name, attr]
29
+ end]
30
+ AttributeSet.new(attributes_hash)
31
+ end
32
+ end
33
+
34
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
35
+ # Workaround for Ruby 2.2 'private attribute?' warning.
36
+ protected
37
+
38
+ attr_reader :default_types
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,262 @@
1
+ require "duck_record/attribute/user_provided_default"
2
+
3
+ module DuckRecord
4
+ # See DuckRecord::Attributes::ClassMethods for documentation
5
+ module Attributes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
10
+ self.attributes_to_define_after_schema_loads = {}
11
+ end
12
+
13
+ module ClassMethods
14
+ # Defines an attribute with a type on this model. It will override the
15
+ # type of existing attributes if needed. This allows control over how
16
+ # values are converted to and from SQL when assigned to a model. It also
17
+ # changes the behavior of values passed to
18
+ # {DuckRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
19
+ # your domain objects across much of Active Record, without having to
20
+ # rely on implementation details or monkey patching.
21
+ #
22
+ # +name+ The name of the methods to define attribute methods for, and the
23
+ # column which this will persist to.
24
+ #
25
+ # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
26
+ # to be used for this attribute. See the examples below for more
27
+ # information about providing custom type objects.
28
+ #
29
+ # ==== Options
30
+ #
31
+ # The following options are accepted:
32
+ #
33
+ # +default+ The default value to use when no value is provided. If this option
34
+ # is not passed, the previous default value (if any) will be used.
35
+ # Otherwise, the default will be +nil+.
36
+ #
37
+ # +array+ (PostgreSQL only) specifies that the type should be an array (see the
38
+ # examples below).
39
+ #
40
+ # +range+ (PostgreSQL only) specifies that the type should be a range (see the
41
+ # examples below).
42
+ #
43
+ # ==== Examples
44
+ #
45
+ # The type detected by Active Record can be overridden.
46
+ #
47
+ # # db/schema.rb
48
+ # create_table :store_listings, force: true do |t|
49
+ # t.decimal :price_in_cents
50
+ # end
51
+ #
52
+ # # app/models/store_listing.rb
53
+ # class StoreListing < DuckRecord::Base
54
+ # end
55
+ #
56
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
57
+ #
58
+ # # before
59
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
60
+ #
61
+ # class StoreListing < DuckRecord::Base
62
+ # attribute :price_in_cents, :integer
63
+ # end
64
+ #
65
+ # # after
66
+ # store_listing.price_in_cents # => 10
67
+ #
68
+ # A default can also be provided.
69
+ #
70
+ # # db/schema.rb
71
+ # create_table :store_listings, force: true do |t|
72
+ # t.string :my_string, default: "original default"
73
+ # end
74
+ #
75
+ # StoreListing.new.my_string # => "original default"
76
+ #
77
+ # # app/models/store_listing.rb
78
+ # class StoreListing < DuckRecord::Base
79
+ # attribute :my_string, :string, default: "new default"
80
+ # end
81
+ #
82
+ # StoreListing.new.my_string # => "new default"
83
+ #
84
+ # class Product < DuckRecord::Base
85
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
86
+ # end
87
+ #
88
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
89
+ # sleep 1
90
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
91
+ #
92
+ # \Attributes do not need to be backed by a database column.
93
+ #
94
+ # # app/models/my_model.rb
95
+ # class MyModel < DuckRecord::Base
96
+ # attribute :my_string, :string
97
+ # attribute :my_int_array, :integer, array: true
98
+ # attribute :my_float_range, :float, range: true
99
+ # end
100
+ #
101
+ # model = MyModel.new(
102
+ # my_string: "string",
103
+ # my_int_array: ["1", "2", "3"],
104
+ # my_float_range: "[1,3.5]",
105
+ # )
106
+ # model.attributes
107
+ # # =>
108
+ # {
109
+ # my_string: "string",
110
+ # my_int_array: [1, 2, 3],
111
+ # my_float_range: 1.0..3.5
112
+ # }
113
+ #
114
+ # ==== Creating Custom Types
115
+ #
116
+ # Users may also define their own custom types, as long as they respond
117
+ # to the methods defined on the value type. The method +deserialize+ or
118
+ # +cast+ will be called on your type object, with raw input from the
119
+ # database or from your controllers. See ActiveModel::Type::Value for the
120
+ # expected API. It is recommended that your type objects inherit from an
121
+ # existing type, or from DuckRecord::Type::Value
122
+ #
123
+ # class MoneyType < DuckRecord::Type::Integer
124
+ # def cast(value)
125
+ # if !value.kind_of?(Numeric) && value.include?('$')
126
+ # price_in_dollars = value.gsub(/\$/, '').to_f
127
+ # super(price_in_dollars * 100)
128
+ # else
129
+ # super
130
+ # end
131
+ # end
132
+ # end
133
+ #
134
+ # # config/initializers/types.rb
135
+ # DuckRecord::Type.register(:money, MoneyType)
136
+ #
137
+ # # app/models/store_listing.rb
138
+ # class StoreListing < DuckRecord::Base
139
+ # attribute :price_in_cents, :money
140
+ # end
141
+ #
142
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
143
+ # store_listing.price_in_cents # => 1000
144
+ #
145
+ # For more details on creating custom types, see the documentation for
146
+ # ActiveModel::Type::Value. For more details on registering your types
147
+ # to be referenced by a symbol, see DuckRecord::Type.register. You can
148
+ # also pass a type object directly, in place of a symbol.
149
+ #
150
+ # ==== \Querying
151
+ #
152
+ # When {DuckRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will
153
+ # use the type defined by the model class to convert the value to SQL,
154
+ # calling +serialize+ on your type object. For example:
155
+ #
156
+ # class Money < Struct.new(:amount, :currency)
157
+ # end
158
+ #
159
+ # class MoneyType < Type::Value
160
+ # def initialize(currency_converter:)
161
+ # @currency_converter = currency_converter
162
+ # end
163
+ #
164
+ # # value will be the result of +deserialize+ or
165
+ # # +cast+. Assumed to be an instance of +Money+ in
166
+ # # this case.
167
+ # def serialize(value)
168
+ # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
169
+ # value_in_bitcoins.amount
170
+ # end
171
+ # end
172
+ #
173
+ # # config/initializers/types.rb
174
+ # DuckRecord::Type.register(:money, MoneyType)
175
+ #
176
+ # # app/models/product.rb
177
+ # class Product < DuckRecord::Base
178
+ # currency_converter = ConversionRatesFromTheInternet.new
179
+ # attribute :price_in_bitcoins, :money, currency_converter: currency_converter
180
+ # end
181
+ #
182
+ # Product.where(price_in_bitcoins: Money.new(5, "USD"))
183
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
184
+ #
185
+ # Product.where(price_in_bitcoins: Money.new(5, "GBP"))
186
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
187
+ #
188
+ # ==== Dirty Tracking
189
+ #
190
+ # The type of an attribute is given the opportunity to change how dirty
191
+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
192
+ # will be called from ActiveModel::Dirty. See the documentation for those
193
+ # methods in ActiveModel::Type::Value for more details.
194
+ def attribute(name, cast_type = Type::Value.new, **options)
195
+ name = name.to_s
196
+ reload_schema_from_cache
197
+
198
+ self.attributes_to_define_after_schema_loads =
199
+ attributes_to_define_after_schema_loads.merge(
200
+ name => [cast_type, options]
201
+ )
202
+ end
203
+
204
+ # This is the low level API which sits beneath +attribute+. It only
205
+ # accepts type objects, and will do its work immediately instead of
206
+ # waiting for the schema to load. Automatic schema detection and
207
+ # ClassMethods#attribute both call this under the hood. While this method
208
+ # is provided so it can be used by plugin authors, application code
209
+ # should probably use ClassMethods#attribute.
210
+ #
211
+ # +name+ The name of the attribute being defined. Expected to be a +String+.
212
+ #
213
+ # +cast_type+ The type object to use for this attribute.
214
+ #
215
+ # +default+ The default value to use when no value is provided. If this option
216
+ # is not passed, the previous default value (if any) will be used.
217
+ # Otherwise, the default will be +nil+. A proc can also be passed, and
218
+ # will be called once each time a new value is needed.
219
+ #
220
+ # +user_provided_default+ Whether the default value should be cast using
221
+ # +cast+ or +deserialize+.
222
+ def define_attribute(
223
+ name,
224
+ cast_type,
225
+ default: NO_DEFAULT_PROVIDED
226
+ )
227
+ attribute_types[name] = cast_type
228
+ define_default_attribute(name, default, cast_type)
229
+ end
230
+
231
+ def load_schema! # :nodoc:
232
+ super
233
+ attributes_to_define_after_schema_loads.each do |name, (type, options)|
234
+ if type.is_a?(Symbol)
235
+ type = DuckRecord::Type.lookup(type, **options.except(:default))
236
+ end
237
+
238
+ define_attribute(name, type, **options.slice(:default))
239
+ end
240
+ end
241
+
242
+ private
243
+
244
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
245
+ private_constant :NO_DEFAULT_PROVIDED
246
+
247
+ def define_default_attribute(name, value, type)
248
+ if value == NO_DEFAULT_PROVIDED
249
+ default_attribute = _default_attributes[name].with_type(type)
250
+ else
251
+ default_attribute = Attribute::UserProvidedDefault.new(
252
+ name,
253
+ value,
254
+ type,
255
+ _default_attributes.fetch(name.to_s) { nil },
256
+ )
257
+ end
258
+ _default_attributes[name] = default_attribute
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,300 @@
1
+ require "yaml"
2
+ require "active_support/benchmarkable"
3
+ require "active_support/dependencies"
4
+ require "active_support/descendants_tracker"
5
+ require "active_support/time"
6
+ require "active_support/core_ext/module/attribute_accessors"
7
+ require "active_support/core_ext/array/extract_options"
8
+ require "active_support/core_ext/hash/deep_merge"
9
+ require "active_support/core_ext/hash/slice"
10
+ require "active_support/core_ext/hash/transform_values"
11
+ require "active_support/core_ext/string/behavior"
12
+ require "active_support/core_ext/kernel/singleton_class"
13
+ require "active_support/core_ext/module/introspection"
14
+ require "active_support/core_ext/object/duplicable"
15
+ require "active_support/core_ext/class/subclasses"
16
+ require "duck_record/define_callbacks"
17
+ require "duck_record/errors"
18
+ require "duck_record/attributes"
19
+
20
+ module DuckRecord #:nodoc:
21
+ # = Active Record
22
+ #
23
+ # Active Record objects don't specify their attributes directly, but rather infer them from
24
+ # the table definition with which they're linked. Adding, removing, and changing attributes
25
+ # and their type is done directly in the database. Any change is instantly reflected in the
26
+ # Active Record objects. The mapping that binds a given Active Record class to a certain
27
+ # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
28
+ #
29
+ # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
30
+ #
31
+ # == Creation
32
+ #
33
+ # Active Records accept constructor parameters either in a hash or as a block. The hash
34
+ # method is especially useful when you're receiving the data from somewhere else, like an
35
+ # HTTP request. It works like this:
36
+ #
37
+ # user = User.new(name: 'David', occupation: 'Code Artist')
38
+ # user.name # => 'David'
39
+ #
40
+ # You can also use block initialization:
41
+ #
42
+ # user = User.new do |u|
43
+ # u.name = 'David'
44
+ # u.occupation = 'Code Artist'
45
+ # end
46
+ #
47
+ # And of course you can just create a bare object and specify the attributes after the fact:
48
+ #
49
+ # user = User.new
50
+ # user.name = 'David'
51
+ # user.occupation = 'Code Artist'
52
+ #
53
+ # == Conditions
54
+ #
55
+ # Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
56
+ # The array form is to be used when the condition input is tainted and requires sanitization. The string form can
57
+ # be used for statements that don't involve tainted data. The hash form works much like the array form, except
58
+ # only equality and range is possible. Examples:
59
+ #
60
+ # class User < DuckRecord::Base
61
+ # def self.authenticate_unsafely(user_name, password)
62
+ # where('user_name = '#{user_name}' AND password = '#{password}'').first
63
+ # end
64
+ #
65
+ # def self.authenticate_safely(user_name, password)
66
+ # where('user_name = ? AND password = ?', user_name, password).first
67
+ # end
68
+ #
69
+ # def self.authenticate_safely_simply(user_name, password)
70
+ # where(user_name: user_name, password: password).first
71
+ # end
72
+ # end
73
+ #
74
+ # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
75
+ # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
76
+ # parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
77
+ # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
78
+ # before inserting them in the query, which will ensure that an attacker can't escape the
79
+ # query and fake the login (or worse).
80
+ #
81
+ # When using multiple parameters in the conditions, it can easily become hard to read exactly
82
+ # what the fourth or fifth question mark is supposed to represent. In those cases, you can
83
+ # resort to named bind variables instead. That's done by replacing the question marks with
84
+ # symbols and supplying a hash with values for the matching symbol keys:
85
+ #
86
+ # Company.where(
87
+ # 'id = :id AND name = :name AND division = :division AND created_at > :accounting_date',
88
+ # { id: 3, name: '37signals', division: 'First', accounting_date: '2005-01-01' }
89
+ # ).first
90
+ #
91
+ # Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
92
+ # operator. For instance:
93
+ #
94
+ # Student.where(first_name: 'Harvey', status: 1)
95
+ # Student.where(params[:student])
96
+ #
97
+ # A range may be used in the hash to use the SQL BETWEEN operator:
98
+ #
99
+ # Student.where(grade: 9..12)
100
+ #
101
+ # An array may be used in the hash to use the SQL IN operator:
102
+ #
103
+ # Student.where(grade: [9,11,12])
104
+ #
105
+ # When joining tables, nested hashes or keys written in the form 'table_name.column_name'
106
+ # can be used to qualify the table name of a particular condition. For instance:
107
+ #
108
+ # Student.joins(:schools).where(schools: { category: 'public' })
109
+ # Student.joins(:schools).where('schools.category' => 'public' )
110
+ #
111
+ # == Overwriting default accessors
112
+ #
113
+ # All column values are automatically available through basic accessors on the Active Record
114
+ # object, but sometimes you want to specialize this behavior. This can be done by overwriting
115
+ # the default accessors (using the same name as the attribute) and calling
116
+ # +super+ to actually change things.
117
+ #
118
+ # class Song < DuckRecord::Base
119
+ # # Uses an integer of seconds to hold the length of the song
120
+ #
121
+ # def length=(minutes)
122
+ # super(minutes.to_i * 60)
123
+ # end
124
+ #
125
+ # def length
126
+ # super / 60
127
+ # end
128
+ # end
129
+ #
130
+ # == Attribute query methods
131
+ #
132
+ # In addition to the basic accessors, query methods are also automatically available on the Active Record object.
133
+ # Query methods allow you to test whether an attribute value is present.
134
+ # Additionally, when dealing with numeric values, a query method will return false if the value is zero.
135
+ #
136
+ # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
137
+ # to determine whether the user has a name:
138
+ #
139
+ # user = User.new(name: 'David')
140
+ # user.name? # => true
141
+ #
142
+ # anonymous = User.new(name: '')
143
+ # anonymous.name? # => false
144
+ #
145
+ # == Accessing attributes before they have been typecasted
146
+ #
147
+ # Sometimes you want to be able to read the raw attribute data without having the column-determined
148
+ # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
149
+ # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
150
+ # you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
151
+ #
152
+ # This is especially useful in validation situations where the user might supply a string for an
153
+ # integer field and you want to display the original string back in an error message. Accessing the
154
+ # attribute normally would typecast the string to 0, which isn't what you want.
155
+ #
156
+ # == Dynamic attribute-based finders
157
+ #
158
+ # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
159
+ # by simple queries without turning to SQL. They work by appending the name of an attribute
160
+ # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
161
+ # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
162
+ # <tt>Person.find_by_user_name(user_name)</tt>.
163
+ #
164
+ # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
165
+ # DuckRecord::RecordNotFound error if they do not return any records,
166
+ # like <tt>Person.find_by_last_name!</tt>.
167
+ #
168
+ # It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with
169
+ # '_and_'.
170
+ #
171
+ # Person.find_by(user_name: user_name, password: password)
172
+ # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
173
+ #
174
+ # It's even possible to call these dynamic finder methods on relations and named scopes.
175
+ #
176
+ # Payment.order('created_on').find_by_amount(50)
177
+ #
178
+ # == Saving arrays, hashes, and other non-mappable objects in text columns
179
+ #
180
+ # Active Record can serialize any object in text columns using YAML. To do so, you must
181
+ # specify this with a call to the class method
182
+ # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize].
183
+ # This makes it possible to store arrays, hashes, and other non-mappable objects without doing
184
+ # any additional work.
185
+ #
186
+ # class User < DuckRecord::Base
187
+ # serialize :preferences
188
+ # end
189
+ #
190
+ # user = User.create(preferences: { 'background' => 'black', 'display' => large })
191
+ # User.find(user.id).preferences # => { 'background' => 'black', 'display' => large }
192
+ #
193
+ # You can also specify a class option as the second parameter that'll raise an exception
194
+ # if a serialized object is retrieved as a descendant of a class not in the hierarchy.
195
+ #
196
+ # class User < DuckRecord::Base
197
+ # serialize :preferences, Hash
198
+ # end
199
+ #
200
+ # user = User.create(preferences: %w( one two three ))
201
+ # User.find(user.id).preferences # raises SerializationTypeMismatch
202
+ #
203
+ # When you specify a class option, the default value for that attribute will be a new
204
+ # instance of that class.
205
+ #
206
+ # class User < DuckRecord::Base
207
+ # serialize :preferences, OpenStruct
208
+ # end
209
+ #
210
+ # user = User.new
211
+ # user.preferences.theme_color = 'red'
212
+ #
213
+ #
214
+ # == Single table inheritance
215
+ #
216
+ # Active Record allows inheritance by storing the name of the class in a
217
+ # column that is named 'type' by default. See DuckRecord::Inheritance for
218
+ # more details.
219
+ #
220
+ # == Connection to multiple databases in different models
221
+ #
222
+ # Connections are usually created through
223
+ # {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved
224
+ # by DuckRecord::Base.connection. All classes inheriting from DuckRecord::Base will use this
225
+ # connection. But you can also set a class-specific connection. For example, if Course is an
226
+ # DuckRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
227
+ # and Course and all of its subclasses will use this connection instead.
228
+ #
229
+ # This feature is implemented by keeping a connection pool in DuckRecord::Base that is
230
+ # a hash indexed by the class. If a connection is requested, the
231
+ # {DuckRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method
232
+ # will go up the class-hierarchy until a connection is found in the connection pool.
233
+ #
234
+ # == Exceptions
235
+ #
236
+ # * DuckRecordError - Generic error class and superclass of all other errors raised by Active Record.
237
+ # * AdapterNotSpecified - The configuration hash used in
238
+ # {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
239
+ # didn't include an <tt>:adapter</tt> key.
240
+ # * AdapterNotFound - The <tt>:adapter</tt> key used in
241
+ # {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
242
+ # specified a non-existent adapter
243
+ # (or a bad spelling of an existing one).
244
+ # * AssociationTypeMismatch - The object assigned to the association wasn't of the type
245
+ # specified in the association definition.
246
+ # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
247
+ # {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
248
+ # You can inspect the +attribute+ property of the exception object to determine which attribute
249
+ # triggered the error.
250
+ # * ConnectionNotEstablished - No connection has been established.
251
+ # Use {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
252
+ # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
253
+ # {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
254
+ # The +errors+ property of this exception contains an array of
255
+ # AttributeAssignmentError
256
+ # objects that should be inspected to determine which attributes triggered the errors.
257
+ # * RecordInvalid - raised by {DuckRecord::Base#save!}[rdoc-ref:Persistence#save!] and
258
+ # {DuckRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
259
+ # when the record is invalid.
260
+ # * RecordNotFound - No record responded to the {DuckRecord::Base.find}[rdoc-ref:FinderMethods#find] method.
261
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
262
+ # Some {DuckRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal
263
+ # nothing was found, please check its documentation for further details.
264
+ # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
265
+ # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
266
+ #
267
+ # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
268
+ # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
269
+ # instances in the current object space.
270
+ class Base
271
+ extend ActiveModel::Naming
272
+
273
+ extend ActiveSupport::Benchmarkable
274
+ extend ActiveSupport::DescendantsTracker
275
+
276
+ extend Translation
277
+ extend Enum
278
+
279
+ include Core
280
+ include Persistence
281
+ include ReadonlyAttributes
282
+ include ModelSchema
283
+ include Inheritance
284
+ include AttributeAssignment
285
+ include ActiveModel::Conversion
286
+ include Validations
287
+ include Attributes
288
+ include AttributeDecorators
289
+ include DefineCallbacks
290
+ include AttributeMethods
291
+ include Callbacks
292
+ include Associations
293
+ include NestedValidateAssociation
294
+ include NestedAttributes
295
+ include Reflection
296
+ include Serialization
297
+ end
298
+
299
+ ActiveSupport.run_load_hooks(:duck_record, Base)
300
+ end