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