tallty_duck_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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