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,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