duck_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +29 -0
  5. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  6. data/lib/duck_record/attribute.rb +221 -0
  7. data/lib/duck_record/attribute_assignment.rb +91 -0
  8. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  9. data/lib/duck_record/attribute_methods/dirty.rb +124 -0
  10. data/lib/duck_record/attribute_methods/read.rb +78 -0
  11. data/lib/duck_record/attribute_methods/write.rb +65 -0
  12. data/lib/duck_record/attribute_methods.rb +332 -0
  13. data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
  14. data/lib/duck_record/attribute_set/builder.rb +124 -0
  15. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  16. data/lib/duck_record/attribute_set.rb +99 -0
  17. data/lib/duck_record/attributes.rb +262 -0
  18. data/lib/duck_record/base.rb +296 -0
  19. data/lib/duck_record/callbacks.rb +324 -0
  20. data/lib/duck_record/core.rb +253 -0
  21. data/lib/duck_record/define_callbacks.rb +23 -0
  22. data/lib/duck_record/errors.rb +44 -0
  23. data/lib/duck_record/inheritance.rb +130 -0
  24. data/lib/duck_record/locale/en.yml +48 -0
  25. data/lib/duck_record/model_schema.rb +64 -0
  26. data/lib/duck_record/serialization.rb +19 -0
  27. data/lib/duck_record/translation.rb +22 -0
  28. data/lib/duck_record/type/array.rb +36 -0
  29. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  30. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  31. data/lib/duck_record/type/json.rb +6 -0
  32. data/lib/duck_record/type/registry.rb +97 -0
  33. data/lib/duck_record/type/serialized.rb +63 -0
  34. data/lib/duck_record/type/text.rb +9 -0
  35. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  36. data/lib/duck_record/type.rb +66 -0
  37. data/lib/duck_record/validations.rb +40 -0
  38. data/lib/duck_record/version.rb +3 -0
  39. data/lib/duck_record.rb +47 -0
  40. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  41. 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