activeentity 0.0.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|