activeentity 0.0.1.beta1

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 (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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Coders # :nodoc:
5
+ class JSON # :nodoc:
6
+ def self.dump(obj)
7
+ ActiveSupport::JSON.encode(obj)
8
+ end
9
+
10
+ def self.load(json)
11
+ ActiveSupport::JSON.decode(json) unless json.blank?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module ActiveEntity
6
+ module Coders # :nodoc:
7
+ class YAMLColumn # :nodoc:
8
+ attr_accessor :object_class
9
+
10
+ def initialize(attr_name, object_class = Object)
11
+ @attr_name = attr_name
12
+ @object_class = object_class
13
+ check_arity_of_constructor
14
+ end
15
+
16
+ def dump(obj)
17
+ return if obj.nil?
18
+
19
+ assert_valid_value(obj, action: "dump")
20
+ YAML.dump obj
21
+ end
22
+
23
+ def load(yaml)
24
+ return object_class.new if object_class != Object && yaml.nil?
25
+ return yaml unless yaml.is_a?(String) && /^---/.match?(yaml)
26
+ obj = YAML.load(yaml)
27
+
28
+ assert_valid_value(obj, action: "load")
29
+ obj ||= object_class.new if object_class != Object
30
+
31
+ obj
32
+ end
33
+
34
+ def assert_valid_value(obj, action:)
35
+ unless obj.nil? || obj.is_a?(object_class)
36
+ raise SerializationTypeMismatch,
37
+ "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def check_arity_of_constructor
44
+ load(nil)
45
+ rescue ArgumentError
46
+ raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,281 @@
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
+ elsif abstract_class?
62
+ "#{super}(abstract)"
63
+ else
64
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
65
+ "#{super}(#{attr_list})"
66
+ end
67
+ end
68
+
69
+ # Overwrite the default class equality method to provide support for decorated models.
70
+ def ===(object) # :nodoc:
71
+ object.is_a?(self)
72
+ end
73
+ end
74
+
75
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
76
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
77
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
78
+ # hence you can't have attributes that aren't part of the table columns.
79
+ #
80
+ # ==== Example:
81
+ # # Instantiates a single new object
82
+ # User.new(first_name: 'Jamie')
83
+ def initialize(attributes = nil)
84
+ self.class.define_attribute_methods
85
+ @attributes = self.class._default_attributes.deep_dup
86
+
87
+ init_internals
88
+ initialize_internals_callback
89
+
90
+ assign_attributes(attributes) if attributes
91
+
92
+ yield self if block_given?
93
+ _run_initialize_callbacks
94
+
95
+ enable_readonly!
96
+ end
97
+
98
+ ##
99
+ # :method: clone
100
+ # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
101
+ # That means that modifying attributes of the clone will modify the original, since they will both point to the
102
+ # same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
103
+ #
104
+ # user = User.first
105
+ # new_user = user.clone
106
+ # user.name # => "Bob"
107
+ # new_user.name = "Joe"
108
+ # user.name # => "Joe"
109
+ #
110
+ # user.object_id == new_user.object_id # => false
111
+ # user.name.object_id == new_user.name.object_id # => true
112
+ #
113
+ # user.name.object_id == user.dup.name.object_id # => false
114
+
115
+ ##
116
+ # :method: dup
117
+ # Duped objects have no id assigned and are treated as new records. Note
118
+ # that this is a "shallow" copy as it copies the object's attributes
119
+ # only, not its associations. The extent of a "deep" copy is application
120
+ # specific and is therefore left to the application to implement according
121
+ # to its need.
122
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
123
+
124
+ ##
125
+ def initialize_dup(other) # :nodoc:
126
+ @attributes = @attributes.deep_dup
127
+
128
+ _run_initialize_callbacks
129
+
130
+ super
131
+ end
132
+
133
+ # Populate +coder+ with attributes about this record that should be
134
+ # serialized. The structure of +coder+ defined in this method is
135
+ # guaranteed to match the structure of +coder+ passed to the #init_with
136
+ # method.
137
+ #
138
+ # Example:
139
+ #
140
+ # class Post < ActiveEntity::Base
141
+ # end
142
+ # coder = {}
143
+ # Post.new.encode_with(coder)
144
+ # coder # => {"attributes" => {"id" => nil, ... }}
145
+ def encode_with(coder)
146
+ self.class.yaml_encoder.encode(@attributes, coder)
147
+ coder["active_entity_yaml_version"] = 2
148
+ end
149
+
150
+ # Clone and freeze the attributes hash such that associations are still
151
+ # accessible, even on destroyed records, but cloned models will not be
152
+ # frozen.
153
+ def freeze
154
+ @attributes = @attributes.clone.freeze
155
+ self
156
+ end
157
+
158
+ # Returns +true+ if the attributes hash has been frozen.
159
+ def frozen?
160
+ @attributes.frozen?
161
+ end
162
+
163
+ # Allows sort on objects
164
+ def <=>(other_object)
165
+ if other_object.is_a?(self.class)
166
+ to_key <=> other_object.to_key
167
+ else
168
+ super
169
+ end
170
+ end
171
+
172
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
173
+ # attributes will be marked as read only since they cannot be saved.
174
+ def readonly?
175
+ @readonly
176
+ end
177
+
178
+ # Marks this record as read only.
179
+ def readonly!
180
+ @readonly = true
181
+ end
182
+
183
+ # Returns the contents of the record as a nicely formatted string.
184
+ def inspect
185
+ # We check defined?(@attributes) not to issue warnings if the object is
186
+ # 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}"
200
+ end
201
+ end.compact.join(", ")
202
+ else
203
+ "not initialized"
204
+ end
205
+
206
+ "#<#{self.class} #{inspection}>"
207
+ end
208
+
209
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
210
+ # when pp is required.
211
+ def pretty_print(pp)
212
+ return super if custom_inspect_method_defined?
213
+ pp.object_address_group(self) do
214
+ if defined?(@attributes) && @attributes
215
+ attr_names = self.class.attribute_names.select { |name| has_attribute?(name) }
216
+ pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
217
+ pp.breakable " "
218
+ pp.group(1) do
219
+ pp.text attr_name
220
+ pp.text ":"
221
+ pp.breakable
222
+ value = _read_attribute(attr_name)
223
+ value = inspection_filter.filter_param(attr_name, value) unless value.nil?
224
+ pp.pp value
225
+ end
226
+ end
227
+ else
228
+ pp.breakable " "
229
+ pp.text "not initialized"
230
+ end
231
+ end
232
+ end
233
+
234
+ # Returns a hash of the given methods with their names as keys and returned values as values.
235
+ def slice(*methods)
236
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
237
+ end
238
+
239
+ private
240
+
241
+ # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
242
+ # the array, and then rescues from the possible +NoMethodError+. If those elements are
243
+ # +ActiveEntity::Base+'s, then this triggers the various +method_missing+'s that we have,
244
+ # which significantly impacts upon performance.
245
+ #
246
+ # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
247
+ #
248
+ # See also https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
249
+ def to_ary
250
+ nil
251
+ end
252
+
253
+ def init_internals
254
+ @readonly = false
255
+ @marked_for_destruction = false
256
+ end
257
+
258
+ def initialize_internals_callback
259
+ end
260
+
261
+ def thaw
262
+ if frozen?
263
+ @attributes = @attributes.dup
264
+ end
265
+ end
266
+
267
+ def custom_inspect_method_defined?
268
+ self.class.instance_method(:inspect).owner != ActiveEntity::Base.instance_method(:inspect).owner
269
+ end
270
+
271
+ def inspection_filter
272
+ @inspection_filter ||= begin
273
+ mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
274
+ def mask.pretty_print(pp)
275
+ pp.text __getobj__
276
+ end
277
+ ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module DefineCallbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods # :nodoc:
8
+ include ActiveModel::Callbacks
9
+ end
10
+
11
+ included do
12
+ include ActiveModel::Validations::Callbacks
13
+
14
+ define_model_callbacks :initialize, only: :after
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,234 @@
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
+ # Good practice is to let the first declared status be the default.
31
+ #
32
+ # Finally, it's also possible to explicitly map the relation between attribute and
33
+ # database integer with a hash:
34
+ #
35
+ # class Conversation < ActiveEntity::Base
36
+ # enum status: { active: 0, archived: 1 }
37
+ # end
38
+ #
39
+ # Note that when an array is used, the implicit mapping from the values to database
40
+ # integers is derived from the order the values appear in the array. In the example,
41
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
42
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
43
+ # database.
44
+ #
45
+ # Therefore, once a value is added to the enum array, its position in the array must
46
+ # be maintained, and new values should only be added to the end of the array. To
47
+ # remove unused values, the explicit hash syntax should be used.
48
+ #
49
+ # In rare circumstances you might need to access the mapping directly.
50
+ # The mappings are exposed through a class method with the pluralized attribute
51
+ # name, which return the mapping in a +HashWithIndifferentAccess+:
52
+ #
53
+ # Conversation.statuses[:active] # => 0
54
+ # Conversation.statuses["archived"] # => 1
55
+ #
56
+ # Use that class method when you need to know the ordinal value of an enum.
57
+ # For example, you can use that when manually building SQL strings:
58
+ #
59
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
60
+ #
61
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
62
+ # multiple enums with same values. If the passed value is +true+, the methods
63
+ # are prefixed/suffixed with the name of the enum. It is also possible to
64
+ # supply a custom value:
65
+ #
66
+ # class Conversation < ActiveEntity::Base
67
+ # enum status: [:active, :archived], _suffix: true
68
+ # enum comments_status: [:active, :inactive], _prefix: :comments
69
+ # end
70
+ #
71
+ # With the above example, the bang and predicate methods along with the
72
+ # associated scopes are now prefixed and/or suffixed accordingly:
73
+ #
74
+ # conversation.active_status!
75
+ # conversation.archived_status? # => false
76
+ #
77
+ # conversation.comments_inactive!
78
+ # conversation.comments_active? # => false
79
+
80
+ module Enum
81
+ def self.extended(base) # :nodoc:
82
+ base.class_attribute(:defined_enums, instance_writer: false, default: {})
83
+ end
84
+
85
+ def inherited(base) # :nodoc:
86
+ base.defined_enums = defined_enums.deep_dup
87
+ super
88
+ end
89
+
90
+ class EnumType < Type::Value # :nodoc:
91
+ delegate :type, to: :subtype
92
+
93
+ def initialize(name, mapping, subtype)
94
+ @name = name
95
+ @mapping = mapping
96
+ @subtype = subtype
97
+ end
98
+
99
+ def cast(value)
100
+ return if value.blank?
101
+
102
+ if mapping.has_key?(value)
103
+ value.to_s
104
+ elsif mapping.has_value?(value)
105
+ mapping.key(value)
106
+ else
107
+ assert_valid_value(value)
108
+ end
109
+ end
110
+
111
+ def deserialize(value)
112
+ return if value.nil?
113
+ mapping.key(subtype.deserialize(value))
114
+ end
115
+
116
+ def serialize(value)
117
+ mapping.fetch(value, value)
118
+ end
119
+
120
+ def assert_valid_value(value)
121
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
122
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
123
+ end
124
+ end
125
+
126
+ private
127
+ attr_reader :name, :mapping, :subtype
128
+ end
129
+
130
+ def enum(definitions)
131
+ klass = self
132
+ enum_prefix = definitions.delete(:_prefix)
133
+ enum_suffix = definitions.delete(:_suffix)
134
+ definitions.each do |name, values|
135
+ assert_valid_enum_definition_values(values)
136
+ # statuses = { }
137
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
138
+ name = name.to_s
139
+
140
+ # def self.statuses() statuses end
141
+ detect_enum_conflict!(name, name.pluralize, true)
142
+ singleton_class.define_method(name.pluralize) { enum_values }
143
+ defined_enums[name] = enum_values
144
+
145
+ detect_enum_conflict!(name, name)
146
+ detect_enum_conflict!(name, "#{name}=")
147
+
148
+ attr = attribute_alias?(name) ? attribute_alias(name) : name
149
+ decorate_attribute_type(attr, :enum) do |subtype|
150
+ EnumType.new(attr, enum_values, subtype)
151
+ end
152
+
153
+ _enum_methods_module.module_eval do
154
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
155
+ pairs.each do |label, value|
156
+ if enum_prefix == true
157
+ prefix = "#{name}_"
158
+ elsif enum_prefix
159
+ prefix = "#{enum_prefix}_"
160
+ end
161
+ if enum_suffix == true
162
+ suffix = "_#{name}"
163
+ elsif enum_suffix
164
+ suffix = "_#{enum_suffix}"
165
+ end
166
+
167
+ value_method_name = "#{prefix}#{label}#{suffix}"
168
+ enum_values[label] = value
169
+ label = label.to_s
170
+
171
+ # def active?() status == "active" end
172
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
173
+ define_method("#{value_method_name}?") { self[attr] == label }
174
+
175
+ # def active!() update!(status: 0) end
176
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
177
+ define_method("#{value_method_name}!") { update!(attr => value) }
178
+ end
179
+ end
180
+ enum_values.freeze
181
+ end
182
+ end
183
+
184
+ private
185
+ def _enum_methods_module
186
+ @_enum_methods_module ||= begin
187
+ mod = Module.new
188
+ include mod
189
+ mod
190
+ end
191
+ end
192
+
193
+ def assert_valid_enum_definition_values(values)
194
+ unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
195
+ error_message = <<~MSG
196
+ Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
197
+ MSG
198
+ raise ArgumentError, error_message
199
+ end
200
+
201
+ if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?)
202
+ raise ArgumentError, "Enum label name must not be blank."
203
+ end
204
+ end
205
+
206
+ ENUM_CONFLICT_MESSAGE = \
207
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
208
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
209
+ "by %{source}."
210
+ private_constant :ENUM_CONFLICT_MESSAGE
211
+
212
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
213
+ if klass_method && dangerous_class_method?(method_name)
214
+ 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
+ elsif !klass_method && dangerous_attribute_method?(method_name)
218
+ raise_conflict_error(enum_name, method_name)
219
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
220
+ raise_conflict_error(enum_name, method_name, source: "another enum")
221
+ end
222
+ end
223
+
224
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Entity")
225
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
226
+ enum: enum_name,
227
+ klass: name,
228
+ type: type,
229
+ method: method_name,
230
+ source: source
231
+ }
232
+ end
233
+ end
234
+ end