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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +42 -0
- data/README.md +145 -0
- data/Rakefile +29 -0
- data/lib/active_entity.rb +73 -0
- data/lib/active_entity/aggregations.rb +276 -0
- data/lib/active_entity/associations.rb +146 -0
- data/lib/active_entity/associations/embedded/association.rb +134 -0
- data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
- data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
- data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
- data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
- data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
- data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
- data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
- data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
- data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
- data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
- data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
- data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
- data/lib/active_entity/attribute_assignment.rb +85 -0
- data/lib/active_entity/attribute_decorators.rb +90 -0
- data/lib/active_entity/attribute_methods.rb +330 -0
- data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
- data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
- data/lib/active_entity/attribute_methods/query.rb +35 -0
- data/lib/active_entity/attribute_methods/read.rb +47 -0
- data/lib/active_entity/attribute_methods/serialization.rb +90 -0
- data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
- data/lib/active_entity/attribute_methods/write.rb +63 -0
- data/lib/active_entity/attributes.rb +165 -0
- data/lib/active_entity/base.rb +303 -0
- data/lib/active_entity/coders/json.rb +15 -0
- data/lib/active_entity/coders/yaml_column.rb +50 -0
- data/lib/active_entity/core.rb +281 -0
- data/lib/active_entity/define_callbacks.rb +17 -0
- data/lib/active_entity/enum.rb +234 -0
- data/lib/active_entity/errors.rb +80 -0
- data/lib/active_entity/gem_version.rb +17 -0
- data/lib/active_entity/inheritance.rb +278 -0
- data/lib/active_entity/integration.rb +78 -0
- data/lib/active_entity/locale/en.yml +45 -0
- data/lib/active_entity/model_schema.rb +115 -0
- data/lib/active_entity/nested_attributes.rb +592 -0
- data/lib/active_entity/readonly_attributes.rb +47 -0
- data/lib/active_entity/reflection.rb +441 -0
- data/lib/active_entity/serialization.rb +25 -0
- data/lib/active_entity/store.rb +242 -0
- data/lib/active_entity/translation.rb +24 -0
- data/lib/active_entity/type.rb +73 -0
- data/lib/active_entity/type/date.rb +9 -0
- data/lib/active_entity/type/date_time.rb +9 -0
- data/lib/active_entity/type/decimal_without_scale.rb +15 -0
- data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
- data/lib/active_entity/type/internal/timezone.rb +17 -0
- data/lib/active_entity/type/json.rb +30 -0
- data/lib/active_entity/type/modifiers/array.rb +72 -0
- data/lib/active_entity/type/registry.rb +92 -0
- data/lib/active_entity/type/serialized.rb +71 -0
- data/lib/active_entity/type/text.rb +11 -0
- data/lib/active_entity/type/time.rb +21 -0
- data/lib/active_entity/type/type_map.rb +62 -0
- data/lib/active_entity/type/unsigned_integer.rb +17 -0
- data/lib/active_entity/validate_embedded_association.rb +305 -0
- data/lib/active_entity/validations.rb +50 -0
- data/lib/active_entity/validations/absence.rb +25 -0
- data/lib/active_entity/validations/associated.rb +60 -0
- data/lib/active_entity/validations/length.rb +26 -0
- data/lib/active_entity/validations/presence.rb +68 -0
- data/lib/active_entity/validations/subset.rb +76 -0
- data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
- data/lib/active_entity/version.rb +10 -0
- data/lib/tasks/active_entity_tasks.rake +6 -0
- 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
|