careacademy-enumerize 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +69 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +2 -0
  5. data/CHANGELOG.md +327 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.global +12 -0
  8. data/Gemfile.mongo_mapper +6 -0
  9. data/Gemfile.rails60 +6 -0
  10. data/Gemfile.rails61 +6 -0
  11. data/Gemfile.rails70 +9 -0
  12. data/Gemfile.railsmaster +5 -0
  13. data/MIT-LICENSE +22 -0
  14. data/README.md +641 -0
  15. data/Rakefile +17 -0
  16. data/enumerize.gemspec +22 -0
  17. data/lib/enumerize/activemodel.rb +47 -0
  18. data/lib/enumerize/activerecord.rb +142 -0
  19. data/lib/enumerize/attribute.rb +192 -0
  20. data/lib/enumerize/attribute_map.rb +40 -0
  21. data/lib/enumerize/base.rb +112 -0
  22. data/lib/enumerize/hooks/formtastic.rb +27 -0
  23. data/lib/enumerize/hooks/sequel_dataset.rb +17 -0
  24. data/lib/enumerize/hooks/simple_form.rb +37 -0
  25. data/lib/enumerize/hooks/uniqueness.rb +22 -0
  26. data/lib/enumerize/integrations/rails_admin.rb +18 -0
  27. data/lib/enumerize/integrations/rspec/matcher.rb +164 -0
  28. data/lib/enumerize/integrations/rspec.rb +19 -0
  29. data/lib/enumerize/module.rb +33 -0
  30. data/lib/enumerize/module_attributes.rb +12 -0
  31. data/lib/enumerize/mongoid.rb +29 -0
  32. data/lib/enumerize/predicatable.rb +23 -0
  33. data/lib/enumerize/predicates.rb +76 -0
  34. data/lib/enumerize/scope/activerecord.rb +53 -0
  35. data/lib/enumerize/scope/mongoid.rb +50 -0
  36. data/lib/enumerize/scope/sequel.rb +56 -0
  37. data/lib/enumerize/sequel.rb +62 -0
  38. data/lib/enumerize/set.rb +81 -0
  39. data/lib/enumerize/utils.rb +12 -0
  40. data/lib/enumerize/value.rb +47 -0
  41. data/lib/enumerize/version.rb +5 -0
  42. data/lib/enumerize.rb +90 -0
  43. data/lib/sequel/plugins/enumerize.rb +18 -0
  44. data/spec/enumerize/integrations/rspec/matcher_spec.rb +261 -0
  45. data/spec/spec_helper.rb +30 -0
  46. data/test/activemodel_test.rb +114 -0
  47. data/test/activerecord_test.rb +679 -0
  48. data/test/attribute_map_test.rb +70 -0
  49. data/test/attribute_test.rb +141 -0
  50. data/test/base_test.rb +230 -0
  51. data/test/formtastic_test.rb +152 -0
  52. data/test/module_attributes_test.rb +52 -0
  53. data/test/mongo_mapper_test.rb +83 -0
  54. data/test/mongoid_test.rb +164 -0
  55. data/test/multiple_test.rb +65 -0
  56. data/test/predicates_test.rb +65 -0
  57. data/test/rails_admin_test.rb +27 -0
  58. data/test/sequel_test.rb +344 -0
  59. data/test/set_test.rb +166 -0
  60. data/test/simple_form_test.rb +156 -0
  61. data/test/support/mock_controller.rb +31 -0
  62. data/test/support/shared_enums.rb +43 -0
  63. data/test/support/view_test_helper.rb +46 -0
  64. data/test/test_helper.rb +53 -0
  65. data/test/value_test.rb +158 -0
  66. metadata +143 -0
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module ActiveRecordSupport
5
+ def enumerize(name, options={})
6
+ super
7
+
8
+ _enumerize_module.dependent_eval do
9
+ if self < ::ActiveRecord::Base
10
+ include InstanceMethods
11
+
12
+ const_get(:ActiveRecord_Relation).include(RelationMethods)
13
+ const_get(:ActiveRecord_AssociationRelation).include(RelationMethods)
14
+ const_get(:ActiveRecord_Associations_CollectionProxy).include(RelationMethods)
15
+
16
+ # Since Rails use `allocate` method on models and initializes them with `init_with` method.
17
+ # This way `initialize` method is not being called, but `after_initialize` callback always gets triggered.
18
+ after_initialize :_set_default_value_for_enumerized_attributes
19
+
20
+ # https://github.com/brainspec/enumerize/issues/111
21
+ require 'enumerize/hooks/uniqueness'
22
+
23
+ unless options[:multiple]
24
+ if ::ActiveRecord.version >= ::Gem::Version.new("7.0.0.alpha")
25
+ attribute(name) do |subtype|
26
+ Type.new(enumerized_attributes[name], subtype)
27
+ end
28
+ elsif ::ActiveRecord.version >= ::Gem::Version.new("6.1.0.alpha")
29
+ decorate_attribute_type(name.to_s) do |subtype|
30
+ Type.new(enumerized_attributes[name], subtype)
31
+ end
32
+ else
33
+ decorate_attribute_type(name, :enumerize) do |subtype|
34
+ Type.new(enumerized_attributes[name], subtype)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module InstanceMethods
43
+ # https://github.com/brainspec/enumerize/issues/74
44
+ def write_attribute(attr_name, value, *options)
45
+ if self.class.enumerized_attributes[attr_name]
46
+ _enumerized_values_for_validation[attr_name.to_s] = value
47
+ end
48
+
49
+ super
50
+ end
51
+
52
+ # Support multiple enumerized attributes
53
+ def becomes(klass)
54
+ became = super
55
+ klass.enumerized_attributes.each do |attr|
56
+ # Rescue when column associated to the enum does not exist.
57
+ begin
58
+ became.send("#{attr.name}=", send(attr.name))
59
+ rescue ActiveModel::MissingAttributeError
60
+ end
61
+ end
62
+
63
+ became
64
+ end
65
+
66
+ def reload(options = nil)
67
+ reloaded = super
68
+
69
+ reloaded.class.enumerized_attributes.each do |attr|
70
+ begin
71
+ # Checks first if the enumerized attribute is in ActiveRecord::Store
72
+ store_attr, _ = reloaded.class.stored_attributes.detect do |_store_attr, keys|
73
+ keys.include?(attr.name)
74
+ end
75
+
76
+ if store_attr.present?
77
+ reloaded.send("#{attr.name}=", reloaded.send(store_attr).with_indifferent_access[attr.name])
78
+ else
79
+ reloaded.send("#{attr.name}=", reloaded[attr.name])
80
+ end
81
+ rescue ActiveModel::MissingAttributeError
82
+ end
83
+ end
84
+
85
+ reloaded
86
+ end
87
+ end
88
+
89
+ module RelationMethods
90
+ def update_all(updates)
91
+ if updates.is_a?(Hash)
92
+ enumerized_attributes.each do |attr|
93
+ next if updates[attr.name].blank? || attr.kind_of?(Enumerize::Multiple)
94
+ enumerize_value = attr.find_value(updates[attr.name])
95
+ updates[attr.name] = enumerize_value && enumerize_value.value
96
+ end
97
+ end
98
+
99
+ super(updates)
100
+ end
101
+ end
102
+
103
+ class Type < ActiveRecord::Type::Value
104
+ delegate :type, to: :@subtype
105
+
106
+ def initialize(attr, subtype)
107
+ @attr = attr
108
+ @subtype = subtype
109
+ end
110
+
111
+ def serialize(value)
112
+ v = @attr.find_value(value)
113
+ (v && v.value) || value
114
+ end
115
+
116
+ alias type_cast_for_database serialize
117
+
118
+ def cast(value)
119
+ @attr.find_value(value)
120
+ end
121
+
122
+ alias type_cast_from_database cast
123
+
124
+ def as_json(options = nil)
125
+ {attr: @attr.name, subtype: @subtype}.as_json(options)
126
+ end
127
+
128
+ def encode_with(coder)
129
+ coder[:class_name] = @attr.klass.name
130
+ coder[:attr_name] = @attr.name
131
+ coder[:subtype] = @subtype
132
+ end
133
+
134
+ def init_with(coder)
135
+ initialize(
136
+ coder[:class_name].constantize.enumerized_attributes[coder[:attr_name]],
137
+ coder[:subtype]
138
+ )
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ class Attribute
5
+ attr_reader :klass, :name, :values, :default_value, :i18n_scope, :skip_validations_value
6
+
7
+ def initialize(klass, name, options={})
8
+ raise ArgumentError, ':in option is required' unless options[:in]
9
+ raise ArgumentError, ':scope option does not work with option :multiple' if options[:multiple] && options[:scope]
10
+
11
+ extend Multiple if options[:multiple]
12
+
13
+ @klass = klass
14
+ @name = name.to_sym
15
+
16
+ if options[:i18n_scope]
17
+ raise ArgumentError, ':i18n_scope option accepts only String or Array of strings' unless Array(options[:i18n_scope]).all? { |s| s.is_a?(String) }
18
+ @i18n_scope = options[:i18n_scope]
19
+ end
20
+
21
+ value_class = options.fetch(:value_class, Value)
22
+ @values = Array(options[:in]).map { |v| value_class.new(self, *v).freeze }
23
+
24
+ @value_hash = Hash[@values.map { |v| [v.value.to_s, v] }]
25
+ @value_hash.merge! Hash[@values.map { |v| [v.to_s, v] }]
26
+
27
+ if options[:default]
28
+ @default_value = find_default_value(options[:default])
29
+ raise ArgumentError, 'invalid default value' unless @default_value
30
+ end
31
+
32
+ @skip_validations_value = options.fetch(:skip_validations, false)
33
+ end
34
+
35
+ def find_default_value(value)
36
+ if value.respond_to?(:call)
37
+ value
38
+ else
39
+ find_value(value)
40
+ end
41
+ end
42
+
43
+ def find_value(value)
44
+ @value_hash[value.to_s] unless value.nil?
45
+ end
46
+
47
+ def find_values(*values)
48
+ values.map { |value| find_value(value) }.compact
49
+ end
50
+
51
+ def each_value
52
+ values.each { |value| yield value }
53
+ end
54
+
55
+ def i18n_scopes
56
+ @i18n_scopes ||= if i18n_scope
57
+ Array(i18n_scope)
58
+ elsif @klass.respond_to?(:model_name)
59
+ ["enumerize.#{@klass.model_name.i18n_key}.#{name}"]
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def options(options = {})
66
+ values = if options.empty?
67
+ @values
68
+ else
69
+ raise ArgumentError, 'Options cannot have both :only and :except' if options[:only] && options[:except]
70
+
71
+ only = Array(options[:only]).map(&:to_s)
72
+ except = Array(options[:except]).map(&:to_s)
73
+
74
+ @values.reject do |value|
75
+ if options[:only]
76
+ !only.include?(value)
77
+ elsif options[:except]
78
+ except.include?(value)
79
+ end
80
+ end
81
+ end
82
+
83
+ values.map { |v| [v.text, v.to_s] }
84
+ end
85
+
86
+ def respond_to_missing?(method, include_private=false)
87
+ @value_hash.include?(method.to_s) || super
88
+ end
89
+
90
+ def define_methods!(mod)
91
+ mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
92
+ def #{name}
93
+ if defined?(super)
94
+ self.class.enumerized_attributes[:#{name}].find_value(super)
95
+ elsif respond_to?(:read_attribute)
96
+ self.class.enumerized_attributes[:#{name}].find_value(read_attribute(:#{name}))
97
+ else
98
+ if defined?(@#{name})
99
+ self.class.enumerized_attributes[:#{name}].find_value(@#{name})
100
+ else
101
+ @#{name} = nil
102
+ end
103
+ end
104
+ end
105
+
106
+ def #{name}=(new_value)
107
+ allowed_value_or_nil = self.class.enumerized_attributes[:#{name}].find_value(new_value)
108
+ allowed_value_or_nil = allowed_value_or_nil.value unless allowed_value_or_nil.nil?
109
+
110
+ if defined?(super)
111
+ super allowed_value_or_nil
112
+ elsif respond_to?(:write_attribute, true)
113
+ write_attribute '#{name}', allowed_value_or_nil
114
+ else
115
+ @#{name} = allowed_value_or_nil
116
+ end
117
+
118
+ _enumerized_values_for_validation['#{name}'] = new_value.nil? ? nil : new_value.to_s
119
+
120
+ allowed_value_or_nil
121
+ end
122
+
123
+ def #{name}_text
124
+ self.#{name} && self.#{name}.text
125
+ end
126
+
127
+ def #{name}_value
128
+ self.#{name} && self.#{name}.value
129
+ end
130
+ RUBY
131
+ end
132
+
133
+ private
134
+
135
+ def method_missing(method)
136
+ if @value_hash.include?(method.to_s)
137
+ find_value(method)
138
+ else
139
+ super
140
+ end
141
+ end
142
+ end
143
+
144
+ module Multiple
145
+ def find_default_value(value)
146
+ if value.respond_to?(:call)
147
+ value
148
+ else
149
+ find_values(*value)
150
+ end
151
+ end
152
+
153
+ def define_methods!(mod)
154
+ mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
155
+ def #{name}
156
+ unless defined?(@_#{name}_enumerized_set)
157
+ if defined?(super)
158
+ self.#{name} = super
159
+ elsif respond_to?(:read_attribute)
160
+ self.#{name} = read_attribute(:#{name})
161
+ else
162
+ if defined?(@#{name})
163
+ self.#{name} = @#{name}
164
+ else
165
+ self.#{name} = []
166
+ end
167
+ end
168
+ end
169
+
170
+ @_#{name}_enumerized_set
171
+ end
172
+
173
+ def #{name}=(values)
174
+ @_#{name}_enumerized_set = Enumerize::Set.new(self, self.class.enumerized_attributes[:#{name}], values)
175
+ raw_values = self.#{name}.values.map(&:value)
176
+
177
+ if defined?(super)
178
+ super raw_values
179
+ elsif respond_to?(:write_attribute, true)
180
+ write_attribute '#{name}', raw_values
181
+ else
182
+ @#{name} = raw_values
183
+ end
184
+
185
+ _enumerized_values_for_validation['#{name}'] = values.respond_to?(:map) ? values.reject(&:blank?).map(&:to_s) : values
186
+
187
+ self.#{name}
188
+ end
189
+ RUBY
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ class AttributeMap
5
+ attr_reader :attributes
6
+
7
+ def initialize
8
+ @attributes = {}
9
+ @dependants = []
10
+ end
11
+
12
+ def [](name)
13
+ @attributes[name.to_s]
14
+ end
15
+
16
+ def <<(attr)
17
+ @attributes[attr.name.to_s] = attr
18
+ @dependants.each do |dependant|
19
+ dependant << attr
20
+ end
21
+ end
22
+
23
+ def each
24
+ @attributes.each_pair do |_name, attr|
25
+ yield attr
26
+ end
27
+ end
28
+
29
+ def empty?
30
+ @attributes.empty?
31
+ end
32
+
33
+ def add_dependant(dependant)
34
+ @dependants << dependant
35
+ each do |attr|
36
+ dependant << attr
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Base
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.singleton_class.prepend ClassMethods::Hook
8
+
9
+ if base.respond_to?(:validate)
10
+ base.validate :_validate_enumerized_attributes
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def enumerize(name, options={})
16
+ attr = Attribute.new(self, name, options)
17
+ enumerized_attributes << attr
18
+
19
+ unless methods.include?(attr.name)
20
+ _enumerize_module._class_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
21
+ def #{attr.name}
22
+ enumerized_attributes[:#{attr.name}]
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ attr.define_methods!(_enumerize_module)
28
+ end
29
+
30
+ def enumerized_attributes
31
+ @enumerized_attributes ||= AttributeMap.new
32
+ end
33
+
34
+ module Hook
35
+ def inherited(subclass)
36
+ enumerized_attributes.add_dependant subclass.enumerized_attributes
37
+ super subclass
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def _enumerize_module
44
+ @_enumerize_module ||= begin
45
+ mod = Module.new
46
+ include mod
47
+ mod
48
+ end
49
+ end
50
+ end
51
+
52
+ def initialize(*args, **kwargs)
53
+ super
54
+ _set_default_value_for_enumerized_attributes
55
+ end
56
+
57
+ def read_attribute_for_validation(key)
58
+ key = key.to_s
59
+
60
+ if _enumerized_values_for_validation.has_key?(key)
61
+ _enumerized_values_for_validation[key]
62
+ elsif defined?(super)
63
+ super
64
+ else
65
+ send(key)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def _enumerized_values_for_validation
72
+ @_enumerized_values_for_validation ||= {}
73
+ end
74
+
75
+ def _validate_enumerized_attributes
76
+ self.class.enumerized_attributes.each do |attr|
77
+ skip_validations = Utils.call_if_callable(attr.skip_validations_value, self)
78
+ next if skip_validations
79
+
80
+ value = read_attribute_for_validation(attr.name)
81
+ next if value.blank?
82
+
83
+ if attr.kind_of? Multiple
84
+ errors.add attr.name unless value.respond_to?(:all?) && value.all? { |v| v.blank? || attr.find_value(v) }
85
+ else
86
+ errors.add attr.name, :inclusion unless attr.find_value(value)
87
+ end
88
+ end
89
+ end
90
+
91
+ def _set_default_value_for_enumerized_attributes
92
+ self.class.enumerized_attributes.each do |attr|
93
+ next if attr.default_value.nil?
94
+ begin
95
+ if respond_to?(attr.name)
96
+ attr_value = public_send(attr.name)
97
+ else
98
+ next
99
+ end
100
+
101
+ value_for_validation = _enumerized_values_for_validation[attr.name.to_s]
102
+
103
+ if (!attr_value || attr_value.empty?) && (!value_for_validation || value_for_validation.empty?)
104
+ value = Utils.call_if_callable(attr.default_value, self)
105
+ public_send("#{attr.name}=", value)
106
+ end
107
+ rescue ActiveModel::MissingAttributeError
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Enumerize
6
+ module Hooks
7
+ module FormtasticFormBuilderExtension
8
+
9
+ def input(method, options={})
10
+ enumerized_object = convert_to_model(object)
11
+ klass = enumerized_object.class
12
+
13
+ if klass.respond_to?(:enumerized_attributes) && (attr = klass.enumerized_attributes[method])
14
+ options[:collection] ||= attr.options
15
+
16
+ if attr.kind_of?(Enumerize::Multiple) && options[:as] != :check_boxes
17
+ options[:input_html] = options.fetch(:input_html, {}).merge(:multiple => true)
18
+ end
19
+ end
20
+
21
+ super(method, options)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ ::Formtastic::FormBuilder.send :prepend, Enumerize::Hooks::FormtasticFormBuilderExtension
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Hooks
5
+ module SequelDataset
6
+ def literal_append(sql, v)
7
+ if v.is_a?(Enumerize::Value)
8
+ super(sql, v.value)
9
+ else
10
+ super(sql, v)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ ::Sequel::Dataset.send :prepend, Enumerize::Hooks::SequelDataset
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Enumerize
6
+ module Hooks
7
+ module SimpleFormBuilderExtension
8
+
9
+ def input(attribute_name, options={}, &block)
10
+ add_input_options_for_enumerized_attribute(attribute_name, options)
11
+ super(attribute_name, options, &block)
12
+ end
13
+
14
+ def input_field(attribute_name, options={})
15
+ add_input_options_for_enumerized_attribute(attribute_name, options)
16
+ super(attribute_name, options)
17
+ end
18
+
19
+ private
20
+
21
+ def add_input_options_for_enumerized_attribute(attribute_name, options)
22
+ enumerized_object = convert_to_model(object)
23
+ klass = enumerized_object.class
24
+
25
+ if klass.respond_to?(:enumerized_attributes) && (attr = klass.enumerized_attributes[attribute_name])
26
+ options[:collection] ||= attr.options
27
+
28
+ if attr.kind_of?(Enumerize::Multiple) && options[:as] != :check_boxes
29
+ options[:input_html] = options.fetch(:input_html, {}).merge(:multiple => true)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ ::SimpleForm::FormBuilder.send :prepend, Enumerize::Hooks::SimpleFormBuilderExtension
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Enumerize
6
+ module Hooks
7
+ module UniquenessValidator
8
+
9
+ def validate_each(record, name, value)
10
+ klass = record.to_model.class
11
+
12
+ if klass.respond_to?(:enumerized_attributes) && (attr = klass.enumerized_attributes[name])
13
+ value = attr.find_value(value).try(:value)
14
+ end
15
+
16
+ super(record, name, value)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ::ActiveRecord::Validations::UniquenessValidator.send :prepend, Enumerize::Hooks::UniquenessValidator
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Integrations
5
+ module RailsAdmin
6
+
7
+ def enumerize(name, options={})
8
+ super
9
+
10
+ _enumerize_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
11
+ def #{name}_enum
12
+ self.class.enumerized_attributes[:#{name}].options
13
+ end
14
+ RUBY
15
+ end
16
+ end
17
+ end
18
+ end