activeentity 0.0.1.beta14 → 6.1.0

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 +24 -7
  4. data/Rakefile +7 -7
  5. data/lib/active_entity.rb +30 -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 +10 -8
  22. data/lib/active_entity/attribute_methods.rb +52 -61
  23. data/lib/active_entity/attribute_methods/before_type_cast.rb +6 -6
  24. data/lib/active_entity/attribute_methods/dirty.rb +13 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +6 -8
  26. data/lib/active_entity/attribute_methods/query.rb +11 -8
  27. data/lib/active_entity/attribute_methods/read.rb +10 -13
  28. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +2 -0
  29. data/lib/active_entity/attribute_methods/write.rb +16 -25
  30. data/lib/active_entity/attributes.rb +76 -2
  31. data/lib/active_entity/base.rb +3 -14
  32. data/lib/active_entity/{define_callbacks.rb → callbacks.rb} +5 -1
  33. data/lib/active_entity/core.rb +97 -39
  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 +4 -4
  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 +42 -47
  53. metadata +33 -36
  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
@@ -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,11 @@
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
- require "active_entity/define_callbacks"
19
- require "active_entity/errors"
20
9
  require "active_entity/attributes"
21
10
 
22
11
  module ActiveEntity #:nodoc:
@@ -28,7 +17,7 @@ module ActiveEntity #:nodoc:
28
17
  # Active Entity objects. The mapping that binds a given Active Entity class to a certain
29
18
  # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
30
19
  #
31
- # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
20
+ # See the mapping rules in table_name and the full example in link:files/activeentity/README_rdoc.html for more insight.
32
21
  #
33
22
  # == Creation
34
23
  #
@@ -290,10 +279,10 @@ module ActiveEntity #:nodoc:
290
279
  include Validations
291
280
  include Attributes
292
281
  include AttributeDecorators
293
- include DefineCallbacks
294
282
  include AttributeMethods
283
+ include Callbacks
295
284
  include Associations
296
- include ValidateEmbeddedAssociation
285
+ include ValidateEmbedsAssociation
297
286
  include NestedAttributes
298
287
  include Reflection
299
288
  include Serialization
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveEntity
4
- module DefineCallbacks
4
+ module Callbacks
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ CALLBACKS = [
8
+ :after_initialize, :before_validation, :after_validation
9
+ ]
10
+
7
11
  module ClassMethods # :nodoc:
8
12
  include ActiveModel::Callbacks
9
13
  end
@@ -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
@@ -27,6 +27,32 @@ module ActiveEntity
27
27
  # conversation.status.nil? # => true
28
28
  # conversation.status # => nil
29
29
  #
30
+ # Scopes based on the allowed values of the enum field will be provided
31
+ # as well. With the above example:
32
+ #
33
+ # Conversation.active
34
+ # Conversation.not_active
35
+ # Conversation.archived
36
+ # Conversation.not_archived
37
+ #
38
+ # Of course, you can also query them directly if the scopes don't fit your
39
+ # needs:
40
+ #
41
+ # Conversation.where(status: [:active, :archived])
42
+ # Conversation.where.not(status: :active)
43
+ #
44
+ # Defining scopes can be disabled by setting +:_scopes+ to +false+.
45
+ #
46
+ # class Conversation < ActiveEntity::Base
47
+ # enum status: [ :active, :archived ], _scopes: false
48
+ # end
49
+ #
50
+ # You can set the default value from the database declaration, like:
51
+ #
52
+ # create_table :conversations do |t|
53
+ # t.column :status, :integer, default: 0
54
+ # end
55
+ #
30
56
  # Good practice is to let the first declared status be the default.
31
57
  #
32
58
  # Finally, it's also possible to explicitly map the relation between attribute and
@@ -97,12 +123,12 @@ module ActiveEntity
97
123
  end
98
124
 
99
125
  def cast(value)
100
- return if value.blank?
101
-
102
126
  if mapping.has_key?(value)
103
127
  value.to_s
104
128
  elsif mapping.has_value?(value)
105
129
  mapping.key(value)
130
+ elsif value.blank?
131
+ nil
106
132
  else
107
133
  assert_valid_value(value)
108
134
  end
@@ -124,6 +150,7 @@ module ActiveEntity
124
150
  end
125
151
 
126
152
  private
153
+
127
154
  attr_reader :name, :mapping, :subtype
128
155
  end
129
156
 
@@ -182,6 +209,7 @@ module ActiveEntity
182
209
  end
183
210
 
184
211
  private
212
+
185
213
  def _enum_methods_module
186
214
  @_enum_methods_module ||= begin
187
215
  mod = Module.new
@@ -212,8 +240,6 @@ module ActiveEntity
212
240
  def detect_enum_conflict!(enum_name, method_name, klass_method = false)
213
241
  if klass_method && dangerous_class_method?(method_name)
214
242
  raise_conflict_error(enum_name, method_name, type: "class")
215
- # elsif klass_method && method_defined_within?(method_name, Relation)
216
- # raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
217
243
  elsif !klass_method && dangerous_attribute_method?(method_name)
218
244
  raise_conflict_error(enum_name, method_name)
219
245
  elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
@@ -7,13 +7,6 @@ module ActiveEntity
7
7
  class ActiveEntityError < StandardError
8
8
  end
9
9
 
10
- # Raised when the single-table inheritance mechanism fails to locate the subclass
11
- # (for example due to improper usage of column that
12
- # {ActiveEntity::Base.inheritance_attribute}[rdoc-ref:ModelSchema::ClassMethods#inheritance_attribute]
13
- # points to).
14
- class SubclassNotFound < ActiveEntityError
15
- end
16
-
17
10
  # Raised when an object assigned to an association has an incorrect type.
18
11
  #
19
12
  # class Ticket < ActiveEntity::Base
@@ -41,10 +34,6 @@ module ActiveEntity
41
34
  class ConfigurationError < ActiveEntityError
42
35
  end
43
36
 
44
- # Raised on attempt to update record that is instantiated as read only.
45
- class ReadOnlyRecord < ActiveEntityError
46
- end
47
-
48
37
  # Raised when attribute has a name reserved by Active Entity (when attribute
49
38
  # has name of one of Active Entity instance methods).
50
39
  class DangerousAttributeError < ActiveEntityError