all_futures 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +4 -1
- data/Gemfile.lock +297 -0
- data/LICENSE.txt +1 -1
- data/README.md +59 -77
- data/Rakefile +13 -1
- data/SUMMARY.md +22 -2
- data/bin/console +8 -0
- data/bin/loc +0 -0
- data/bin/setup +0 -0
- data/bin/standardize +0 -0
- data/lib/all_futures/association.rb +42 -0
- data/lib/all_futures/association_index.rb +69 -0
- data/lib/all_futures/attributes.rb +28 -0
- data/lib/all_futures/base.rb +66 -0
- data/lib/all_futures/callbacks.rb +71 -0
- data/lib/all_futures/configuration.rb +9 -0
- data/lib/all_futures/dirty.rb +116 -0
- data/lib/all_futures/embeds.rb +40 -0
- data/lib/all_futures/errors.rb +37 -0
- data/lib/all_futures/finder.rb +214 -0
- data/lib/all_futures/locale/en.yml +49 -0
- data/lib/all_futures/persistence.rb +595 -0
- data/lib/all_futures/presenter.rb +35 -0
- data/lib/all_futures/railtie.rb +26 -0
- data/lib/all_futures/translation.rb +22 -0
- data/lib/all_futures/validations.rb +10 -0
- data/lib/all_futures/version.rb +3 -1
- data/lib/all_futures/versions.rb +80 -0
- data/lib/all_futures.rb +24 -1
- data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
- data/lib/vendor/README.md +8 -0
- data/lib/vendor/active_entity/aggregations.rb +277 -0
- data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
- data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
- data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
- data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
- data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
- data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
- data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
- data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
- data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
- data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
- data/lib/vendor/active_entity/associations.rb +168 -0
- data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
- data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
- data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
- data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
- data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
- data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
- data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
- data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
- data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
- data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
- data/lib/vendor/active_entity/attribute_methods.rb +833 -0
- data/lib/vendor/active_entity/attributes.rb +240 -0
- data/lib/vendor/active_entity/base.rb +293 -0
- data/lib/vendor/active_entity/callbacks.rb +21 -0
- data/lib/vendor/active_entity/coders/json.rb +15 -0
- data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
- data/lib/vendor/active_entity/core.rb +348 -0
- data/lib/vendor/active_entity/enum.rb +260 -0
- data/lib/vendor/active_entity/errors.rb +69 -0
- data/lib/vendor/active_entity/gem_version.rb +17 -0
- data/lib/vendor/active_entity/inheritance.rb +176 -0
- data/lib/vendor/active_entity/integration.rb +78 -0
- data/lib/vendor/active_entity/locale/en.yml +45 -0
- data/lib/vendor/active_entity/model_schema.rb +118 -0
- data/lib/vendor/active_entity/nested_attributes.rb +554 -0
- data/lib/vendor/active_entity/persistence.rb +20 -0
- data/lib/vendor/active_entity/railtie.rb +76 -0
- data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
- data/lib/vendor/active_entity/reflection.rb +447 -0
- data/lib/vendor/active_entity/serialization.rb +28 -0
- data/lib/vendor/active_entity/store.rb +291 -0
- data/lib/vendor/active_entity/translation.rb +24 -0
- data/lib/vendor/active_entity/type/date.rb +9 -0
- data/lib/vendor/active_entity/type/date_time.rb +9 -0
- data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
- data/lib/vendor/active_entity/type/json.rb +30 -0
- data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
- data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
- data/lib/vendor/active_entity/type/registry.rb +112 -0
- data/lib/vendor/active_entity/type/serialized.rb +71 -0
- data/lib/vendor/active_entity/type/text.rb +11 -0
- data/lib/vendor/active_entity/type/time.rb +21 -0
- data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
- data/lib/vendor/active_entity/type.rb +75 -0
- data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
- data/lib/vendor/active_entity/validations/absence.rb +25 -0
- data/lib/vendor/active_entity/validations/associated.rb +60 -0
- data/lib/vendor/active_entity/validations/length.rb +26 -0
- data/lib/vendor/active_entity/validations/presence.rb +68 -0
- data/lib/vendor/active_entity/validations/subset.rb +76 -0
- data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
- data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
- data/lib/vendor/active_entity/validations.rb +47 -0
- data/lib/vendor/active_entity/version.rb +10 -0
- data/lib/vendor/active_entity.rb +99 -0
- data/lib/vendor/core_ext/array_without_blank.rb +48 -0
- metadata +172 -31
- data/lib/all_futures/all_futures.rb +0 -28
- data/lib/all_futures/config.rb +0 -14
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveEntity
|
|
4
|
+
module Associations
|
|
5
|
+
module Embeds
|
|
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, :size, 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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveEntity
|
|
4
|
+
module Associations
|
|
5
|
+
module Embeds
|
|
6
|
+
# = Active Entity Belongs To Association
|
|
7
|
+
class EmbeddedInAssociation < SingularAssociation #:nodoc:
|
|
8
|
+
def default(&block)
|
|
9
|
+
writer(owner.instance_exec(&block)) if reader.nil?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def replace(record)
|
|
15
|
+
if record
|
|
16
|
+
raise_on_type_mismatch!(record)
|
|
17
|
+
set_inverse_instance(record)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
self.target = record
|
|
21
|
+
end
|
|
22
|
+
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
|
|
23
|
+
# has_one associations.
|
|
24
|
+
def invertible_for?(record)
|
|
25
|
+
inverse = inverse_reflection_for(record)
|
|
26
|
+
inverse&.embeds_one?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveEntity
|
|
4
|
+
module Associations
|
|
5
|
+
module Embeds
|
|
6
|
+
# = Active Entity Has Many Association
|
|
7
|
+
# This is the proxy that handles a has many association.
|
|
8
|
+
#
|
|
9
|
+
# If the association has a <tt>:through</tt> option further specialization
|
|
10
|
+
# is provided by its child HasManyThroughAssociation.
|
|
11
|
+
class EmbedsManyAssociation < CollectionAssociation #:nodoc:
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveEntity
|
|
4
|
+
module Associations
|
|
5
|
+
module Embeds
|
|
6
|
+
# = Active Entity Has One Association
|
|
7
|
+
class EmbedsOneAssociation < SingularAssociation #:nodoc:
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def replace(record)
|
|
11
|
+
self.target =
|
|
12
|
+
if record.is_a? reflection.klass
|
|
13
|
+
record
|
|
14
|
+
elsif record.nil?
|
|
15
|
+
nil
|
|
16
|
+
elsif record.respond_to?(:to_h)
|
|
17
|
+
build_record(record.to_h)
|
|
18
|
+
end
|
|
19
|
+
rescue => ex
|
|
20
|
+
raise_on_type_mismatch!(record)
|
|
21
|
+
raise ex
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveEntity
|
|
4
|
+
module Associations
|
|
5
|
+
module Embeds
|
|
6
|
+
class SingularAssociation < Association #:nodoc:
|
|
7
|
+
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
|
|
8
|
+
def reader
|
|
9
|
+
target
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
|
|
13
|
+
def writer(record)
|
|
14
|
+
replace(record)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build(attributes = {}, &block)
|
|
18
|
+
record = build_record(attributes, &block)
|
|
19
|
+
set_new_record(record)
|
|
20
|
+
record
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def replace(_record)
|
|
26
|
+
raise NotImplementedError, "Subclasses must implement a replace(record) method"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_new_record(record)
|
|
30
|
+
replace(record)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/enumerable"
|
|
4
|
+
require "active_support/core_ext/string/conversions"
|
|
5
|
+
|
|
6
|
+
module ActiveEntity
|
|
7
|
+
class AssociationNotFoundError < ConfigurationError #:nodoc:
|
|
8
|
+
def initialize(record = nil, association_name = nil)
|
|
9
|
+
if record && association_name
|
|
10
|
+
super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
|
|
11
|
+
else
|
|
12
|
+
super("Association was not found.")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class InverseOfAssociationNotFoundError < ActiveEntityError #:nodoc:
|
|
18
|
+
def initialize(reflection = nil, associated_class = nil)
|
|
19
|
+
if reflection
|
|
20
|
+
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
|
|
21
|
+
else
|
|
22
|
+
super("Could not find the inverse association.")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# See ActiveEntity::Associations::ClassMethods for documentation.
|
|
28
|
+
module Associations # :nodoc:
|
|
29
|
+
extend ActiveSupport::Autoload
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
# These classes will be loaded when associations are created.
|
|
32
|
+
# So there is no need to eager load them.
|
|
33
|
+
module Embeds
|
|
34
|
+
extend ActiveSupport::Autoload
|
|
35
|
+
|
|
36
|
+
autoload :Association, "active_entity/associations/embeds/association"
|
|
37
|
+
autoload :SingularAssociation, "active_entity/associations/embeds/singular_association"
|
|
38
|
+
autoload :CollectionAssociation, "active_entity/associations/embeds/collection_association"
|
|
39
|
+
autoload :CollectionProxy, "active_entity/associations/embeds/collection_proxy"
|
|
40
|
+
|
|
41
|
+
module Builder #:nodoc:
|
|
42
|
+
autoload :Association, "active_entity/associations/embeds/builder/association"
|
|
43
|
+
autoload :SingularAssociation, "active_entity/associations/embeds/builder/singular_association"
|
|
44
|
+
autoload :CollectionAssociation, "active_entity/associations/embeds/builder/collection_association"
|
|
45
|
+
|
|
46
|
+
autoload :EmbeddedIn, "active_entity/associations/embeds/builder/embedded_in"
|
|
47
|
+
autoload :EmbedsOne, "active_entity/associations/embeds/builder/embeds_one"
|
|
48
|
+
autoload :EmbedsMany, "active_entity/associations/embeds/builder/embeds_many"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
eager_autoload do
|
|
52
|
+
autoload :EmbeddedInAssociation
|
|
53
|
+
autoload :EmbedsOneAssociation
|
|
54
|
+
autoload :EmbedsManyAssociation
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.eager_load!
|
|
59
|
+
super
|
|
60
|
+
Embeds.eager_load!
|
|
61
|
+
end
|
|
62
|
+
# Returns the association instance for the given name, instantiating it if it doesn't already exist
|
|
63
|
+
def association(name) #:nodoc:
|
|
64
|
+
association = association_instance_get(name)
|
|
65
|
+
|
|
66
|
+
if association.nil?
|
|
67
|
+
unless reflection = self.class._reflect_on_association(name)
|
|
68
|
+
raise AssociationNotFoundError.new(self, name)
|
|
69
|
+
end
|
|
70
|
+
association = reflection.association_class.new(self, reflection)
|
|
71
|
+
association_instance_set(name, association)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
association
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def association_cached?(name) # :nodoc:
|
|
78
|
+
@association_cache.key?(name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def initialize_dup(*) # :nodoc:
|
|
82
|
+
@association_cache = {}
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def init_internals
|
|
89
|
+
@association_cache = {}
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the specified association instance if it exists, +nil+ otherwise.
|
|
94
|
+
def association_instance_get(name)
|
|
95
|
+
@association_cache[name]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Set the specified association instance.
|
|
99
|
+
def association_instance_set(name, association)
|
|
100
|
+
@association_cache[name] = association
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# \Associations are a set of macro-like class methods for tying objects together through
|
|
104
|
+
# foreign keys. They express relationships like "Project has one Project Manager"
|
|
105
|
+
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
|
|
106
|
+
# class which are specialized according to the collection or association symbol and the
|
|
107
|
+
# options hash. It works much the same way as Ruby's own <tt>attr*</tt>
|
|
108
|
+
# methods.
|
|
109
|
+
#
|
|
110
|
+
# class Project < ActiveEntity::Base
|
|
111
|
+
# belongs_to :portfolio
|
|
112
|
+
# has_one :project_manager
|
|
113
|
+
# has_many :milestones
|
|
114
|
+
# has_and_belongs_to_many :categories
|
|
115
|
+
# end
|
|
116
|
+
#
|
|
117
|
+
# The project class now has the following methods (and more) to ease the traversal and
|
|
118
|
+
# manipulation of its relationships:
|
|
119
|
+
# * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt>
|
|
120
|
+
# * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt>
|
|
121
|
+
# * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>,
|
|
122
|
+
# <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>,
|
|
123
|
+
# <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt>
|
|
124
|
+
# * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>,
|
|
125
|
+
# <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt>
|
|
126
|
+
#
|
|
127
|
+
# === A word of warning
|
|
128
|
+
#
|
|
129
|
+
# Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveEntity::Core] of
|
|
130
|
+
# <tt>ActiveEntity::Base</tt>. Since the association adds a method with that name to
|
|
131
|
+
# its model, using an association with the same name as one provided by <tt>ActiveEntity::Base</tt> will override the method inherited through <tt>ActiveEntity::Base</tt> and will break things.
|
|
132
|
+
# For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveEntity::Base</tt> instance methods.
|
|
133
|
+
module ClassMethods
|
|
134
|
+
def embedded_in(name, **options)
|
|
135
|
+
reflection = Embeds::Builder::EmbeddedIn.build(self, name, options)
|
|
136
|
+
Reflection.add_reflection self, name, reflection
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def embeds_one(name, **options)
|
|
140
|
+
reflection = Embeds::Builder::EmbedsOne.build(self, name, options)
|
|
141
|
+
Reflection.add_reflection self, name, reflection
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def embeds_many(name, **options)
|
|
145
|
+
reflection = Embeds::Builder::EmbedsMany.build(self, name, options)
|
|
146
|
+
Reflection.add_reflection self, name, reflection
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def association_names
|
|
150
|
+
@association_names ||=
|
|
151
|
+
if !abstract_class?
|
|
152
|
+
reflections.keys.map(&:to_sym)
|
|
153
|
+
else
|
|
154
|
+
[]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def embeds_association_names
|
|
159
|
+
@association_names ||=
|
|
160
|
+
if !abstract_class?
|
|
161
|
+
reflections.select { |_, r| r.embedded? }.keys.map(&:to_sym)
|
|
162
|
+
else
|
|
163
|
+
[]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model/forbidden_attributes_protection"
|
|
4
|
+
|
|
5
|
+
module ActiveEntity
|
|
6
|
+
module AttributeAssignment
|
|
7
|
+
include ActiveModel::AttributeAssignment
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def _assign_attributes(attributes)
|
|
12
|
+
multi_parameter_attributes = nested_parameter_attributes = nil
|
|
13
|
+
|
|
14
|
+
attributes.each do |k, v|
|
|
15
|
+
key = k.to_s
|
|
16
|
+
|
|
17
|
+
if key.include?("(")
|
|
18
|
+
(multi_parameter_attributes ||= {})[key] = v
|
|
19
|
+
elsif v.is_a?(Hash)
|
|
20
|
+
(nested_parameter_attributes ||= {})[key] = v
|
|
21
|
+
else
|
|
22
|
+
_assign_attribute(key, v)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
assign_nested_parameter_attributes(nested_parameter_attributes) if nested_parameter_attributes
|
|
27
|
+
assign_multiparameter_attributes(multi_parameter_attributes) if multi_parameter_attributes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Assign any deferred nested attributes after the base attributes have been set.
|
|
31
|
+
def assign_nested_parameter_attributes(pairs)
|
|
32
|
+
pairs.each { |k, v| _assign_attribute(k, v) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
|
36
|
+
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
|
37
|
+
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
|
38
|
+
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
|
39
|
+
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
|
|
40
|
+
# f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
|
|
41
|
+
def assign_multiparameter_attributes(pairs)
|
|
42
|
+
execute_callstack_for_multiparameter_attributes(
|
|
43
|
+
extract_callstack_for_multiparameter_attributes(pairs)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute_callstack_for_multiparameter_attributes(callstack)
|
|
48
|
+
errors = []
|
|
49
|
+
callstack.each do |name, values_with_empty_parameters|
|
|
50
|
+
if values_with_empty_parameters.each_value.all?(&:nil?)
|
|
51
|
+
values = nil
|
|
52
|
+
else
|
|
53
|
+
values = values_with_empty_parameters
|
|
54
|
+
end
|
|
55
|
+
send("#{name}=", values)
|
|
56
|
+
rescue => ex
|
|
57
|
+
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
|
|
58
|
+
end
|
|
59
|
+
unless errors.empty?
|
|
60
|
+
error_descriptions = errors.map(&:message).join(",")
|
|
61
|
+
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_callstack_for_multiparameter_attributes(pairs)
|
|
66
|
+
attributes = {}
|
|
67
|
+
|
|
68
|
+
pairs.each do |(multiparameter_name, value)|
|
|
69
|
+
attribute_name = multiparameter_name.split("(").first
|
|
70
|
+
attributes[attribute_name] ||= {}
|
|
71
|
+
|
|
72
|
+
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
|
73
|
+
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attributes
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def type_cast_attribute_value(multiparameter_name, value)
|
|
80
|
+
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find_parameter_position(multiparameter_name)
|
|
84
|
+
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|