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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +297 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +59 -77
  6. data/Rakefile +13 -1
  7. data/SUMMARY.md +22 -2
  8. data/bin/console +8 -0
  9. data/bin/loc +0 -0
  10. data/bin/setup +0 -0
  11. data/bin/standardize +0 -0
  12. data/lib/all_futures/association.rb +42 -0
  13. data/lib/all_futures/association_index.rb +69 -0
  14. data/lib/all_futures/attributes.rb +28 -0
  15. data/lib/all_futures/base.rb +66 -0
  16. data/lib/all_futures/callbacks.rb +71 -0
  17. data/lib/all_futures/configuration.rb +9 -0
  18. data/lib/all_futures/dirty.rb +116 -0
  19. data/lib/all_futures/embeds.rb +40 -0
  20. data/lib/all_futures/errors.rb +37 -0
  21. data/lib/all_futures/finder.rb +214 -0
  22. data/lib/all_futures/locale/en.yml +49 -0
  23. data/lib/all_futures/persistence.rb +595 -0
  24. data/lib/all_futures/presenter.rb +35 -0
  25. data/lib/all_futures/railtie.rb +26 -0
  26. data/lib/all_futures/translation.rb +22 -0
  27. data/lib/all_futures/validations.rb +10 -0
  28. data/lib/all_futures/version.rb +3 -1
  29. data/lib/all_futures/versions.rb +80 -0
  30. data/lib/all_futures.rb +24 -1
  31. data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
  32. data/lib/vendor/README.md +8 -0
  33. data/lib/vendor/active_entity/aggregations.rb +277 -0
  34. data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
  35. data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
  36. data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
  37. data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
  38. data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
  39. data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
  40. data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
  41. data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
  42. data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
  43. data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
  44. data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
  45. data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
  46. data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
  47. data/lib/vendor/active_entity/associations.rb +168 -0
  48. data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
  49. data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
  50. data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
  51. data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
  52. data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
  53. data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
  54. data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
  55. data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
  56. data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
  57. data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
  58. data/lib/vendor/active_entity/attribute_methods.rb +833 -0
  59. data/lib/vendor/active_entity/attributes.rb +240 -0
  60. data/lib/vendor/active_entity/base.rb +293 -0
  61. data/lib/vendor/active_entity/callbacks.rb +21 -0
  62. data/lib/vendor/active_entity/coders/json.rb +15 -0
  63. data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
  64. data/lib/vendor/active_entity/core.rb +348 -0
  65. data/lib/vendor/active_entity/enum.rb +260 -0
  66. data/lib/vendor/active_entity/errors.rb +69 -0
  67. data/lib/vendor/active_entity/gem_version.rb +17 -0
  68. data/lib/vendor/active_entity/inheritance.rb +176 -0
  69. data/lib/vendor/active_entity/integration.rb +78 -0
  70. data/lib/vendor/active_entity/locale/en.yml +45 -0
  71. data/lib/vendor/active_entity/model_schema.rb +118 -0
  72. data/lib/vendor/active_entity/nested_attributes.rb +554 -0
  73. data/lib/vendor/active_entity/persistence.rb +20 -0
  74. data/lib/vendor/active_entity/railtie.rb +76 -0
  75. data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
  76. data/lib/vendor/active_entity/reflection.rb +447 -0
  77. data/lib/vendor/active_entity/serialization.rb +28 -0
  78. data/lib/vendor/active_entity/store.rb +291 -0
  79. data/lib/vendor/active_entity/translation.rb +24 -0
  80. data/lib/vendor/active_entity/type/date.rb +9 -0
  81. data/lib/vendor/active_entity/type/date_time.rb +9 -0
  82. data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
  83. data/lib/vendor/active_entity/type/json.rb +30 -0
  84. data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
  85. data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
  86. data/lib/vendor/active_entity/type/registry.rb +112 -0
  87. data/lib/vendor/active_entity/type/serialized.rb +71 -0
  88. data/lib/vendor/active_entity/type/text.rb +11 -0
  89. data/lib/vendor/active_entity/type/time.rb +21 -0
  90. data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
  91. data/lib/vendor/active_entity/type.rb +75 -0
  92. data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
  93. data/lib/vendor/active_entity/validations/absence.rb +25 -0
  94. data/lib/vendor/active_entity/validations/associated.rb +60 -0
  95. data/lib/vendor/active_entity/validations/length.rb +26 -0
  96. data/lib/vendor/active_entity/validations/presence.rb +68 -0
  97. data/lib/vendor/active_entity/validations/subset.rb +76 -0
  98. data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
  99. data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
  100. data/lib/vendor/active_entity/validations.rb +47 -0
  101. data/lib/vendor/active_entity/version.rb +10 -0
  102. data/lib/vendor/active_entity.rb +99 -0
  103. data/lib/vendor/core_ext/array_without_blank.rb +48 -0
  104. metadata +172 -31
  105. data/lib/all_futures/all_futures.rb +0 -28
  106. data/lib/all_futures/config.rb +0 -14
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+ require "active_support/core_ext/string/filters"
5
+ require "active_support/parameter_filter"
6
+ require "concurrent/map"
7
+
8
+ module ActiveEntity
9
+ module Core
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ ##
14
+ # :singleton-method:
15
+ #
16
+ # Accepts a logger conforming to the interface of Log4r which is then
17
+ # passed on to any new database connections made and which can be
18
+ # retrieved on both a class and instance level by calling +logger+.
19
+ mattr_accessor :logger, instance_writer: false
20
+
21
+ ##
22
+ # :singleton-method:
23
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
24
+ # dates and times from the database. This is set to :utc by default.
25
+ mattr_accessor :default_timezone, instance_writer: false, default: :utc
26
+
27
+ self.filter_attributes = []
28
+ end
29
+
30
+ module ClassMethods
31
+ def initialize_generated_modules # :nodoc:
32
+ generated_association_methods
33
+ end
34
+
35
+ def generated_association_methods # :nodoc:
36
+ @generated_association_methods ||= begin
37
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
38
+ private_constant :GeneratedAssociationMethods
39
+ include mod
40
+
41
+ mod
42
+ end
43
+ end
44
+
45
+ # Returns columns which shouldn't be exposed while calling +#inspect+.
46
+ def filter_attributes
47
+ if defined?(@filter_attributes)
48
+ @filter_attributes
49
+ else
50
+ superclass.filter_attributes
51
+ end
52
+ end
53
+
54
+ # Specifies columns which shouldn't be exposed while calling +#inspect+.
55
+ attr_writer :filter_attributes
56
+
57
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
58
+ def inspect # :nodoc:
59
+ if self == Base
60
+ super
61
+ else
62
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
63
+ "#{super}#{ "(abstract)" if abstract_class? }(#{attr_list})"
64
+ end
65
+ end
66
+
67
+ # Overwrite the default class equality method to provide support for decorated models.
68
+ def ===(object) # :nodoc:
69
+ object.is_a?(self)
70
+ end
71
+ end
72
+
73
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
74
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
75
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
76
+ # hence you can't have attributes that aren't part of the table columns.
77
+ #
78
+ # ==== Example:
79
+ # # Instantiates a single new object
80
+ # User.new(first_name: 'Jamie')
81
+ def initialize(attributes = nil)
82
+ @attributes = self.class._default_attributes.deep_dup
83
+
84
+ init_internals
85
+ initialize_internals_callback
86
+
87
+ assign_attributes(attributes) if attributes
88
+
89
+ yield self if block_given?
90
+ _run_initialize_callbacks
91
+
92
+ enable_attr_readonly!
93
+ end
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
+
131
+ ##
132
+ # :method: clone
133
+ # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
134
+ # That means that modifying attributes of the clone will modify the original, since they will both point to the
135
+ # same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
136
+ #
137
+ # user = User.first
138
+ # new_user = user.clone
139
+ # user.name # => "Bob"
140
+ # new_user.name = "Joe"
141
+ # user.name # => "Joe"
142
+ #
143
+ # user.object_id == new_user.object_id # => false
144
+ # user.name.object_id == new_user.name.object_id # => true
145
+ #
146
+ # user.name.object_id == user.dup.name.object_id # => false
147
+
148
+ ##
149
+ # :method: dup
150
+ # Duped objects have no id assigned and are treated as new records. Note
151
+ # that this is a "shallow" copy as it copies the object's attributes
152
+ # only, not its associations. The extent of a "deep" copy is application
153
+ # specific and is therefore left to the application to implement according
154
+ # to its need.
155
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
156
+
157
+ ##
158
+ def initialize_dup(other) # :nodoc:
159
+ @attributes = @attributes.deep_dup
160
+
161
+ _run_initialize_callbacks
162
+
163
+ super
164
+ end
165
+
166
+ # Populate +coder+ with attributes about this record that should be
167
+ # serialized. The structure of +coder+ defined in this method is
168
+ # guaranteed to match the structure of +coder+ passed to the #init_with
169
+ # method.
170
+ #
171
+ # Example:
172
+ #
173
+ # class Post < ActiveEntity::Base
174
+ # end
175
+ # coder = {}
176
+ # Post.new.encode_with(coder)
177
+ # coder # => {"attributes" => {"id" => nil, ... }}
178
+ def encode_with(coder)
179
+ self.class.yaml_encoder.encode(@attributes, coder)
180
+ coder["active_entity_yaml_version"] = 2
181
+ end
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
+
210
+ # Clone and freeze the attributes hash such that associations are still
211
+ # accessible, even on destroyed records, but cloned models will not be
212
+ # frozen.
213
+ def freeze
214
+ @attributes = @attributes.clone.freeze
215
+ self
216
+ end
217
+
218
+ # Returns +true+ if the attributes hash has been frozen.
219
+ def frozen?
220
+ @attributes.frozen?
221
+ end
222
+
223
+ # Allows sort on objects
224
+ def <=>(other_object)
225
+ if other_object.is_a?(self.class)
226
+ to_key <=> other_object.to_key
227
+ else
228
+ super
229
+ end
230
+ end
231
+
232
+ def present? # :nodoc:
233
+ true
234
+ end
235
+
236
+ def blank? # :nodoc:
237
+ false
238
+ end
239
+
240
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
241
+ # attributes will be marked as read only since they cannot be saved.
242
+ def readonly?
243
+ @readonly
244
+ end
245
+
246
+ # Marks this record as read only.
247
+ def readonly!
248
+ @readonly = true
249
+ end
250
+
251
+ # Returns the contents of the record as a nicely formatted string.
252
+ def inspect
253
+ # We check defined?(@attributes) not to issue warnings if the object is
254
+ # allocated but not initialized.
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)
264
+ end
265
+ "#{name}: #{value}"
266
+ end
267
+ end.compact.join(", ")
268
+ else
269
+ "not initialized"
270
+ end
271
+
272
+ "#<#{self.class} #{inspection}>"
273
+ end
274
+
275
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
276
+ # when pp is required.
277
+ def pretty_print(pp)
278
+ return super if custom_inspect_method_defined?
279
+ pp.object_address_group(self) do
280
+ if defined?(@attributes) && @attributes
281
+ attr_names = self.class.attribute_names.select { |name| has_attribute?(name) }
282
+ pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
283
+ pp.breakable " "
284
+ pp.group(1) do
285
+ pp.text attr_name
286
+ pp.text ":"
287
+ pp.breakable
288
+ value = _read_attribute(attr_name)
289
+ value = inspection_filter.filter_param(attr_name, value) unless value.nil?
290
+ pp.pp value
291
+ end
292
+ end
293
+ else
294
+ pp.breakable " "
295
+ pp.text "not initialized"
296
+ end
297
+ end
298
+ end
299
+
300
+ # Returns a hash of the given methods with their names as keys and returned values as values.
301
+ def slice(*methods)
302
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
303
+ end
304
+
305
+ private
306
+
307
+ # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
308
+ # the array, and then rescues from the possible +NoMethodError+. If those elements are
309
+ # +ActiveEntity::Base+'s, then this triggers the various +method_missing+'s that we have,
310
+ # which significantly impacts upon performance.
311
+ #
312
+ # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
313
+ #
314
+ # See also https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
315
+ def to_ary
316
+ nil
317
+ end
318
+
319
+ def init_internals
320
+ @primary_key = self.class.primary_key
321
+ @readonly = false
322
+ @marked_for_destruction = false
323
+
324
+ self.class.define_attribute_methods
325
+ end
326
+
327
+ def initialize_internals_callback
328
+ end
329
+
330
+ def custom_inspect_method_defined?
331
+ self.class.instance_method(:inspect).owner != ActiveEntity::Base.instance_method(:inspect).owner
332
+ end
333
+
334
+ class InspectionMask < DelegateClass(::String)
335
+ def pretty_print(pp)
336
+ pp.text __getobj__
337
+ end
338
+ end
339
+ private_constant :InspectionMask
340
+
341
+ def inspection_filter
342
+ @inspection_filter ||= begin
343
+ mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
344
+ ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
345
+ end
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActiveEntity
6
+ # Declare an enum attribute where the values map to integers in the database,
7
+ # but can be queried by name. Example:
8
+ #
9
+ # class Conversation < ActiveEntity::Base
10
+ # enum status: [ :active, :archived ]
11
+ # end
12
+ #
13
+ # # conversation.update! status: 0
14
+ # conversation.active!
15
+ # conversation.active? # => true
16
+ # conversation.status # => "active"
17
+ #
18
+ # # conversation.update! status: 1
19
+ # conversation.archived!
20
+ # conversation.archived? # => true
21
+ # conversation.status # => "archived"
22
+ #
23
+ # # conversation.status = 1
24
+ # conversation.status = "archived"
25
+ #
26
+ # conversation.status = nil
27
+ # conversation.status.nil? # => true
28
+ # conversation.status # => nil
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
+ #
56
+ # Good practice is to let the first declared status be the default.
57
+ #
58
+ # Finally, it's also possible to explicitly map the relation between attribute and
59
+ # database integer with a hash:
60
+ #
61
+ # class Conversation < ActiveEntity::Base
62
+ # enum status: { active: 0, archived: 1 }
63
+ # end
64
+ #
65
+ # Note that when an array is used, the implicit mapping from the values to database
66
+ # integers is derived from the order the values appear in the array. In the example,
67
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
68
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
69
+ # database.
70
+ #
71
+ # Therefore, once a value is added to the enum array, its position in the array must
72
+ # be maintained, and new values should only be added to the end of the array. To
73
+ # remove unused values, the explicit hash syntax should be used.
74
+ #
75
+ # In rare circumstances you might need to access the mapping directly.
76
+ # The mappings are exposed through a class method with the pluralized attribute
77
+ # name, which return the mapping in a +HashWithIndifferentAccess+:
78
+ #
79
+ # Conversation.statuses[:active] # => 0
80
+ # Conversation.statuses["archived"] # => 1
81
+ #
82
+ # Use that class method when you need to know the ordinal value of an enum.
83
+ # For example, you can use that when manually building SQL strings:
84
+ #
85
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
86
+ #
87
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
88
+ # multiple enums with same values. If the passed value is +true+, the methods
89
+ # are prefixed/suffixed with the name of the enum. It is also possible to
90
+ # supply a custom value:
91
+ #
92
+ # class Conversation < ActiveEntity::Base
93
+ # enum status: [:active, :archived], _suffix: true
94
+ # enum comments_status: [:active, :inactive], _prefix: :comments
95
+ # end
96
+ #
97
+ # With the above example, the bang and predicate methods along with the
98
+ # associated scopes are now prefixed and/or suffixed accordingly:
99
+ #
100
+ # conversation.active_status!
101
+ # conversation.archived_status? # => false
102
+ #
103
+ # conversation.comments_inactive!
104
+ # conversation.comments_active? # => false
105
+
106
+ module Enum
107
+ def self.extended(base) # :nodoc:
108
+ base.class_attribute(:defined_enums, instance_writer: false, default: {})
109
+ end
110
+
111
+ def inherited(base) # :nodoc:
112
+ base.defined_enums = defined_enums.deep_dup
113
+ super
114
+ end
115
+
116
+ class EnumType < Type::Value # :nodoc:
117
+ delegate :type, to: :subtype
118
+
119
+ def initialize(name, mapping, subtype)
120
+ @name = name
121
+ @mapping = mapping
122
+ @subtype = subtype
123
+ end
124
+
125
+ def cast(value)
126
+ if mapping.has_key?(value)
127
+ value.to_s
128
+ elsif mapping.has_value?(value)
129
+ mapping.key(value)
130
+ elsif value.blank?
131
+ nil
132
+ else
133
+ assert_valid_value(value)
134
+ end
135
+ end
136
+
137
+ def deserialize(value)
138
+ return if value.nil?
139
+ mapping.key(subtype.deserialize(value))
140
+ end
141
+
142
+ def serialize(value)
143
+ mapping.fetch(value, value)
144
+ end
145
+
146
+ def assert_valid_value(value)
147
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
148
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ attr_reader :name, :mapping, :subtype
155
+ end
156
+
157
+ def enum(definitions)
158
+ klass = self
159
+ enum_prefix = definitions.delete(:_prefix)
160
+ enum_suffix = definitions.delete(:_suffix)
161
+ definitions.each do |name, values|
162
+ assert_valid_enum_definition_values(values)
163
+ # statuses = { }
164
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
165
+ name = name.to_s
166
+
167
+ # def self.statuses() statuses end
168
+ detect_enum_conflict!(name, name.pluralize, true)
169
+ singleton_class.define_method(name.pluralize) { enum_values }
170
+ defined_enums[name] = enum_values
171
+
172
+ detect_enum_conflict!(name, name)
173
+ detect_enum_conflict!(name, "#{name}=")
174
+
175
+ attr = attribute_alias?(name) ? attribute_alias(name) : name
176
+ decorate_attribute_type(attr, :enum) do |subtype|
177
+ EnumType.new(attr, enum_values, subtype)
178
+ end
179
+
180
+ _enum_methods_module.module_eval do
181
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
182
+ pairs.each do |label, value|
183
+ if enum_prefix == true
184
+ prefix = "#{name}_"
185
+ elsif enum_prefix
186
+ prefix = "#{enum_prefix}_"
187
+ end
188
+ if enum_suffix == true
189
+ suffix = "_#{name}"
190
+ elsif enum_suffix
191
+ suffix = "_#{enum_suffix}"
192
+ end
193
+
194
+ value_method_name = "#{prefix}#{label}#{suffix}"
195
+ enum_values[label] = value
196
+ label = label.to_s
197
+
198
+ # def active?() status == "active" end
199
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
200
+ define_method("#{value_method_name}?") { self[attr] == label }
201
+
202
+ # def active!() update!(status: 0) end
203
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
204
+ define_method("#{value_method_name}!") { update!(attr => value) }
205
+ end
206
+ end
207
+ enum_values.freeze
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ def _enum_methods_module
214
+ @_enum_methods_module ||= begin
215
+ mod = Module.new
216
+ include mod
217
+ mod
218
+ end
219
+ end
220
+
221
+ def assert_valid_enum_definition_values(values)
222
+ unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
223
+ error_message = <<~MSG
224
+ Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
225
+ MSG
226
+ raise ArgumentError, error_message
227
+ end
228
+
229
+ if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?)
230
+ raise ArgumentError, "Enum label name must not be blank."
231
+ end
232
+ end
233
+
234
+ ENUM_CONFLICT_MESSAGE = \
235
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
236
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
237
+ "by %{source}."
238
+ private_constant :ENUM_CONFLICT_MESSAGE
239
+
240
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
241
+ if klass_method && dangerous_class_method?(method_name)
242
+ raise_conflict_error(enum_name, method_name, type: "class")
243
+ elsif !klass_method && dangerous_attribute_method?(method_name)
244
+ raise_conflict_error(enum_name, method_name)
245
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
246
+ raise_conflict_error(enum_name, method_name, source: "another enum")
247
+ end
248
+ end
249
+
250
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Entity")
251
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
252
+ enum: enum_name,
253
+ klass: name,
254
+ type: type,
255
+ method: method_name,
256
+ source: source
257
+ }
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ # = Active Entity Errors
5
+ #
6
+ # Generic Active Entity exception class.
7
+ class ActiveEntityError < StandardError
8
+ end
9
+
10
+ # Raised when an object assigned to an association has an incorrect type.
11
+ #
12
+ # class Ticket < ActiveEntity::Base
13
+ # has_many :patches
14
+ # end
15
+ #
16
+ # class Patch < ActiveEntity::Base
17
+ # belongs_to :ticket
18
+ # end
19
+ #
20
+ # # Comments are not patches, this assignment raises AssociationTypeMismatch.
21
+ # @ticket.patches << Comment.new(content: "Please attach tests to your patch.")
22
+ class AssociationTypeMismatch < ActiveEntityError
23
+ end
24
+
25
+ # Raised when unserialized object's type mismatches one specified for serializable field.
26
+ class SerializationTypeMismatch < ActiveEntityError
27
+ end
28
+
29
+ # Raised when association is being configured improperly or user tries to use
30
+ # offset and limit together with
31
+ # {ActiveEntity::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or
32
+ # {ActiveEntity::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many]
33
+ # associations.
34
+ class ConfigurationError < ActiveEntityError
35
+ end
36
+
37
+ # Raised when attribute has a name reserved by Active Entity (when attribute
38
+ # has name of one of Active Entity instance methods).
39
+ class DangerousAttributeError < ActiveEntityError
40
+ end
41
+
42
+ # Raised when unknown attributes are supplied via mass assignment.
43
+ UnknownAttributeError = ActiveModel::UnknownAttributeError
44
+
45
+ # Raised when an error occurred while doing a mass assignment to an attribute through the
46
+ # {ActiveEntity::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
47
+ # The exception has an +attribute+ property that is the name of the offending attribute.
48
+ class AttributeAssignmentError < ActiveEntityError
49
+ attr_reader :exception, :attribute
50
+
51
+ def initialize(message = nil, exception = nil, attribute = nil)
52
+ super(message)
53
+ @exception = exception
54
+ @attribute = attribute
55
+ end
56
+ end
57
+
58
+ # Raised when there are multiple errors while doing a mass assignment through the
59
+ # {ActiveEntity::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
60
+ # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
61
+ # objects, each corresponding to the error while assigning to an attribute.
62
+ class MultiparameterAssignmentErrors < ActiveEntityError
63
+ attr_reader :errors
64
+
65
+ def initialize(errors = nil)
66
+ @errors = errors
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ # Returns the version of the currently loaded Active Entity as a <tt>Gem::Version</tt>
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 6
11
+ MINOR = 3
12
+ TINY = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end