activeentity 0.0.1.beta14 → 0.0.1.beta15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +4 -4
  4. data/Rakefile +7 -7
  5. data/lib/active_entity.rb +29 -7
  6. data/lib/active_entity/aggregations.rb +2 -1
  7. data/lib/active_entity/associations.rb +46 -24
  8. data/lib/active_entity/associations/{embedded → embeds}/association.rb +2 -2
  9. data/lib/active_entity/associations/{embedded → embeds}/builder/association.rb +1 -1
  10. data/lib/active_entity/associations/{embedded → embeds}/builder/collection_association.rb +1 -1
  11. data/lib/active_entity/associations/{embedded → embeds}/builder/embedded_in.rb +1 -1
  12. data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_many.rb +1 -1
  13. data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_one.rb +1 -1
  14. data/lib/active_entity/associations/{embedded → embeds}/builder/singular_association.rb +1 -1
  15. data/lib/active_entity/associations/{embedded → embeds}/collection_association.rb +1 -1
  16. data/lib/active_entity/associations/{embedded → embeds}/collection_proxy.rb +2 -2
  17. data/lib/active_entity/associations/{embedded → embeds}/embedded_in_association.rb +1 -1
  18. data/lib/active_entity/associations/{embedded → embeds}/embeds_many_association.rb +1 -1
  19. data/lib/active_entity/associations/{embedded → embeds}/embeds_one_association.rb +2 -1
  20. data/lib/active_entity/associations/{embedded → embeds}/singular_association.rb +1 -1
  21. data/lib/active_entity/attribute_assignment.rb +9 -3
  22. data/lib/active_entity/attribute_methods.rb +12 -11
  23. data/lib/active_entity/attribute_methods/before_type_cast.rb +1 -1
  24. data/lib/active_entity/attribute_methods/dirty.rb +13 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +1 -1
  26. data/lib/active_entity/attribute_methods/query.rb +11 -4
  27. data/lib/active_entity/attribute_methods/read.rb +1 -3
  28. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +2 -0
  29. data/lib/active_entity/attribute_methods/write.rb +4 -6
  30. data/lib/active_entity/attributes.rb +76 -2
  31. data/lib/active_entity/base.rb +3 -12
  32. data/lib/active_entity/core.rb +97 -39
  33. data/lib/active_entity/define_callbacks.rb +4 -0
  34. data/lib/active_entity/enum.rb +30 -4
  35. data/lib/active_entity/errors.rb +0 -11
  36. data/lib/active_entity/gem_version.rb +1 -1
  37. data/lib/active_entity/inheritance.rb +4 -106
  38. data/lib/active_entity/integration.rb +1 -1
  39. data/lib/active_entity/model_schema.rb +0 -12
  40. data/lib/active_entity/nested_attributes.rb +5 -12
  41. data/lib/active_entity/railtie.rb +61 -1
  42. data/lib/active_entity/readonly_attributes.rb +9 -1
  43. data/lib/active_entity/reflection.rb +22 -19
  44. data/lib/active_entity/serialization.rb +9 -6
  45. data/lib/active_entity/store.rb +51 -2
  46. data/lib/active_entity/type.rb +8 -8
  47. data/lib/active_entity/type/registry.rb +5 -5
  48. data/lib/active_entity/{validate_embedded_association.rb → validate_embeds_association.rb} +6 -6
  49. data/lib/active_entity/validations.rb +2 -6
  50. data/lib/active_entity/validations/associated.rb +1 -1
  51. data/lib/active_entity/validations/{uniqueness_in_embedding.rb → uniqueness_in_embeds.rb} +1 -1
  52. data/lib/active_entity/validations/uniqueness_on_active_record.rb +46 -40
  53. metadata +27 -30
  54. data/lib/active_entity/type/decimal_without_scale.rb +0 -15
  55. data/lib/active_entity/type/hash_lookup_type_map.rb +0 -25
  56. data/lib/active_entity/type/type_map.rb +0 -62
  57. data/lib/tasks/active_entity_tasks.rake +0 -6
@@ -16,6 +16,7 @@ module ActiveEntity
16
16
  include Query
17
17
  include PrimaryKey
18
18
  include TimeZoneConversion
19
+ include Dirty
19
20
  include Serialization
20
21
  end
21
22
 
@@ -32,7 +33,8 @@ module ActiveEntity
32
33
  end
33
34
 
34
35
  def initialize_generated_modules # :nodoc:
35
- @generated_attribute_methods = GeneratedAttributeMethods.new
36
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
37
+ private_constant :GeneratedAttributeMethods
36
38
  @attribute_methods_generated = false
37
39
  include @generated_attribute_methods
38
40
 
@@ -111,13 +113,11 @@ module ActiveEntity
111
113
  # A class method is 'dangerous' if it is already (re)defined by Active Entity, but
112
114
  # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
113
115
  def dangerous_class_method?(method_name)
114
- RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
115
- end
116
+ return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s)
116
117
 
117
- def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
118
- if klass.respond_to?(name, true)
119
- if superklass.respond_to?(name, true)
120
- klass.method(name).owner != superklass.method(name).owner
118
+ if Base.respond_to?(method_name, true)
119
+ if Object.respond_to?(method_name, true)
120
+ Base.method(method_name).owner != Object.method(method_name).owner
121
121
  else
122
122
  true
123
123
  end
@@ -126,8 +126,8 @@ module ActiveEntity
126
126
  end
127
127
  end
128
128
 
129
- # Returns an array of column names as strings if it's not an abstract class and
130
- # table exists. Otherwise it returns an empty array.
129
+ # Returns an array of column names as strings if it's not an abstract class.
130
+ # Otherwise it returns an empty array.
131
131
  #
132
132
  # class Person < ActiveEntity::Base
133
133
  # end
@@ -302,6 +302,7 @@ module ActiveEntity
302
302
  end
303
303
 
304
304
  private
305
+
305
306
  def attribute_method?(attr_name)
306
307
  # We check defined? because Syck calls respond_to? before actually calling initialize.
307
308
  defined?(@attributes) && @attributes.key?(attr_name)
@@ -323,8 +324,8 @@ module ActiveEntity
323
324
  end
324
325
  end
325
326
 
326
- def readonly_attribute?(name)
327
- self.class.readonly_attributes.include?(name)
327
+ def pk_attribute?(name)
328
+ name == @primary_key
328
329
  end
329
330
  end
330
331
  end
@@ -65,7 +65,7 @@ module ActiveEntity
65
65
 
66
66
  private
67
67
 
68
- # Handle *_before_type_cast for method_missing.
68
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
69
69
  def attribute_before_type_cast(attribute_name)
70
70
  read_attribute_before_type_cast(attribute_name)
71
71
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/attribute_accessors"
4
+
5
+ module ActiveEntity
6
+ module AttributeMethods
7
+ module Dirty
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveModel::Dirty
11
+ end
12
+ end
13
+ end
@@ -90,7 +90,7 @@ module ActiveEntity
90
90
  #
91
91
  # Project.primary_key # => "foo_id"
92
92
  def primary_key=(value)
93
- @primary_key = value&.to_s
93
+ @primary_key = value && -value.to_s
94
94
  end
95
95
  end
96
96
  end
@@ -16,17 +16,24 @@ module ActiveEntity
16
16
  when true then true
17
17
  when false, nil then false
18
18
  else
19
- if Numeric === value || value !~ /[^0-9]/
20
- !value.to_i.zero?
19
+ if !type_for_attribute(attr_name) { false }
20
+ if Numeric === value || !value.match?(/[^0-9]/)
21
+ !value.to_i.zero?
22
+ else
23
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
24
+ !value.blank?
25
+ end
26
+ elsif value.respond_to?(:zero?)
27
+ !value.zero?
21
28
  else
22
- return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
23
29
  !value.blank?
24
30
  end
25
31
  end
26
32
  end
27
33
 
28
34
  private
29
- # Handle *? for method_missing.
35
+
36
+ # Dispatch target for <tt>*?</tt> attribute methods.
30
37
  def attribute?(attribute_name)
31
38
  query_attribute(attribute_name)
32
39
  end
@@ -27,9 +27,7 @@ module ActiveEntity
27
27
  # to a date object, like Date.new(2004, 12, 12)).
28
28
  def read_attribute(attr_name, &block)
29
29
  name = attr_name.to_s
30
- if self.class.attribute_alias?(name)
31
- name = self.class.attribute_alias(name)
32
- end
30
+ name = self.class.attribute_aliases[name] || name
33
31
 
34
32
  _read_attribute(name, &block)
35
33
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/try"
4
+
3
5
  module ActiveEntity
4
6
  module AttributeMethods
5
7
  module TimeZoneConversion
@@ -31,9 +31,7 @@ module ActiveEntity
31
31
  # turned into +nil+.
32
32
  def write_attribute(attr_name, value)
33
33
  name = attr_name.to_s
34
- if self.class.attribute_alias?(name)
35
- name = self.class.attribute_alias(name)
36
- end
34
+ name = self.class.attribute_aliases[name] || name
37
35
 
38
36
  _write_attribute(name, value)
39
37
  end
@@ -48,13 +46,13 @@ module ActiveEntity
48
46
  end
49
47
 
50
48
  private
49
+
51
50
  def write_attribute_without_type_cast(attr_name, value)
52
- name = attr_name.to_s
53
- @attributes.write_cast_value(name, value)
51
+ @attributes.write_cast_value(attr_name.to_s, value)
54
52
  value
55
53
  end
56
54
 
57
- # Handle *= for method_missing.
55
+ # Dispatch target for <tt>*=</tt> attribute methods.
58
56
  def attribute=(attribute_name, value)
59
57
  _write_attribute(attribute_name, value)
60
58
  end
@@ -12,7 +12,13 @@ module ActiveEntity
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- # Defines an attribute with a type on this model.
15
+ # Defines an attribute with a type on this model. It will override the
16
+ # type of existing attributes if needed. This allows control over how
17
+ # values are converted to and from SQL when assigned to a model. It also
18
+ # changes the behavior of values passed to
19
+ # {ActiveEntity::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
20
+ # your domain objects across much of Active Entity, without having to
21
+ # rely on implementation details or monkey patching.
16
22
  #
17
23
  # +name+ The name of the methods to define attribute methods for, and the
18
24
  # column which this will persist to.
@@ -29,11 +35,66 @@ module ActiveEntity
29
35
  # is not passed, the previous default value (if any) will be used.
30
36
  # Otherwise, the default will be +nil+.
31
37
  #
32
- # +array+ Specifies that the type should be an array (see the
38
+ # +array+ (PostgreSQL only) specifies that the type should be an array (see the
33
39
  # examples below).
34
40
  #
41
+ # +range+ (PostgreSQL only) specifies that the type should be a range (see the
42
+ # examples below).
43
+ #
44
+ # When using a symbol for +cast_type+, extra options are forwarded to the
45
+ # constructor of the type object.
46
+ #
35
47
  # ==== Examples
36
48
  #
49
+ # The type detected by Active Entity can be overridden.
50
+ #
51
+ # # db/schema.rb
52
+ # create_table :store_listings, force: true do |t|
53
+ # t.decimal :price_in_cents
54
+ # end
55
+ #
56
+ # # app/models/store_listing.rb
57
+ # class StoreListing < ActiveEntity::Base
58
+ # end
59
+ #
60
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
61
+ #
62
+ # # before
63
+ # store_listing.price_in_cents # => BigDecimal(10.1)
64
+ #
65
+ # class StoreListing < ActiveEntity::Base
66
+ # attribute :price_in_cents, :integer
67
+ # end
68
+ #
69
+ # # after
70
+ # store_listing.price_in_cents # => 10
71
+ #
72
+ # A default can also be provided.
73
+ #
74
+ # # db/schema.rb
75
+ # create_table :store_listings, force: true do |t|
76
+ # t.string :my_string, default: "original default"
77
+ # end
78
+ #
79
+ # StoreListing.new.my_string # => "original default"
80
+ #
81
+ # # app/models/store_listing.rb
82
+ # class StoreListing < ActiveEntity::Base
83
+ # attribute :my_string, :string, default: "new default"
84
+ # end
85
+ #
86
+ # StoreListing.new.my_string # => "new default"
87
+ #
88
+ # class Product < ActiveEntity::Base
89
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
90
+ # end
91
+ #
92
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
93
+ # sleep 1
94
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
95
+ #
96
+ # \Attributes do not need to be backed by a database column.
97
+ #
37
98
  # # app/models/my_model.rb
38
99
  # class MyModel < ActiveEntity::Base
39
100
  # attribute :my_string, :string
@@ -54,6 +115,16 @@ module ActiveEntity
54
115
  # my_float_range: 1.0..3.5
55
116
  # }
56
117
  #
118
+ # Passing options to the type constructor
119
+ #
120
+ # # app/models/my_model.rb
121
+ # class MyModel < ActiveEntity::Base
122
+ # attribute :small_int, :integer, limit: 2
123
+ # end
124
+ #
125
+ # MyModel.create(small_int: 65537)
126
+ # # => Error: 65537 is out of range for the limit of two bytes
127
+ #
57
128
  # ==== Creating Custom Types
58
129
  #
59
130
  # Users may also define their own custom types, as long as they respond
@@ -121,6 +192,8 @@ module ActiveEntity
121
192
  # is not passed, the previous default value (if any) will be used.
122
193
  # Otherwise, the default will be +nil+. A proc can also be passed, and
123
194
  # will be called once each time a new value is needed.
195
+ #
196
+ # +cast+ or +deserialize+.
124
197
  def define_attribute(
125
198
  name,
126
199
  cast_type,
@@ -158,6 +231,7 @@ module ActiveEntity
158
231
  _default_attributes.fetch(name.to_s) { nil },
159
232
  )
160
233
  end
234
+
161
235
  _default_attributes[name] = default_attribute
162
236
  end
163
237
  end
@@ -1,22 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
3
  require "active_support/benchmarkable"
5
4
  require "active_support/dependencies"
6
5
  require "active_support/descendants_tracker"
7
6
  require "active_support/time"
8
- require "active_support/core_ext/module/attribute_accessors"
9
- require "active_support/core_ext/array/extract_options"
10
- require "active_support/core_ext/hash/deep_merge"
11
- require "active_support/core_ext/hash/slice"
12
- require "active_support/core_ext/string/behavior"
13
- require "active_support/core_ext/kernel/singleton_class"
14
- require "active_support/core_ext/module/introspection"
15
- require "active_support/core_ext/object/duplicable"
16
7
  require "active_support/core_ext/class/subclasses"
17
8
  require "active_entity/attribute_decorators"
18
9
  require "active_entity/define_callbacks"
19
- require "active_entity/errors"
20
10
  require "active_entity/attributes"
21
11
 
22
12
  module ActiveEntity #:nodoc:
@@ -28,7 +18,7 @@ module ActiveEntity #:nodoc:
28
18
  # Active Entity objects. The mapping that binds a given Active Entity class to a certain
29
19
  # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
30
20
  #
31
- # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
21
+ # See the mapping rules in table_name and the full example in link:files/activeentity/README_rdoc.html for more insight.
32
22
  #
33
23
  # == Creation
34
24
  #
@@ -292,8 +282,9 @@ module ActiveEntity #:nodoc:
292
282
  include AttributeDecorators
293
283
  include DefineCallbacks
294
284
  include AttributeMethods
285
+ include Callbacks
295
286
  include Associations
296
- include ValidateEmbeddedAssociation
287
+ include ValidateEmbedsAssociation
297
288
  include NestedAttributes
298
289
  include Reflection
299
290
  include Serialization
@@ -58,11 +58,9 @@ module ActiveEntity
58
58
  def inspect # :nodoc:
59
59
  if self == Base
60
60
  super
61
- elsif abstract_class?
62
- "#{super}(abstract)"
63
61
  else
64
62
  attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
65
- "#{super}(#{attr_list})"
63
+ "#{super}#{ "(abstract)" if abstract_class? }(#{attr_list})"
66
64
  end
67
65
  end
68
66
 
@@ -81,7 +79,6 @@ module ActiveEntity
81
79
  # # Instantiates a single new object
82
80
  # User.new(first_name: 'Jamie')
83
81
  def initialize(attributes = nil)
84
- self.class.define_attribute_methods
85
82
  @attributes = self.class._default_attributes.deep_dup
86
83
 
87
84
  init_internals
@@ -95,6 +92,42 @@ module ActiveEntity
95
92
  enable_attr_readonly!
96
93
  end
97
94
 
95
+ # Initialize an empty model object from +coder+. +coder+ should be
96
+ # the result of previously encoding an Active Entity model, using
97
+ # #encode_with.
98
+ #
99
+ # class Post < ActiveEntity::Base
100
+ # end
101
+ #
102
+ # old_post = Post.new(title: "hello world")
103
+ # coder = {}
104
+ # old_post.encode_with(coder)
105
+ #
106
+ # post = Post.allocate
107
+ # post.init_with(coder)
108
+ # post.title # => 'hello world'
109
+ def init_with(coder, &block)
110
+ attributes = self.class.yaml_encoder.decode(coder)
111
+ init_with_attributes(attributes, coder["new_record"], &block)
112
+ end
113
+
114
+ ##
115
+ # Initialize an empty model object from +attributes+.
116
+ # +attributes+ should be an attributes object, and unlike the
117
+ # `initialize` method, no assignment calls are made per attribute.
118
+ def init_with_attributes(attributes) # :nodoc:
119
+ @attributes = attributes
120
+
121
+ init_internals
122
+
123
+ yield self if block_given?
124
+
125
+ _run_find_callbacks
126
+ _run_initialize_callbacks
127
+
128
+ self
129
+ end
130
+
98
131
  ##
99
132
  # :method: clone
100
133
  # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
@@ -147,6 +180,33 @@ module ActiveEntity
147
180
  coder["active_entity_yaml_version"] = 2
148
181
  end
149
182
 
183
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
184
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
185
+ #
186
+ # Note that new records are different from any other record by definition, unless the
187
+ # other record is the receiver itself. Besides, if you fetch existing records with
188
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
189
+ #
190
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
191
+ # models are still comparable.
192
+ def ==(comparison_object)
193
+ super ||
194
+ comparison_object.instance_of?(self.class) &&
195
+ !id.nil? &&
196
+ comparison_object.id == id
197
+ end
198
+ alias :eql? :==
199
+
200
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
201
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
202
+ def hash
203
+ if id
204
+ self.class.hash ^ id.hash
205
+ else
206
+ super
207
+ end
208
+ end
209
+
150
210
  # Clone and freeze the attributes hash such that associations are still
151
211
  # accessible, even on destroyed records, but cloned models will not be
152
212
  # frozen.
@@ -169,6 +229,14 @@ module ActiveEntity
169
229
  end
170
230
  end
171
231
 
232
+ def present? # :nodoc:
233
+ true
234
+ end
235
+
236
+ def blank? # :nodoc:
237
+ false
238
+ end
239
+
172
240
  # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
173
241
  # attributes will be marked as read only since they cannot be saved.
174
242
  def readonly?
@@ -184,24 +252,22 @@ module ActiveEntity
184
252
  def inspect
185
253
  # We check defined?(@attributes) not to issue warnings if the object is
186
254
  # allocated but not initialized.
187
- inspection =
188
- if defined?(@attributes) && @attributes
189
- self.class.attribute_names.collect do |name|
190
- if has_attribute?(name)
191
- attr = _read_attribute(name)
192
- value =
193
- if attr.nil?
194
- attr.inspect
195
- else
196
- attr = format_for_inspect(attr)
197
- inspection_filter.filter_param(name, attr)
198
- end
199
- "#{name}: #{value}"
255
+ inspection = if defined?(@attributes) && @attributes
256
+ self.class.attribute_names.collect do |name|
257
+ if has_attribute?(name)
258
+ attr = _read_attribute(name)
259
+ value = if attr.nil?
260
+ attr.inspect
261
+ else
262
+ attr = format_for_inspect(attr)
263
+ inspection_filter.filter_param(name, attr)
200
264
  end
201
- end.compact.join(", ")
202
- else
203
- "not initialized"
204
- end
265
+ "#{name}: #{value}"
266
+ end
267
+ end.compact.join(", ")
268
+ else
269
+ "not initialized"
270
+ end
205
271
 
206
272
  "#<#{self.class} #{inspection}>"
207
273
  end
@@ -236,14 +302,6 @@ module ActiveEntity
236
302
  Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
237
303
  end
238
304
 
239
- def present? # :nodoc:
240
- true
241
- end
242
-
243
- def blank? # :nodoc:
244
- false
245
- end
246
-
247
305
  private
248
306
 
249
307
  # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
@@ -261,27 +319,27 @@ module ActiveEntity
261
319
  def init_internals
262
320
  @readonly = false
263
321
  @marked_for_destruction = false
264
- end
265
322
 
266
- def initialize_internals_callback
323
+ self.class.define_attribute_methods
267
324
  end
268
325
 
269
- def thaw
270
- if frozen?
271
- @attributes = @attributes.dup
272
- end
326
+ def initialize_internals_callback
273
327
  end
274
328
 
275
329
  def custom_inspect_method_defined?
276
330
  self.class.instance_method(:inspect).owner != ActiveEntity::Base.instance_method(:inspect).owner
277
331
  end
278
332
 
333
+ class InspectionMask < DelegateClass(::String)
334
+ def pretty_print(pp)
335
+ pp.text __getobj__
336
+ end
337
+ end
338
+ private_constant :InspectionMask
339
+
279
340
  def inspection_filter
280
341
  @inspection_filter ||= begin
281
- mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
282
- def mask.pretty_print(pp)
283
- pp.text __getobj__
284
- end
342
+ mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
285
343
  ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
286
344
  end
287
345
  end