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,324 @@
1
+ module DuckRecord
2
+ # = Active Record \Callbacks
3
+ #
4
+ # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
5
+ # before or after an alteration of the object state. This can be used to make sure that associated and
6
+ # dependent objects are deleted when {DuckRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or
7
+ # to massage attributes before they're validated (by overwriting +before_validation+).
8
+ # As an example of the callbacks initiated, consider the {DuckRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record:
9
+ #
10
+ # * (-) <tt>save</tt>
11
+ # * (-) <tt>valid</tt>
12
+ # * (1) <tt>before_validation</tt>
13
+ # * (-) <tt>validate</tt>
14
+ # * (2) <tt>after_validation</tt>
15
+ # * (3) <tt>before_save</tt>
16
+ # * (4) <tt>before_create</tt>
17
+ # * (-) <tt>create</tt>
18
+ # * (5) <tt>after_create</tt>
19
+ # * (6) <tt>after_save</tt>
20
+ # * (7) <tt>after_commit</tt>
21
+ #
22
+ # Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
23
+ # Check out DuckRecord::Transactions for more details about <tt>after_commit</tt> and
24
+ # <tt>after_rollback</tt>.
25
+ #
26
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
27
+ # object is touched.
28
+ #
29
+ # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
30
+ # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
31
+ # are instantiated as well.
32
+ #
33
+ # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
34
+ # Active Record life cycle. The sequence for calling {DuckRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar,
35
+ # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
36
+ #
37
+ # Examples:
38
+ # class CreditCard < DuckRecord::Base
39
+ # # Strip everything but digits, so the user can specify "555 234 34" or
40
+ # # "5552-3434" and both will mean "55523434"
41
+ # before_validation(on: :create) do
42
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
43
+ # end
44
+ # end
45
+ #
46
+ # class Subscription < DuckRecord::Base
47
+ # before_create :record_signup
48
+ #
49
+ # private
50
+ # def record_signup
51
+ # self.signed_up_on = Date.today
52
+ # end
53
+ # end
54
+ #
55
+ # class Firm < DuckRecord::Base
56
+ # # Disables access to the system, for associated clients and people when the firm is destroyed
57
+ # before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
58
+ # before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
59
+ # end
60
+ #
61
+ # == Inheritable callback queues
62
+ #
63
+ # Besides the overwritable callback methods, it's also possible to register callbacks through the
64
+ # use of the callback macros. Their main advantage is that the macros add behavior into a callback
65
+ # queue that is kept intact down through an inheritance hierarchy.
66
+ #
67
+ # class Topic < DuckRecord::Base
68
+ # before_destroy :destroy_author
69
+ # end
70
+ #
71
+ # class Reply < Topic
72
+ # before_destroy :destroy_readers
73
+ # end
74
+ #
75
+ # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
76
+ # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
77
+ # where the +before_destroy+ method is overridden:
78
+ #
79
+ # class Topic < DuckRecord::Base
80
+ # def before_destroy() destroy_author end
81
+ # end
82
+ #
83
+ # class Reply < Topic
84
+ # def before_destroy() destroy_readers end
85
+ # end
86
+ #
87
+ # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
88
+ # So, use the callback macros when you want to ensure that a certain callback is called for the entire
89
+ # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
90
+ # to decide whether they want to call +super+ and trigger the inherited callbacks.
91
+ #
92
+ # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
93
+ # callbacks before specifying the associations. Otherwise, you might trigger the loading of a
94
+ # child before the parent has registered the callbacks and they won't be inherited.
95
+ #
96
+ # == Types of callbacks
97
+ #
98
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
99
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
100
+ # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
101
+ # creating mix-ins), and inline eval methods are deprecated.
102
+ #
103
+ # The method reference callbacks work by specifying a protected or private method available in the object, like this:
104
+ #
105
+ # class Topic < DuckRecord::Base
106
+ # before_destroy :delete_parents
107
+ #
108
+ # private
109
+ # def delete_parents
110
+ # self.class.delete_all "parent_id = #{id}"
111
+ # end
112
+ # end
113
+ #
114
+ # The callback objects have methods named after the callback called with the record as the only parameter, such as:
115
+ #
116
+ # class BankAccount < DuckRecord::Base
117
+ # before_save EncryptionWrapper.new
118
+ # after_save EncryptionWrapper.new
119
+ # after_initialize EncryptionWrapper.new
120
+ # end
121
+ #
122
+ # class EncryptionWrapper
123
+ # def before_save(record)
124
+ # record.credit_card_number = encrypt(record.credit_card_number)
125
+ # end
126
+ #
127
+ # def after_save(record)
128
+ # record.credit_card_number = decrypt(record.credit_card_number)
129
+ # end
130
+ #
131
+ # alias_method :after_initialize, :after_save
132
+ #
133
+ # private
134
+ # def encrypt(value)
135
+ # # Secrecy is committed
136
+ # end
137
+ #
138
+ # def decrypt(value)
139
+ # # Secrecy is unveiled
140
+ # end
141
+ # end
142
+ #
143
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
144
+ # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
145
+ # initialization data such as the name of the attribute to work with:
146
+ #
147
+ # class BankAccount < DuckRecord::Base
148
+ # before_save EncryptionWrapper.new("credit_card_number")
149
+ # after_save EncryptionWrapper.new("credit_card_number")
150
+ # after_initialize EncryptionWrapper.new("credit_card_number")
151
+ # end
152
+ #
153
+ # class EncryptionWrapper
154
+ # def initialize(attribute)
155
+ # @attribute = attribute
156
+ # end
157
+ #
158
+ # def before_save(record)
159
+ # record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
160
+ # end
161
+ #
162
+ # def after_save(record)
163
+ # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
164
+ # end
165
+ #
166
+ # alias_method :after_initialize, :after_save
167
+ #
168
+ # private
169
+ # def encrypt(value)
170
+ # # Secrecy is committed
171
+ # end
172
+ #
173
+ # def decrypt(value)
174
+ # # Secrecy is unveiled
175
+ # end
176
+ # end
177
+ #
178
+ # == <tt>before_validation*</tt> returning statements
179
+ #
180
+ # If the +before_validation+ callback throws +:abort+, the process will be
181
+ # aborted and {DuckRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
182
+ # If {DuckRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an DuckRecord::RecordInvalid exception.
183
+ # Nothing will be appended to the errors object.
184
+ #
185
+ # == Canceling callbacks
186
+ #
187
+ # If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and
188
+ # the associated action are cancelled.
189
+ # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
190
+ # methods on the model, which are called last.
191
+ #
192
+ # == Ordering callbacks
193
+ #
194
+ # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
195
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the
196
+ # <tt>dependent: :destroy</tt> option.
197
+ #
198
+ # Let's look at the code below:
199
+ #
200
+ # class Topic < DuckRecord::Base
201
+ # has_many :children, dependent: :destroy
202
+ #
203
+ # before_destroy :log_children
204
+ #
205
+ # private
206
+ # def log_children
207
+ # # Child processing
208
+ # end
209
+ # end
210
+ #
211
+ # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
212
+ # because the {DuckRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first.
213
+ # You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
214
+ #
215
+ # class Topic < DuckRecord::Base
216
+ # has_many :children, dependent: :destroy
217
+ #
218
+ # before_destroy :log_children, prepend: true
219
+ #
220
+ # private
221
+ # def log_children
222
+ # # Child processing
223
+ # end
224
+ # end
225
+ #
226
+ # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available.
227
+ #
228
+ # Also, there are cases when you want several callbacks of the same type to
229
+ # be executed in order.
230
+ #
231
+ # For example:
232
+ #
233
+ # class Topic
234
+ # has_many :children
235
+ #
236
+ # after_save :log_children
237
+ # after_save :do_something_else
238
+ #
239
+ # private
240
+ #
241
+ # def log_chidren
242
+ # # Child processing
243
+ # end
244
+ #
245
+ # def do_something_else
246
+ # # Something else
247
+ # end
248
+ # end
249
+ #
250
+ # In this case the +log_children+ gets executed before +do_something_else+.
251
+ # The same applies to all non-transactional callbacks.
252
+ #
253
+ # In case there are multiple transactional callbacks as seen below, the order
254
+ # is reversed.
255
+ #
256
+ # For example:
257
+ #
258
+ # class Topic
259
+ # has_many :children
260
+ #
261
+ # after_commit :log_children
262
+ # after_commit :do_something_else
263
+ #
264
+ # private
265
+ #
266
+ # def log_chidren
267
+ # # Child processing
268
+ # end
269
+ #
270
+ # def do_something_else
271
+ # # Something else
272
+ # end
273
+ # end
274
+ #
275
+ # In this case the +do_something_else+ gets executed before +log_children+.
276
+ #
277
+ # == \Transactions
278
+ #
279
+ # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!],
280
+ # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks.
281
+ # If everything goes fine a COMMIT is executed once the chain has been completed.
282
+ #
283
+ # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
284
+ # can also trigger a ROLLBACK raising an exception in any of the callbacks,
285
+ # including <tt>after_*</tt> hooks. Note, however, that in that case the client
286
+ # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception
287
+ # instead of quietly returning +false+.
288
+ #
289
+ # == Debugging callbacks
290
+ #
291
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support
292
+ # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
293
+ # defines what part of the chain the callback runs in.
294
+ #
295
+ # To find all callbacks in the before_save callback chain:
296
+ #
297
+ # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
298
+ #
299
+ # Returns an array of callback objects that form the before_save chain.
300
+ #
301
+ # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
302
+ #
303
+ # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
304
+ #
305
+ # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
306
+ #
307
+ module Callbacks
308
+ extend ActiveSupport::Concern
309
+
310
+ CALLBACKS = [
311
+ :after_initialize, :before_validation, :after_validation
312
+ ]
313
+
314
+ module ClassMethods # :nodoc:
315
+ include ActiveModel::Callbacks
316
+ end
317
+
318
+ included do
319
+ include ActiveModel::Validations::Callbacks
320
+
321
+ define_model_callbacks :initialize, only: :after
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,13 @@
1
+ module DuckRecord
2
+ module Coders # :nodoc:
3
+ class JSON # :nodoc:
4
+ def self.dump(obj)
5
+ ActiveSupport::JSON.encode(obj)
6
+ end
7
+
8
+ def self.load(json)
9
+ ActiveSupport::JSON.decode(json) unless json.blank?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ require "yaml"
2
+
3
+ module DuckRecord
4
+ module Coders # :nodoc:
5
+ class YAMLColumn # :nodoc:
6
+ attr_accessor :object_class
7
+
8
+ def initialize(attr_name, object_class = Object)
9
+ @attr_name = attr_name
10
+ @object_class = object_class
11
+ check_arity_of_constructor
12
+ end
13
+
14
+ def dump(obj)
15
+ return if obj.nil?
16
+
17
+ assert_valid_value(obj, action: "dump")
18
+ YAML.dump obj
19
+ end
20
+
21
+ def load(yaml)
22
+ return object_class.new if object_class != Object && yaml.nil?
23
+ return yaml unless yaml.is_a?(String) && /^---/.match?(yaml)
24
+ obj = YAML.load(yaml)
25
+
26
+ assert_valid_value(obj, action: "load")
27
+ obj ||= object_class.new if object_class != Object
28
+
29
+ obj
30
+ end
31
+
32
+ def assert_valid_value(obj, action:)
33
+ unless obj.nil? || obj.is_a?(object_class)
34
+ raise SerializationTypeMismatch,
35
+ "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def check_arity_of_constructor
42
+ load(nil)
43
+ rescue ArgumentError
44
+ raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,262 @@
1
+ require "thread"
2
+ require "active_support/core_ext/hash/indifferent_access"
3
+ require "active_support/core_ext/object/duplicable"
4
+ require "active_support/core_ext/string/filters"
5
+
6
+ module DuckRecord
7
+ module Core
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ ##
12
+ # :singleton-method:
13
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
14
+ # dates and times from the database. This is set to :utc by default.
15
+ mattr_accessor :default_timezone, instance_writer: false
16
+ self.default_timezone = :utc
17
+ end
18
+
19
+ module ClassMethods
20
+ def allocate
21
+ define_attribute_methods
22
+ super
23
+ end
24
+
25
+ def inherited(child_class) # :nodoc:
26
+ super
27
+ end
28
+
29
+ def initialize_generated_modules # :nodoc:
30
+ generated_association_methods
31
+ end
32
+
33
+ def generated_association_methods
34
+ @generated_association_methods ||= begin
35
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
36
+ private_constant :GeneratedAssociationMethods
37
+ include mod
38
+
39
+ mod
40
+ end
41
+ end
42
+
43
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
44
+ def inspect
45
+ if abstract_class?
46
+ "#{super}(abstract)"
47
+ else
48
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
49
+ "#{super}(#{attr_list})"
50
+ end
51
+ end
52
+ end
53
+
54
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
55
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
56
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
57
+ # hence you can't have attributes that aren't part of the table columns.
58
+ #
59
+ # ==== Example:
60
+ # # Instantiates a single new object
61
+ # User.new(first_name: 'Jamie')
62
+ def initialize(attributes = nil)
63
+ self.class.define_attribute_methods
64
+ @attributes = self.class._default_attributes.deep_dup
65
+
66
+ disable_attr_readonly!
67
+
68
+ init_internals
69
+ initialize_internals_callback
70
+
71
+ if attributes
72
+ assign_attributes(attributes)
73
+ clear_changes_information
74
+ end
75
+
76
+ yield self if block_given?
77
+ _run_initialize_callbacks
78
+
79
+ enable_attr_readonly!
80
+ end
81
+
82
+ # Initialize an empty model object from +coder+. +coder+ should be
83
+ # the result of previously encoding an Active Record model, using
84
+ # #encode_with.
85
+ #
86
+ # class Post < DuckRecord::Base
87
+ # end
88
+ #
89
+ # old_post = Post.new(title: "hello world")
90
+ # coder = {}
91
+ # old_post.encode_with(coder)
92
+ #
93
+ # post = Post.allocate
94
+ # post.init_with(coder)
95
+ # post.title # => 'hello world'
96
+ def init_with(coder)
97
+ @attributes = self.class.yaml_encoder.decode(coder)
98
+
99
+ init_internals
100
+
101
+ self.class.define_attribute_methods
102
+
103
+ yield self if block_given?
104
+
105
+ _run_initialize_callbacks
106
+
107
+ self
108
+ end
109
+
110
+ ##
111
+ # :method: clone
112
+ # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
113
+ # That means that modifying attributes of the clone will modify the original, since they will both point to the
114
+ # same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
115
+ #
116
+ # user = User.first
117
+ # new_user = user.clone
118
+ # user.name # => "Bob"
119
+ # new_user.name = "Joe"
120
+ # user.name # => "Joe"
121
+ #
122
+ # user.object_id == new_user.object_id # => false
123
+ # user.name.object_id == new_user.name.object_id # => true
124
+ #
125
+ # user.name.object_id == user.dup.name.object_id # => false
126
+
127
+ ##
128
+ # :method: dup
129
+ # Duped objects have no id assigned and are treated as new records. Note
130
+ # that this is a "shallow" copy as it copies the object's attributes
131
+ # only, not its associations. The extent of a "deep" copy is application
132
+ # specific and is therefore left to the application to implement according
133
+ # to its need.
134
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
135
+
136
+ ##
137
+ def initialize_dup(other) # :nodoc:
138
+ @attributes = @attributes.deep_dup
139
+
140
+ _run_initialize_callbacks
141
+
142
+ super
143
+ end
144
+
145
+ # Populate +coder+ with attributes about this record that should be
146
+ # serialized. The structure of +coder+ defined in this method is
147
+ # guaranteed to match the structure of +coder+ passed to the #init_with
148
+ # method.
149
+ #
150
+ # Example:
151
+ #
152
+ # class Post < DuckRecord::Base
153
+ # end
154
+ # coder = {}
155
+ # Post.new.encode_with(coder)
156
+ # coder # => {"attributes" => {"id" => nil, ... }}
157
+ def encode_with(coder)
158
+ self.class.yaml_encoder.encode(@attributes, coder)
159
+ coder["duck_record_yaml_version"] = 2
160
+ end
161
+
162
+ # Clone and freeze the attributes hash such that associations are still
163
+ # accessible, even on destroyed records, but cloned models will not be
164
+ # frozen.
165
+ def freeze
166
+ @attributes = @attributes.clone.freeze
167
+ self
168
+ end
169
+
170
+ # Returns +true+ if the attributes hash has been frozen.
171
+ def frozen?
172
+ @attributes.frozen?
173
+ end
174
+
175
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
176
+ # attributes will be marked as read only since they cannot be saved.
177
+ def readonly?
178
+ @readonly
179
+ end
180
+
181
+ # Marks this record as read only.
182
+ def readonly!
183
+ @readonly = true
184
+ end
185
+
186
+ # Returns the contents of the record as a nicely formatted string.
187
+ def inspect
188
+ # We check defined?(@attributes) not to issue warnings if the object is
189
+ # allocated but not initialized.
190
+ inspection = if defined?(@attributes) && @attributes
191
+ self.class.attribute_names.collect do |name|
192
+ if has_attribute?(name)
193
+ "#{name}: #{attribute_for_inspect(name)}"
194
+ end
195
+ end.compact.join(", ")
196
+ else
197
+ "not initialized"
198
+ end
199
+
200
+ "#<#{self.class} #{inspection}>"
201
+ end
202
+
203
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
204
+ # when pp is required.
205
+ def pretty_print(pp)
206
+ return super if custom_inspect_method_defined?
207
+ pp.object_address_group(self) do
208
+ if defined?(@attributes) && @attributes
209
+ pp.seplist(self.class.attribute_names, proc { pp.text "," }) do |attribute_name|
210
+ attribute_value = read_attribute(attribute_name)
211
+ pp.breakable " "
212
+ pp.group(1) do
213
+ pp.text attribute_name
214
+ pp.text ":"
215
+ pp.breakable
216
+ pp.pp attribute_value
217
+ end
218
+ end
219
+ else
220
+ pp.breakable " "
221
+ pp.text "not initialized"
222
+ end
223
+ end
224
+ end
225
+
226
+ # Returns a hash of the given methods with their names as keys and returned values as values.
227
+ def slice(*methods)
228
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
229
+ end
230
+
231
+ private
232
+
233
+ # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
234
+ # the array, and then rescues from the possible +NoMethodError+. If those elements are
235
+ # +DuckRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
236
+ # which significantly impacts upon performance.
237
+ #
238
+ # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
239
+ #
240
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
241
+ def to_ary
242
+ nil
243
+ end
244
+
245
+ def init_internals
246
+ @readonly = false
247
+ end
248
+
249
+ def initialize_internals_callback
250
+ end
251
+
252
+ def thaw
253
+ if frozen?
254
+ @attributes = @attributes.dup
255
+ end
256
+ end
257
+
258
+ def custom_inspect_method_defined?
259
+ self.class.instance_method(:inspect).owner != DuckRecord::Base.instance_method(:inspect).owner
260
+ end
261
+ end
262
+ end