activeentity 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +42 -0
  3. data/README.md +145 -0
  4. data/Rakefile +29 -0
  5. data/lib/active_entity.rb +73 -0
  6. data/lib/active_entity/aggregations.rb +276 -0
  7. data/lib/active_entity/associations.rb +146 -0
  8. data/lib/active_entity/associations/embedded/association.rb +134 -0
  9. data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
  10. data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
  11. data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
  12. data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
  13. data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
  14. data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
  15. data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
  16. data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
  17. data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
  18. data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
  19. data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
  20. data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
  21. data/lib/active_entity/attribute_assignment.rb +85 -0
  22. data/lib/active_entity/attribute_decorators.rb +90 -0
  23. data/lib/active_entity/attribute_methods.rb +330 -0
  24. data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
  26. data/lib/active_entity/attribute_methods/query.rb +35 -0
  27. data/lib/active_entity/attribute_methods/read.rb +47 -0
  28. data/lib/active_entity/attribute_methods/serialization.rb +90 -0
  29. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
  30. data/lib/active_entity/attribute_methods/write.rb +63 -0
  31. data/lib/active_entity/attributes.rb +165 -0
  32. data/lib/active_entity/base.rb +303 -0
  33. data/lib/active_entity/coders/json.rb +15 -0
  34. data/lib/active_entity/coders/yaml_column.rb +50 -0
  35. data/lib/active_entity/core.rb +281 -0
  36. data/lib/active_entity/define_callbacks.rb +17 -0
  37. data/lib/active_entity/enum.rb +234 -0
  38. data/lib/active_entity/errors.rb +80 -0
  39. data/lib/active_entity/gem_version.rb +17 -0
  40. data/lib/active_entity/inheritance.rb +278 -0
  41. data/lib/active_entity/integration.rb +78 -0
  42. data/lib/active_entity/locale/en.yml +45 -0
  43. data/lib/active_entity/model_schema.rb +115 -0
  44. data/lib/active_entity/nested_attributes.rb +592 -0
  45. data/lib/active_entity/readonly_attributes.rb +47 -0
  46. data/lib/active_entity/reflection.rb +441 -0
  47. data/lib/active_entity/serialization.rb +25 -0
  48. data/lib/active_entity/store.rb +242 -0
  49. data/lib/active_entity/translation.rb +24 -0
  50. data/lib/active_entity/type.rb +73 -0
  51. data/lib/active_entity/type/date.rb +9 -0
  52. data/lib/active_entity/type/date_time.rb +9 -0
  53. data/lib/active_entity/type/decimal_without_scale.rb +15 -0
  54. data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
  55. data/lib/active_entity/type/internal/timezone.rb +17 -0
  56. data/lib/active_entity/type/json.rb +30 -0
  57. data/lib/active_entity/type/modifiers/array.rb +72 -0
  58. data/lib/active_entity/type/registry.rb +92 -0
  59. data/lib/active_entity/type/serialized.rb +71 -0
  60. data/lib/active_entity/type/text.rb +11 -0
  61. data/lib/active_entity/type/time.rb +21 -0
  62. data/lib/active_entity/type/type_map.rb +62 -0
  63. data/lib/active_entity/type/unsigned_integer.rb +17 -0
  64. data/lib/active_entity/validate_embedded_association.rb +305 -0
  65. data/lib/active_entity/validations.rb +50 -0
  66. data/lib/active_entity/validations/absence.rb +25 -0
  67. data/lib/active_entity/validations/associated.rb +60 -0
  68. data/lib/active_entity/validations/length.rb +26 -0
  69. data/lib/active_entity/validations/presence.rb +68 -0
  70. data/lib/active_entity/validations/subset.rb +76 -0
  71. data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
  72. data/lib/active_entity/version.rb +10 -0
  73. data/lib/tasks/active_entity_tasks.rake +6 -0
  74. metadata +155 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
4
+ class EmbeddedIn < SingularAssociation #:nodoc:
5
+ def self.macro
6
+ :embedded_in
7
+ end
8
+
9
+ def self.valid_options(options)
10
+ super + [:default]
11
+ end
12
+
13
+ def self.define_callbacks(model, reflection)
14
+ super
15
+ add_default_callbacks(model, reflection) if reflection.options[:default]
16
+ end
17
+
18
+ def self.add_default_callbacks(model, reflection)
19
+ model.before_validation lambda { |o|
20
+ o.association(reflection.name).default(&reflection.options[:default])
21
+ }
22
+ end
23
+
24
+ def self.define_validations(model, reflection)
25
+ if reflection.options.key?(:required)
26
+ reflection.options[:optional] = !reflection.options.delete(:required)
27
+ end
28
+
29
+ required = !reflection.options[:optional]
30
+
31
+ super
32
+
33
+ if required
34
+ model.validates_presence_of reflection.name, message: :required
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
4
+ class EmbedsMany < CollectionAssociation #:nodoc:
5
+ def self.macro
6
+ :embeds_many
7
+ end
8
+
9
+ def self.valid_options(options)
10
+ super + [:inverse_of, :index_errors]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
4
+ class EmbedsOne < SingularAssociation #:nodoc:
5
+ def self.macro
6
+ :embeds_one
7
+ end
8
+
9
+ def self.define_validations(model, reflection)
10
+ super
11
+ if reflection.options[:required]
12
+ model.validates_presence_of reflection.name, message: :required
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class is inherited by the has_one and belongs_to association classes
4
+
5
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
6
+ class SingularAssociation < Association #:nodoc:
7
+ def self.valid_options(options)
8
+ super + [:inverse_of, :required]
9
+ end
10
+
11
+ def self.define_accessors(model, reflection)
12
+ super
13
+ mixin = model.generated_association_methods
14
+ name = reflection.name
15
+
16
+ define_constructors(mixin, name) if reflection.constructable?
17
+ end
18
+
19
+ # Defines the (build|create)_association methods for belongs_to or has_one association
20
+ def self.define_constructors(mixin, name)
21
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
22
+ def build_#{name}(*args, &block)
23
+ association(:#{name}).build(*args, &block)
24
+ end
25
+ CODE
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ # = Active Entity Association Collection
7
+ #
8
+ # CollectionAssociation is an abstract class that provides common stuff to
9
+ # ease the implementation of association proxies that represent
10
+ # collections. See the class hierarchy in Association.
11
+ #
12
+ # CollectionAssociation:
13
+ # HasManyAssociation => has_many
14
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
15
+ #
16
+ # The CollectionAssociation class provides common methods to the collections
17
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
18
+ # the +:through association+ option.
19
+ #
20
+ # You need to be careful with assumptions regarding the target: The proxy
21
+ # does not fetch records from the database until it needs them, but new
22
+ # ones created with +build+ are added to the target. So, the target may be
23
+ # non-empty and still lack children waiting to be read from the database.
24
+ # If you look directly to the database you cannot assume that's the entire
25
+ # collection because new records may have been added to the target, etc.
26
+ #
27
+ # If you need to work on all current children, new and existing records,
28
+ # +load_target+ and the +loaded+ flag are your friends.
29
+ class CollectionAssociation < Association #:nodoc:
30
+ def initialize(owner, reflection)
31
+ super
32
+
33
+ @target = []
34
+ end
35
+
36
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
37
+ def reader
38
+ @proxy ||= CollectionProxy.new(klass, self)
39
+ end
40
+
41
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
42
+ def writer(records)
43
+ replace(records)
44
+ end
45
+
46
+ def find(&block)
47
+ target.find(&block)
48
+ end
49
+
50
+ def build(attributes = {}, &block)
51
+ if attributes.is_a?(Array)
52
+ attributes.collect { |attr| build(attr, &block) }
53
+ else
54
+ add_to_target(build_record(attributes, &block))
55
+ end
56
+ end
57
+
58
+ # Add +records+ to this association. Returns +self+ so method calls may
59
+ # be chained. Since << flattens its argument list and inserts each record,
60
+ # +push+ and +concat+ behave identically.
61
+ def concat(*records)
62
+ records = records.flatten
63
+ concat_records(records)
64
+ end
65
+
66
+ # Removes all records from the association without calling callbacks
67
+ # on the associated records. It honors the +:dependent+ option. However
68
+ # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
69
+ # deletion strategy for the association is applied.
70
+ #
71
+ # You can force a particular deletion strategy by passing a parameter.
72
+ #
73
+ # Example:
74
+ #
75
+ # @author.books.delete_all(:nullify)
76
+ # @author.books.delete_all(:delete_all)
77
+ #
78
+ # See delete for more info.
79
+ def delete_all
80
+ target.clear
81
+ end
82
+ alias destroy_all delete_all
83
+
84
+ # Removes +records+ from this association calling +before_remove+ and
85
+ # +after_remove+ callbacks.
86
+ #
87
+ # This method is abstract in the sense that +delete_records+ has to be
88
+ # provided by descendants. Note this method does not imply the records
89
+ # are actually removed from the database, that depends precisely on
90
+ # +delete_records+. They are in any case removed from the collection.
91
+ def delete(*records)
92
+ records.each do |record|
93
+ target.delete(record)
94
+ end
95
+ end
96
+ alias destroy delete
97
+
98
+ # Returns the size of the collection by executing a SELECT COUNT(*)
99
+ # query if the collection hasn't been loaded, and calling
100
+ # <tt>collection.size</tt> if it has.
101
+ #
102
+ # If the collection has been already loaded +size+ and +length+ are
103
+ # equivalent. If not and you are going to need the records anyway
104
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
105
+ #
106
+ # This method is abstract in the sense that it relies on
107
+ # +count_records+, which is a method descendants have to provide.
108
+ def size
109
+ target.size
110
+ end
111
+
112
+ # Returns true if the collection is empty.
113
+ #
114
+ # If the collection has been loaded
115
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
116
+ # collection has not been loaded, it is equivalent to
117
+ # <tt>collection.exists?</tt>. If the collection has not already been
118
+ # loaded and you are going to fetch the records anyway it is better to
119
+ # check <tt>collection.length.zero?</tt>.
120
+ def empty?
121
+ size.zero?
122
+ end
123
+
124
+ # Replace this collection with +other_array+. This will perform a diff
125
+ # and delete/add only records that have changed.
126
+ def replace(other_array)
127
+ other_array.each { |val| raise_on_type_mismatch!(val) }
128
+
129
+ target.replace other_array
130
+ end
131
+
132
+ def include?(record)
133
+ if record.is_a?(reflection.klass)
134
+ target.include?(record)
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ def add_to_target(record, skip_callbacks = false, &block)
141
+ # index = @target.index(record)
142
+ # replace_on_target(record, index, skip_callbacks, &block)
143
+ replace_on_target(record, nil, skip_callbacks, &block)
144
+ end
145
+
146
+ private
147
+
148
+ def concat_records(records)
149
+ records.each do |record|
150
+ raise_on_type_mismatch!(record)
151
+ add_to_target(record)
152
+ end
153
+
154
+ records
155
+ end
156
+
157
+ def replace_on_target(record, index, skip_callbacks)
158
+ callback(:before_add, record) unless skip_callbacks
159
+
160
+ set_inverse_instance(record)
161
+
162
+ yield(record) if block_given?
163
+
164
+ if index
165
+ target[index] = record
166
+ else
167
+ target << record
168
+ end
169
+
170
+ callback(:after_add, record) unless skip_callbacks
171
+
172
+ record
173
+ end
174
+
175
+ def callback(method, record)
176
+ callbacks_for(method).each do |callback|
177
+ callback.call(method, owner, record)
178
+ end
179
+ end
180
+
181
+ def callbacks_for(callback_name)
182
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
183
+ owner.class.send(full_callback_name)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ # Association proxies in Active Entity are middlemen between the object that
7
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
8
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
9
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
10
+ # ActiveEntity::Reflection::AssociationReflection.
11
+ #
12
+ # For example, given
13
+ #
14
+ # class Blog < ActiveEntity::Base
15
+ # has_many :posts
16
+ # end
17
+ #
18
+ # blog = Blog.first
19
+ #
20
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
21
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
22
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
23
+ #
24
+ # This class delegates unknown methods to <tt>@target</tt> via
25
+ # <tt>method_missing</tt>.
26
+ #
27
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
28
+ #
29
+ # blog.posts.count
30
+ #
31
+ # is computed directly through SQL and does not trigger by itself the
32
+ # instantiation of the actual post records.
33
+ class CollectionProxy
34
+ include Enumerable
35
+
36
+ attr_reader :klass
37
+ alias :model :klass
38
+
39
+ delegate :to_xml, :encode_with, :length, :each, :join,
40
+ :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
41
+ :find, :last, :take, :blank?, :present?, :empty?, :any?, :one?, :many?, :include?,
42
+ :to_sentence, :to_formatted_s, :as_json,
43
+ :shuffle, :split, :slice, :index, :rindex, to: :records
44
+
45
+ def initialize(klass, association)
46
+ @klass = klass
47
+
48
+ @association = association
49
+
50
+ extensions = association.extensions
51
+ extend(*extensions) if extensions.any?
52
+ end
53
+
54
+ # Initializes new record from relation while maintaining the current
55
+ # scope.
56
+ #
57
+ # Expects arguments in the same format as {ActiveEntity::Base.new}[rdoc-ref:Core.new].
58
+ #
59
+ # users = User.where(name: 'DHH')
60
+ # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
61
+ #
62
+ # You can also pass a block to new with the new record as argument:
63
+ #
64
+ # user = users.new { |user| user.name = 'Oscar' }
65
+ # user.name # => Oscar
66
+ def new(attributes = nil, &block)
67
+ klass.new(attributes, &block)
68
+ end
69
+
70
+ alias build new
71
+
72
+ def pretty_print(q)
73
+ q.pp(records)
74
+ end
75
+
76
+ def inspect
77
+ entries = records.take([size, 11].compact.min).map!(&:inspect)
78
+
79
+ entries[10] = "..." if entries.size == 11
80
+
81
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
82
+ end
83
+
84
+ # Returns +true+ if the association has been loaded, otherwise +false+.
85
+ #
86
+ # person.pets.loaded? # => false
87
+ # person.pets
88
+ # person.pets.loaded? # => true
89
+ def loaded?
90
+ @association.loaded?
91
+ end
92
+
93
+ # Returns a new object of the collection type that has been instantiated
94
+ # with +attributes+ and linked to this object, but have not yet been saved.
95
+ # You can pass an array of attributes hashes, this will return an array
96
+ # with the new objects.
97
+ #
98
+ # class Person
99
+ # has_many :pets
100
+ # end
101
+ #
102
+ # person.pets.build
103
+ # # => #<Pet id: nil, name: nil, person_id: 1>
104
+ #
105
+ # person.pets.build(name: 'Fancy-Fancy')
106
+ # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
107
+ #
108
+ # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
109
+ # # => [
110
+ # # #<Pet id: nil, name: "Spook", person_id: 1>,
111
+ # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
112
+ # # #<Pet id: nil, name: "Brain", person_id: 1>
113
+ # # ]
114
+ #
115
+ # person.pets.size # => 5 # size of the collection
116
+ # person.pets.count # => 0 # count from database
117
+ def build(attributes = {}, &block)
118
+ @association.build(attributes, &block)
119
+ end
120
+ alias_method :new, :build
121
+
122
+ # Add one or more records to the collection by setting their foreign keys
123
+ # to the association's primary key. Since #<< flattens its argument list and
124
+ # inserts each record, +push+ and #concat behave identically. Returns +self+
125
+ # so method calls may be chained.
126
+ #
127
+ # class Person < ActiveEntity::Base
128
+ # has_many :pets
129
+ # end
130
+ #
131
+ # person.pets.size # => 0
132
+ # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
133
+ # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
134
+ # person.pets.size # => 3
135
+ #
136
+ # person.id # => 1
137
+ # person.pets
138
+ # # => [
139
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
140
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
141
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
142
+ # # ]
143
+ #
144
+ # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
145
+ # person.pets.size # => 5
146
+ def concat(*records)
147
+ @association.concat(*records)
148
+ end
149
+
150
+ # Replaces this collection with +other_array+. This will perform a diff
151
+ # and delete/add only records that have changed.
152
+ #
153
+ # class Person < ActiveEntity::Base
154
+ # has_many :pets
155
+ # end
156
+ #
157
+ # person.pets
158
+ # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
159
+ #
160
+ # other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
161
+ #
162
+ # person.pets.replace(other_pets)
163
+ #
164
+ # person.pets
165
+ # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
166
+ #
167
+ # If the supplied array has an incorrect association type, it raises
168
+ # an <tt>ActiveEntity::AssociationTypeMismatch</tt> error:
169
+ #
170
+ # person.pets.replace(["doo", "ggie", "gaga"])
171
+ # # => ActiveEntity::AssociationTypeMismatch: Pet expected, got String
172
+ def replace(other_array)
173
+ @association.replace(other_array)
174
+ end
175
+
176
+ def delete_all
177
+ @association.delete_all
178
+ end
179
+ alias destroy_all delete_all
180
+
181
+ def delete(*records)
182
+ @association.delete(*records)
183
+ end
184
+ alias destroy delete
185
+
186
+ # Equivalent to +delete_all+. The difference is that returns +self+, instead
187
+ # of an array with the deleted objects, so methods can be chained. See
188
+ # +delete_all+ for more information.
189
+ # Note that because +delete_all+ removes records by directly
190
+ # running an SQL query into the database, the +updated_at+ column of
191
+ # the object is not changed.
192
+ def clear
193
+ delete_all
194
+ self
195
+ end
196
+
197
+ def proxy_association
198
+ @association
199
+ end
200
+
201
+ # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
202
+ # contain the same number of elements and if each element is equal
203
+ # to the corresponding element in the +other+ array, otherwise returns
204
+ # +false+.
205
+ #
206
+ # class Person < ActiveEntity::Base
207
+ # has_many :pets
208
+ # end
209
+ #
210
+ # person.pets
211
+ # # => [
212
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
213
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
214
+ # # ]
215
+ #
216
+ # other = person.pets.to_ary
217
+ #
218
+ # person.pets == other
219
+ # # => true
220
+ #
221
+ # other = [Pet.new(id: 1), Pet.new(id: 2)]
222
+ #
223
+ # person.pets == other
224
+ # # => false
225
+ def ==(other)
226
+ records == other
227
+ end
228
+
229
+ ##
230
+ # :method: to_ary
231
+ #
232
+ # :call-seq:
233
+ # to_ary()
234
+ #
235
+ # Returns a new array of objects from the collection. If the collection
236
+ # hasn't been loaded, it fetches the records from the database.
237
+ #
238
+ # class Person < ActiveEntity::Base
239
+ # has_many :pets
240
+ # end
241
+ #
242
+ # person.pets
243
+ # # => [
244
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
245
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
246
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
247
+ # # ]
248
+ #
249
+ # other_pets = person.pets.to_ary
250
+ # # => [
251
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
252
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
253
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
254
+ # # ]
255
+ #
256
+ # other_pets.replace([Pet.new(name: 'BooGoo')])
257
+ #
258
+ # other_pets
259
+ # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
260
+ #
261
+ # person.pets
262
+ # # This is not affected by replace
263
+ # # => [
264
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
265
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
266
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
267
+ # # ]
268
+ # Converts relation objects to Array.
269
+ def to_ary
270
+ records.dup
271
+ end
272
+ alias to_a to_ary
273
+
274
+ def records # :nodoc:
275
+ @association.target
276
+ end
277
+
278
+ # Adds one or more +records+ to the collection by setting their foreign keys
279
+ # to the association's primary key. Returns +self+, so several appends may be
280
+ # chained together.
281
+ #
282
+ # class Person < ActiveEntity::Base
283
+ # has_many :pets
284
+ # end
285
+ #
286
+ # person.pets.size # => 0
287
+ # person.pets << Pet.new(name: 'Fancy-Fancy')
288
+ # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
289
+ # person.pets.size # => 3
290
+ #
291
+ # person.id # => 1
292
+ # person.pets
293
+ # # => [
294
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
295
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
296
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
297
+ # # ]
298
+ def <<(*records)
299
+ proxy_association.concat(records) && self
300
+ end
301
+ alias_method :push, :<<
302
+ alias_method :append, :<<
303
+
304
+ def prepend(*_args)
305
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end