tallty_duck_record 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +41 -0
- data/README.md +82 -0
- data/Rakefile +28 -0
- data/lib/core_ext/array_without_blank.rb +46 -0
- data/lib/duck_record.rb +65 -0
- data/lib/duck_record/associations.rb +130 -0
- data/lib/duck_record/associations/association.rb +271 -0
- data/lib/duck_record/associations/belongs_to_association.rb +71 -0
- data/lib/duck_record/associations/builder/association.rb +127 -0
- data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
- data/lib/duck_record/associations/builder/collection_association.rb +45 -0
- data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
- data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
- data/lib/duck_record/associations/builder/has_many.rb +11 -0
- data/lib/duck_record/associations/builder/has_one.rb +20 -0
- data/lib/duck_record/associations/builder/singular_association.rb +33 -0
- data/lib/duck_record/associations/collection_association.rb +476 -0
- data/lib/duck_record/associations/collection_proxy.rb +1160 -0
- data/lib/duck_record/associations/embeds_association.rb +92 -0
- data/lib/duck_record/associations/embeds_many_association.rb +203 -0
- data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
- data/lib/duck_record/associations/embeds_one_association.rb +48 -0
- data/lib/duck_record/associations/foreign_association.rb +11 -0
- data/lib/duck_record/associations/has_many_association.rb +17 -0
- data/lib/duck_record/associations/has_one_association.rb +39 -0
- data/lib/duck_record/associations/singular_association.rb +73 -0
- data/lib/duck_record/attribute.rb +213 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute_assignment.rb +118 -0
- data/lib/duck_record/attribute_decorators.rb +89 -0
- data/lib/duck_record/attribute_methods.rb +325 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +107 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/serialization.rb +66 -0
- data/lib/duck_record/attribute_methods/write.rb +70 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
- data/lib/duck_record/attribute_set.rb +98 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +300 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/coders/json.rb +13 -0
- data/lib/duck_record/coders/yaml_column.rb +48 -0
- data/lib/duck_record/core.rb +262 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/enum.rb +139 -0
- data/lib/duck_record/errors.rb +71 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +46 -0
- data/lib/duck_record/model_schema.rb +71 -0
- data/lib/duck_record/nested_attributes.rb +555 -0
- data/lib/duck_record/nested_validate_association.rb +262 -0
- data/lib/duck_record/persistence.rb +39 -0
- data/lib/duck_record/readonly_attributes.rb +36 -0
- data/lib/duck_record/reflection.rb +650 -0
- data/lib/duck_record/serialization.rb +26 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type.rb +77 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/array_without_blank.rb +36 -0
- data/lib/duck_record/type/date.rb +7 -0
- data/lib/duck_record/type/date_time.rb +7 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/internal/timezone.rb +15 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/time.rb +19 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/validations.rb +67 -0
- data/lib/duck_record/validations/subset.rb +74 -0
- data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- 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,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
|