duck_record 0.0.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +29 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute.rb +221 -0
- data/lib/duck_record/attribute_assignment.rb +91 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +124 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/write.rb +65 -0
- data/lib/duck_record/attribute_methods.rb +332 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
- data/lib/duck_record/attribute_set/builder.rb +124 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attribute_set.rb +99 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +296 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/core.rb +253 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/errors.rb +44 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +48 -0
- data/lib/duck_record/model_schema.rb +64 -0
- data/lib/duck_record/serialization.rb +19 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type/array.rb +36 -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/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/unsigned_integer.rb +15 -0
- data/lib/duck_record/type.rb +66 -0
- data/lib/duck_record/validations.rb +40 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/duck_record.rb +47 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,253 @@
|
|
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
|
+
module ClassMethods
|
11
|
+
def allocate
|
12
|
+
define_attribute_methods
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize_find_by_cache # :nodoc:
|
17
|
+
@find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def inherited(child_class) # :nodoc:
|
21
|
+
# initialize cache at class definition for thread safety
|
22
|
+
child_class.initialize_find_by_cache
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize_generated_modules # :nodoc:
|
27
|
+
generated_association_methods
|
28
|
+
end
|
29
|
+
|
30
|
+
def generated_association_methods
|
31
|
+
@generated_association_methods ||= begin
|
32
|
+
mod = const_set(:GeneratedAssociationMethods, Module.new)
|
33
|
+
private_constant :GeneratedAssociationMethods
|
34
|
+
include mod
|
35
|
+
|
36
|
+
mod
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a string like 'Post(id:integer, title:string, body:text)'
|
41
|
+
def inspect
|
42
|
+
if abstract_class?
|
43
|
+
"#{super}(abstract)"
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
51
|
+
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
|
52
|
+
# In both instances, valid attribute keys are determined by the column names of the associated table --
|
53
|
+
# hence you can't have attributes that aren't part of the table columns.
|
54
|
+
#
|
55
|
+
# ==== Example:
|
56
|
+
# # Instantiates a single new object
|
57
|
+
# User.new(first_name: 'Jamie')
|
58
|
+
def initialize(attributes = nil)
|
59
|
+
self.class.define_attribute_methods
|
60
|
+
@attributes = self.class._default_attributes.deep_dup
|
61
|
+
|
62
|
+
init_internals
|
63
|
+
initialize_internals_callback
|
64
|
+
|
65
|
+
assign_attributes(attributes) if attributes
|
66
|
+
|
67
|
+
yield self if block_given?
|
68
|
+
_run_initialize_callbacks
|
69
|
+
end
|
70
|
+
|
71
|
+
# Initialize an empty model object from +coder+. +coder+ should be
|
72
|
+
# the result of previously encoding an Active Record model, using
|
73
|
+
# #encode_with.
|
74
|
+
#
|
75
|
+
# class Post < DuckRecord::Base
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# old_post = Post.new(title: "hello world")
|
79
|
+
# coder = {}
|
80
|
+
# old_post.encode_with(coder)
|
81
|
+
#
|
82
|
+
# post = Post.allocate
|
83
|
+
# post.init_with(coder)
|
84
|
+
# post.title # => 'hello world'
|
85
|
+
def init_with(coder)
|
86
|
+
coder = LegacyYamlAdapter.convert(self.class, coder)
|
87
|
+
@attributes = self.class.yaml_encoder.decode(coder)
|
88
|
+
|
89
|
+
init_internals
|
90
|
+
|
91
|
+
self.class.define_attribute_methods
|
92
|
+
|
93
|
+
yield self if block_given?
|
94
|
+
|
95
|
+
_run_find_callbacks
|
96
|
+
_run_initialize_callbacks
|
97
|
+
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# :method: clone
|
103
|
+
# Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
|
104
|
+
# That means that modifying attributes of the clone will modify the original, since they will both point to the
|
105
|
+
# same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
|
106
|
+
#
|
107
|
+
# user = User.first
|
108
|
+
# new_user = user.clone
|
109
|
+
# user.name # => "Bob"
|
110
|
+
# new_user.name = "Joe"
|
111
|
+
# user.name # => "Joe"
|
112
|
+
#
|
113
|
+
# user.object_id == new_user.object_id # => false
|
114
|
+
# user.name.object_id == new_user.name.object_id # => true
|
115
|
+
#
|
116
|
+
# user.name.object_id == user.dup.name.object_id # => false
|
117
|
+
|
118
|
+
##
|
119
|
+
# :method: dup
|
120
|
+
# Duped objects have no id assigned and are treated as new records. Note
|
121
|
+
# that this is a "shallow" copy as it copies the object's attributes
|
122
|
+
# only, not its associations. The extent of a "deep" copy is application
|
123
|
+
# specific and is therefore left to the application to implement according
|
124
|
+
# to its need.
|
125
|
+
# The dup method does not preserve the timestamps (created|updated)_(at|on).
|
126
|
+
|
127
|
+
##
|
128
|
+
def initialize_dup(other) # :nodoc:
|
129
|
+
@attributes = @attributes.deep_dup
|
130
|
+
|
131
|
+
_run_initialize_callbacks
|
132
|
+
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
# Populate +coder+ with attributes about this record that should be
|
137
|
+
# serialized. The structure of +coder+ defined in this method is
|
138
|
+
# guaranteed to match the structure of +coder+ passed to the #init_with
|
139
|
+
# method.
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
#
|
143
|
+
# class Post < DuckRecord::Base
|
144
|
+
# end
|
145
|
+
# coder = {}
|
146
|
+
# Post.new.encode_with(coder)
|
147
|
+
# coder # => {"attributes" => {"id" => nil, ... }}
|
148
|
+
def encode_with(coder)
|
149
|
+
self.class.yaml_encoder.encode(@attributes, coder)
|
150
|
+
coder['duck_record_yaml_version'] = 2
|
151
|
+
end
|
152
|
+
|
153
|
+
# Clone and freeze the attributes hash such that associations are still
|
154
|
+
# accessible, even on destroyed records, but cloned models will not be
|
155
|
+
# frozen.
|
156
|
+
def freeze
|
157
|
+
@attributes = @attributes.clone.freeze
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns +true+ if the attributes hash has been frozen.
|
162
|
+
def frozen?
|
163
|
+
@attributes.frozen?
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
|
167
|
+
# attributes will be marked as read only since they cannot be saved.
|
168
|
+
def readonly?
|
169
|
+
@readonly
|
170
|
+
end
|
171
|
+
|
172
|
+
# Marks this record as read only.
|
173
|
+
def readonly!
|
174
|
+
@readonly = true
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns the contents of the record as a nicely formatted string.
|
178
|
+
def inspect
|
179
|
+
# We check defined?(@attributes) not to issue warnings if the object is
|
180
|
+
# allocated but not initialized.
|
181
|
+
inspection = if defined?(@attributes) && @attributes
|
182
|
+
self.class.attribute_names.collect do |name|
|
183
|
+
if has_attribute?(name)
|
184
|
+
"#{name}: #{attribute_for_inspect(name)}"
|
185
|
+
end
|
186
|
+
end.compact.join(', ')
|
187
|
+
else
|
188
|
+
'not initialized'
|
189
|
+
end
|
190
|
+
|
191
|
+
"#<#{self.class} #{inspection}>"
|
192
|
+
end
|
193
|
+
|
194
|
+
# Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
|
195
|
+
# when pp is required.
|
196
|
+
def pretty_print(pp)
|
197
|
+
return super if custom_inspect_method_defined?
|
198
|
+
pp.object_address_group(self) do
|
199
|
+
if defined?(@attributes) && @attributes
|
200
|
+
pp.seplist(self.class.attribute_names, proc { pp.text ',' }) do |attribute_name|
|
201
|
+
attribute_value = read_attribute(attribute_name)
|
202
|
+
pp.breakable " "
|
203
|
+
pp.group(1) do
|
204
|
+
pp.text attribute_name
|
205
|
+
pp.text ":"
|
206
|
+
pp.breakable
|
207
|
+
pp.pp attribute_value
|
208
|
+
end
|
209
|
+
end
|
210
|
+
else
|
211
|
+
pp.breakable " "
|
212
|
+
pp.text "not initialized"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns a hash of the given methods with their names as keys and returned values as values.
|
218
|
+
def slice(*methods)
|
219
|
+
Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
|
225
|
+
# the array, and then rescues from the possible +NoMethodError+. If those elements are
|
226
|
+
# +DuckRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
|
227
|
+
# which significantly impacts upon performance.
|
228
|
+
#
|
229
|
+
# So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
|
230
|
+
#
|
231
|
+
# See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
|
232
|
+
def to_ary
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
|
236
|
+
def init_internals
|
237
|
+
@readonly = false
|
238
|
+
end
|
239
|
+
|
240
|
+
def initialize_internals_callback
|
241
|
+
end
|
242
|
+
|
243
|
+
def thaw
|
244
|
+
if frozen?
|
245
|
+
@attributes = @attributes.dup
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def custom_inspect_method_defined?
|
250
|
+
self.class.instance_method(:inspect).owner != DuckRecord::Base.instance_method(:inspect).owner
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
# This module exists because `DuckRecord::AttributeMethods::Dirty` needs to
|
3
|
+
# define callbacks, but continue to have its version of `save` be the super
|
4
|
+
# method of `DuckRecord::Callbacks`. This will be removed when the removal
|
5
|
+
# of deprecated code removes this need.
|
6
|
+
module DefineCallbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
CALLBACKS = [
|
10
|
+
:after_initialize, :before_validation, :after_validation,
|
11
|
+
]
|
12
|
+
|
13
|
+
module ClassMethods # :nodoc:
|
14
|
+
include ActiveModel::Callbacks
|
15
|
+
end
|
16
|
+
|
17
|
+
included do
|
18
|
+
include ActiveModel::Validations::Callbacks
|
19
|
+
|
20
|
+
define_model_callbacks :initialize, only: :after
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
# = Active Record Errors
|
3
|
+
#
|
4
|
+
# Generic Active Record exception class.
|
5
|
+
class DuckRecordError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Raised on attempt to update record that is instantiated as read only.
|
9
|
+
class ReadOnlyRecord < DuckRecordError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raised when attribute has a name reserved by Active Record (when attribute
|
13
|
+
# has name of one of Active Record instance methods).
|
14
|
+
class DangerousAttributeError < DuckRecordError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when unknown attributes are supplied via mass assignment.
|
18
|
+
UnknownAttributeError = ActiveModel::UnknownAttributeError
|
19
|
+
|
20
|
+
# Raised when an error occurred while doing a mass assignment to an attribute through the
|
21
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
|
22
|
+
# The exception has an +attribute+ property that is the name of the offending attribute.
|
23
|
+
class AttributeAssignmentError < DuckRecordError
|
24
|
+
attr_reader :exception, :attribute
|
25
|
+
|
26
|
+
def initialize(message = nil, exception = nil, attribute = nil)
|
27
|
+
super(message)
|
28
|
+
@exception = exception
|
29
|
+
@attribute = attribute
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Raised when there are multiple errors while doing a mass assignment through the
|
34
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
|
35
|
+
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
|
36
|
+
# objects, each corresponding to the error while assigning to an attribute.
|
37
|
+
class MultiparameterAssignmentErrors < DuckRecordError
|
38
|
+
attr_reader :errors
|
39
|
+
|
40
|
+
def initialize(errors = nil)
|
41
|
+
@errors = errors
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
# == Single table inheritance
|
5
|
+
#
|
6
|
+
# Active Record allows inheritance by storing the name of the class in a column that by
|
7
|
+
# default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
|
8
|
+
# This means that an inheritance looking like this:
|
9
|
+
#
|
10
|
+
# class Company < DuckRecord::Base; end
|
11
|
+
# class Firm < Company; end
|
12
|
+
# class Client < Company; end
|
13
|
+
# class PriorityClient < Client; end
|
14
|
+
#
|
15
|
+
# When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
|
16
|
+
# the companies table with type = "Firm". You can then fetch this row again using
|
17
|
+
# <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
|
18
|
+
#
|
19
|
+
# Be aware that because the type column is an attribute on the record every new
|
20
|
+
# subclass will instantly be marked as dirty and the type column will be included
|
21
|
+
# in the list of changed attributes on the record. This is different from non
|
22
|
+
# Single Table Inheritance(STI) classes:
|
23
|
+
#
|
24
|
+
# Company.new.changed? # => false
|
25
|
+
# Firm.new.changed? # => true
|
26
|
+
# Firm.new.changes # => {"type"=>["","Firm"]}
|
27
|
+
#
|
28
|
+
# If you don't have a type column defined in your table, single-table inheritance won't
|
29
|
+
# be triggered. In that case, it'll work just like normal subclasses with no special magic
|
30
|
+
# for differentiating between them or reloading the right type with find.
|
31
|
+
#
|
32
|
+
# Note, all the attributes for all the cases are kept in the same table. Read more:
|
33
|
+
# http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
|
34
|
+
#
|
35
|
+
module Inheritance
|
36
|
+
extend ActiveSupport::Concern
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
# Determines if one of the attributes passed in is the inheritance column,
|
40
|
+
# and if the inheritance column is attr accessible, it initializes an
|
41
|
+
# instance of the given subclass instead of the base class.
|
42
|
+
def new(*args, &block)
|
43
|
+
if abstract_class? || self == Base
|
44
|
+
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the class descending directly from DuckRecord::Base, or
|
51
|
+
# an abstract class, if any, in the inheritance hierarchy.
|
52
|
+
#
|
53
|
+
# If A extends DuckRecord::Base, A.base_class will return A. If B descends from A
|
54
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
55
|
+
#
|
56
|
+
# If B < A and C < B and if A is an abstract_class then both B.base_class
|
57
|
+
# and C.base_class would return B as the answer since A is an abstract_class.
|
58
|
+
def base_class
|
59
|
+
unless self < Base
|
60
|
+
raise DuckRecordError, "#{name} doesn't belong in a hierarchy descending from DuckRecord"
|
61
|
+
end
|
62
|
+
|
63
|
+
if superclass == Base || superclass.abstract_class?
|
64
|
+
self
|
65
|
+
else
|
66
|
+
superclass.base_class
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
|
71
|
+
# If you are using inheritance with DuckRecord and don't want child classes
|
72
|
+
# to utilize the implied STI table name of the parent class, this will need to be true.
|
73
|
+
# For example, given the following:
|
74
|
+
#
|
75
|
+
# class SuperClass < DuckRecord::Base
|
76
|
+
# self.abstract_class = true
|
77
|
+
# end
|
78
|
+
# class Child < SuperClass
|
79
|
+
# self.table_name = 'the_table_i_really_want'
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
#
|
83
|
+
# <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
|
84
|
+
#
|
85
|
+
attr_accessor :abstract_class
|
86
|
+
|
87
|
+
# Returns whether this class is an abstract class or not.
|
88
|
+
def abstract_class?
|
89
|
+
defined?(@abstract_class) && @abstract_class == true
|
90
|
+
end
|
91
|
+
|
92
|
+
def inherited(subclass)
|
93
|
+
subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
# Returns the class type of the record using the current module as a prefix. So descendants of
|
100
|
+
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
101
|
+
def compute_type(type_name)
|
102
|
+
if type_name.start_with?("::".freeze)
|
103
|
+
# If the type is prefixed with a scope operator then we assume that
|
104
|
+
# the type_name is an absolute reference.
|
105
|
+
ActiveSupport::Dependencies.constantize(type_name)
|
106
|
+
else
|
107
|
+
type_candidate = @_type_candidates_cache[type_name]
|
108
|
+
if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
|
109
|
+
return type_constant
|
110
|
+
end
|
111
|
+
|
112
|
+
# Build a list of candidates to search for
|
113
|
+
candidates = []
|
114
|
+
name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
|
115
|
+
candidates << type_name
|
116
|
+
|
117
|
+
candidates.each do |candidate|
|
118
|
+
constant = ActiveSupport::Dependencies.safe_constantize(candidate)
|
119
|
+
if candidate == constant.to_s
|
120
|
+
@_type_candidates_cache[type_name] = candidate
|
121
|
+
return constant
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|