enumerize 0.3.0 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -1
  3. data/.rspec +2 -0
  4. data/.travis.yml +38 -11
  5. data/CHANGELOG.md +258 -0
  6. data/Gemfile +5 -11
  7. data/Gemfile.global +20 -0
  8. data/Gemfile.mongo_mapper +7 -0
  9. data/Gemfile.rails42 +7 -0
  10. data/Gemfile.rails50 +7 -0
  11. data/Gemfile.rails52 +7 -0
  12. data/README.md +405 -17
  13. data/Rakefile +7 -1
  14. data/enumerize.gemspec +6 -3
  15. data/lib/enumerize/activemodel.rb +47 -0
  16. data/lib/enumerize/activerecord.rb +127 -2
  17. data/lib/enumerize/attribute.rb +167 -7
  18. data/lib/enumerize/attribute_map.rb +4 -0
  19. data/lib/enumerize/base.rb +60 -61
  20. data/lib/enumerize/hooks/formtastic.rb +11 -12
  21. data/lib/enumerize/hooks/sequel_dataset.rb +17 -0
  22. data/lib/enumerize/hooks/simple_form.rb +21 -8
  23. data/lib/enumerize/hooks/uniqueness.rb +22 -0
  24. data/lib/enumerize/integrations/rails_admin.rb +18 -0
  25. data/lib/enumerize/integrations/rspec/matcher.rb +161 -0
  26. data/lib/enumerize/integrations/rspec.rb +19 -0
  27. data/lib/enumerize/module.rb +33 -0
  28. data/lib/enumerize/module_attributes.rb +3 -2
  29. data/lib/enumerize/mongoid.rb +29 -0
  30. data/lib/enumerize/predicatable.rb +23 -0
  31. data/lib/enumerize/predicates.rb +76 -0
  32. data/lib/enumerize/scope/activerecord.rb +49 -0
  33. data/lib/enumerize/scope/mongoid.rb +46 -0
  34. data/lib/enumerize/scope/sequel.rb +52 -0
  35. data/lib/enumerize/sequel.rb +62 -0
  36. data/lib/enumerize/set.rb +81 -0
  37. data/lib/enumerize/utils.rb +12 -0
  38. data/lib/enumerize/value.rb +19 -46
  39. data/lib/enumerize/version.rb +3 -1
  40. data/lib/enumerize.rb +56 -4
  41. data/lib/sequel/plugins/enumerize.rb +18 -0
  42. data/spec/enumerize/integrations/rspec/matcher_spec.rb +260 -0
  43. data/spec/spec_helper.rb +30 -0
  44. data/test/activemodel_test.rb +114 -0
  45. data/test/activerecord_test.rb +542 -8
  46. data/test/attribute_map_test.rb +2 -0
  47. data/test/attribute_test.rb +102 -4
  48. data/test/base_test.rb +61 -39
  49. data/test/formtastic_test.rb +102 -17
  50. data/test/module_attributes_test.rb +25 -2
  51. data/test/mongo_mapper_test.rb +84 -0
  52. data/test/mongoid_test.rb +98 -7
  53. data/test/multiple_test.rb +59 -0
  54. data/test/predicates_test.rb +65 -0
  55. data/test/rails_admin_test.rb +27 -0
  56. data/test/sequel_test.rb +341 -0
  57. data/test/set_test.rb +166 -0
  58. data/test/simple_form_test.rb +110 -4
  59. data/test/support/mock_controller.rb +11 -1
  60. data/test/support/shared_enums.rb +43 -0
  61. data/test/support/view_test_helper.rb +6 -0
  62. data/test/test_helper.rb +25 -6
  63. data/test/value_test.rb +102 -13
  64. metadata +62 -28
  65. data/gemfiles/Gemfile-ar-3.1.x +0 -13
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Integrations
5
+ module RSpec
6
+ class Matcher
7
+
8
+ def initialize(expected_attr)
9
+ self.expected_attr = expected_attr
10
+ end
11
+
12
+ def in(*expected_values)
13
+ self.expected_values = expected_values.flatten
14
+ self
15
+ end
16
+
17
+ def with_default(expected_default)
18
+ self.expected_default = expected_default.to_s
19
+ self
20
+ end
21
+
22
+ def with_i18n_scope(expected_i18n_scope)
23
+ self.expected_i18n_scope = expected_i18n_scope
24
+ self
25
+ end
26
+
27
+ def with_predicates(expected_predicates)
28
+ self.expected_predicates = expected_predicates
29
+ self
30
+ end
31
+
32
+ def with_multiple(expected_multiple)
33
+ self.expected_multiple = expected_multiple
34
+ self
35
+ end
36
+
37
+ def with_scope(expected_scope)
38
+ self.expected_scope = expected_scope
39
+ self
40
+ end
41
+
42
+ def failure_message
43
+ "Expected #{expectation}"
44
+ end
45
+
46
+ def failure_message_when_negated
47
+ "Did not expect #{expectation}"
48
+ end
49
+
50
+ def description
51
+ description = "define enumerize :#{expected_attr}"
52
+ description += " in: #{quote_values(expected_values)}" if expected_values
53
+ description += " with #{expected_default.inspect} as default value" if expected_default
54
+ description += " i18n_scope: #{expected_i18n_scope.inspect}" if expected_i18n_scope
55
+ description += " predicates: #{expected_predicates.inspect}" if expected_predicates
56
+ description += " multiple: #{expected_multiple.inspect}" if expected_multiple
57
+ description += " scope: #{expected_scope.inspect}" if expected_scope
58
+
59
+ description
60
+ end
61
+
62
+ def matches?(subject)
63
+ self.subject = subject
64
+ matches = true
65
+
66
+ matches &= matches_attribute?
67
+ matches &= matches_values? if expected_values
68
+ matches &= matches_default_value? if expected_default
69
+ matches &= matches_i18n_scope? if expected_i18n_scope
70
+ matches &= matches_predicates? if expected_predicates
71
+ matches &= matches_multiple? if expected_multiple
72
+ matches &= matches_scope? if expected_scope
73
+
74
+ matches
75
+ end
76
+
77
+ private
78
+ attr_accessor :expected_attr, :expected_values, :subject, :expected_default,
79
+ :expected_i18n_scope, :expected_predicates, :expected_multiple,
80
+ :expected_scope
81
+
82
+ def expectation
83
+ "#{subject.class.name} to #{description}"
84
+ end
85
+
86
+ def matches_attribute?
87
+ attributes.present?
88
+ end
89
+
90
+ def matches_values?
91
+ matches_array_values? || matches_hash_values?
92
+ end
93
+
94
+ def matches_array_values?
95
+ sorted_values == enumerized_values
96
+ end
97
+
98
+ def matches_hash_values?
99
+ return unless expected_values.first.is_a?(Hash)
100
+ expected_values.first.all? { |k, v| enumerized_value_hash[k.to_s] == v; }
101
+ end
102
+
103
+ def matches_default_value?
104
+ expected_default == enumerized_default
105
+ end
106
+
107
+ def matches_i18n_scope?
108
+ attributes.i18n_scope == expected_i18n_scope
109
+ end
110
+
111
+ def matches_predicates?
112
+ if expected_predicates.is_a?(TrueClass)
113
+ subject.respond_to?("#{sorted_values.first}?")
114
+ else
115
+ subject.respond_to?("#{expected_attr}_#{attributes.values.first}?")
116
+ end
117
+ end
118
+
119
+ def matches_multiple?
120
+ subject.public_send(expected_attr).is_a?(Enumerize::Set)
121
+ end
122
+
123
+ def matches_scope?
124
+ if expected_scope.is_a?(TrueClass)
125
+ subject_class.respond_to?("with_#{expected_attr}")
126
+ else
127
+ subject_class.respond_to?(expected_scope[:scope])
128
+ end
129
+ end
130
+
131
+ def sorted_values
132
+ @sorted_values ||=expected_values.map(&:to_s).sort
133
+ end
134
+
135
+ def enumerized_values
136
+ @enumerized_values ||= attributes.values.sort
137
+ end
138
+
139
+ def enumerized_default
140
+ @enumerized_default ||= attributes.default_value
141
+ end
142
+
143
+ def enumerized_value_hash
144
+ @enumerized_value_hash ||= attributes.instance_variable_get('@value_hash')
145
+ end
146
+
147
+ def attributes
148
+ subject_class.enumerized_attributes.attributes[expected_attr.to_s]
149
+ end
150
+
151
+ def subject_class
152
+ @subject_class ||= subject.class
153
+ end
154
+
155
+ def quote_values(values)
156
+ sorted_values.map(&:inspect).join(', ')
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'enumerize/integrations/rspec/matcher'
4
+
5
+ module Enumerize
6
+ module Integrations
7
+ module RSpec
8
+ def enumerize(attr)
9
+ ::Enumerize::Integrations::RSpec::Matcher.new(attr)
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ module RSpec
16
+ module Matchers
17
+ include Enumerize::Integrations::RSpec
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ class Module < ::Module
5
+ attr_reader :_class_methods
6
+
7
+ def initialize
8
+ super
9
+
10
+ @_class_methods = ::Module.new
11
+ @_dependents = []
12
+ @_dependent_evals = []
13
+ end
14
+
15
+ def included(klass)
16
+ klass.extend _class_methods
17
+
18
+ @_dependent_evals.each do |block|
19
+ klass.instance_eval(&block)
20
+ end
21
+
22
+ @_dependents << klass
23
+ end
24
+
25
+ def dependent_eval(&block)
26
+ @_dependents.each do |klass|
27
+ klass.instance_eval(&block)
28
+ end
29
+
30
+ @_dependent_evals << block
31
+ end
32
+ end
33
+ end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Enumerize
2
4
  module ModuleAttributes
3
5
  def included(base)
4
- base.send :include, Enumerize
6
+ base.extend Enumerize
5
7
  base.send :include, _enumerize_module
6
- base.extend _enumerize_module._class_methods
7
8
  enumerized_attributes.add_dependant base.enumerized_attributes
8
9
  super
9
10
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module MongoidSupport
5
+ def enumerize(name, options={})
6
+ super
7
+
8
+ _enumerize_module.dependent_eval do
9
+ if self < ::Mongoid::Document
10
+ include InstanceMethods
11
+
12
+ after_initialize :_set_default_value_for_enumerized_attributes
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def reload
19
+ reloaded = super
20
+
21
+ reloaded.class.enumerized_attributes.each do |attr|
22
+ reloaded.send("#{attr.name}=", reloaded[attr.name])
23
+ end
24
+
25
+ reloaded
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Predicatable
5
+ def respond_to_missing?(method, include_private=false)
6
+ predicate_method?(method) || super
7
+ end
8
+
9
+ private
10
+
11
+ def method_missing(method, *args, &block)
12
+ if predicate_method?(method)
13
+ predicate_call(method[0..-2], *args, &block)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def predicate_method?(method)
20
+ method[-1] == '?' && @attr.values.include?(method[0..-2])
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module/delegation'
4
+
5
+ module Enumerize
6
+ # Predicate methods.
7
+ #
8
+ # Basic usage:
9
+ #
10
+ # class User
11
+ # extend Enumerize
12
+ # enumerize :sex, in: %w(male female), predicates: true
13
+ # end
14
+ #
15
+ # user = User.new
16
+ #
17
+ # user.male? # => false
18
+ # user.female? # => false
19
+ #
20
+ # user.sex = 'male'
21
+ #
22
+ # user.male? # => true
23
+ # user.female? # => false
24
+ #
25
+ # Using prefix:
26
+ #
27
+ # class User
28
+ # extend Enumerize
29
+ # enumerize :sex, in: %w(male female), predicates: { prefix: true }
30
+ # end
31
+ #
32
+ # user = User.new
33
+ # user.sex = 'female'
34
+ # user.sex_female? # => true
35
+ #
36
+ # Use <tt>only</tt> and <tt>except</tt> options to specify what values create
37
+ # predicate methods for.
38
+ module Predicates
39
+ def enumerize(name, options={})
40
+ super
41
+
42
+ if options[:predicates]
43
+ Builder.new(enumerized_attributes[name], options[:predicates]).build(_enumerize_module)
44
+ end
45
+ end
46
+
47
+ class Builder
48
+ def initialize(attr, options)
49
+ @attr = attr
50
+ @options = options.is_a?(Hash) ? options : {}
51
+ end
52
+
53
+ def values
54
+ values = @attr.values
55
+
56
+ if @options[:only]
57
+ values &= Array(@options[:only]).map(&:to_s)
58
+ end
59
+
60
+ if @options[:except]
61
+ values -= Array(@options[:except]).map(&:to_s)
62
+ end
63
+
64
+ values
65
+ end
66
+
67
+ def names
68
+ values.map { |v| "#{v.tr('-', '_')}?" }
69
+ end
70
+
71
+ def build(klass)
72
+ klass.delegate(*names, to: @attr.name, prefix: @options[:prefix], allow_nil: true)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Scope
5
+ module ActiveRecord
6
+ def enumerize(name, options={})
7
+ super
8
+
9
+ _enumerize_module.dependent_eval do
10
+ if self < ::ActiveRecord::Base
11
+ if options[:scope]
12
+ _define_activerecord_scope_methods!(name, options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def _define_activerecord_scope_methods!(name, options)
21
+ return _define_activerecord_shallow_scopes!(name) if options[:scope] == :shallow
22
+
23
+ scope_name = options[:scope] == true ? "with_#{name}" : options[:scope]
24
+
25
+ define_singleton_method scope_name do |*values|
26
+ values = enumerized_attributes[name].find_values(*values).map(&:value)
27
+ values = values.first if values.size == 1
28
+
29
+ where(name => values)
30
+ end
31
+
32
+ if options[:scope] == true
33
+ define_singleton_method "without_#{name}" do |*values|
34
+ values = enumerized_attributes[name].find_values(*values).map(&:value)
35
+ where(arel_table[name].not_in(values))
36
+ end
37
+ end
38
+ end
39
+
40
+ def _define_activerecord_shallow_scopes!(attribute_name)
41
+ enumerized_attributes[attribute_name].each_value do |value_obj|
42
+ define_singleton_method(value_obj) do
43
+ where(attribute_name => value_obj.value)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Scope
5
+ module Mongoid
6
+ def enumerize(name, options={})
7
+ super
8
+
9
+ _enumerize_module.dependent_eval do
10
+ if self < ::Mongoid::Document
11
+ if options[:scope]
12
+ _define_mongoid_scope_methods!(name, options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def _define_mongoid_scope_methods!(name, options)
21
+ return _define_mongoid_shallow_scopes!(name) if options[:scope] == :shallow
22
+ scope_name = options[:scope] == true ? "with_#{name}" : options[:scope]
23
+
24
+ define_singleton_method scope_name do |*values|
25
+ values = enumerized_attributes[name].find_values(*values).map(&:value)
26
+ self.in(name => values)
27
+ end
28
+
29
+ if options[:scope] == true
30
+ define_singleton_method "without_#{name}" do |*values|
31
+ values = enumerized_attributes[name].find_values(*values).map(&:value)
32
+ not_in(name => values)
33
+ end
34
+ end
35
+ end
36
+
37
+ def _define_mongoid_shallow_scopes!(attribute_name)
38
+ enumerized_attributes[attribute_name].each_value do |value_obj|
39
+ define_singleton_method(value_obj) do
40
+ self.in(attribute_name => value_obj.value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Scope
5
+ module Sequel
6
+ def enumerize(name, options={})
7
+ super
8
+
9
+ _enumerize_module.dependent_eval do
10
+ if defined?(::Sequel::Model) && self < ::Sequel::Model
11
+ if options[:scope]
12
+ _define_sequel_scope_methods!(name, options)
13
+ end
14
+
15
+ require 'enumerize/hooks/sequel_dataset'
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def _define_sequel_scope_methods!(name, options)
23
+ return _define_sequel_shallow_scopes!(name) if options[:scope] == :shallow
24
+
25
+ klass = self
26
+ scope_name = options[:scope] == true ? "with_#{name}" : options[:scope]
27
+
28
+ def_dataset_method scope_name do |*values|
29
+ values = values.map { |value| klass.enumerized_attributes[name].find_value(value).value }
30
+ values = values.first if values.size == 1
31
+
32
+ where(name => values)
33
+ end
34
+
35
+ if options[:scope] == true
36
+ def_dataset_method "without_#{name}" do |*values|
37
+ values = values.map { |value| klass.enumerized_attributes[name].find_value(value).value }
38
+ exclude(name => values)
39
+ end
40
+ end
41
+ end
42
+
43
+ def _define_sequel_shallow_scopes!(attribute_name)
44
+ enumerized_attributes[attribute_name].each_value do |value_obj|
45
+ def_dataset_method(value_obj) do
46
+ where(attribute_name => value_obj.value.to_s)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module SequelSupport
5
+ def enumerize(name, options={})
6
+ super
7
+
8
+ _enumerize_module.dependent_eval do
9
+ if defined?(::Sequel::Model) && self < ::Sequel::Model
10
+ include InstanceMethods
11
+
12
+ require 'enumerize/hooks/sequel_dataset'
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ module InstanceMethods
20
+ def validate
21
+ super
22
+
23
+ self.class.enumerized_attributes.each do |attr|
24
+ skip_validations = Utils.call_if_callable(attr.skip_validations_value, self)
25
+ next if skip_validations
26
+
27
+ value = read_attribute_for_validation(attr.name)
28
+ next if value.blank?
29
+
30
+ if attr.kind_of? Multiple
31
+ errors.add attr.name, "is invalid" unless value.respond_to?(:all?) && value.all? { |v| v.blank? || attr.find_value(v) }
32
+ else
33
+ errors.add attr.name, "is not included in the list" unless attr.find_value(value)
34
+ end
35
+ end
36
+ end
37
+
38
+ def _set_default_value_for_enumerized_attributes
39
+ _enumerized_values_for_validation.delete_if do |k, v|
40
+ v.nil?
41
+ end
42
+
43
+ if defined?(Sequel::Plugins::Serialization::InstanceMethods)
44
+ modules = self.class.ancestors
45
+ plugin_idx = modules.index(Sequel::Plugins::Serialization::InstanceMethods)
46
+
47
+ if plugin_idx && plugin_idx < modules.index(Enumerize::SequelSupport::InstanceMethods)
48
+ abort "ERROR: You need to enable the Sequel serialization plugin before calling any enumerize methods on a model."
49
+ end
50
+
51
+ plugin_idx = modules.index(Sequel::Plugins::ValidationHelpers::InstanceMethods)
52
+
53
+ if plugin_idx && plugin_idx < modules.index(Enumerize::SequelSupport::InstanceMethods)
54
+ abort "ERROR: You need to enable the Sequel validation_helpers plugin before calling any enumerize methods on a model."
55
+ end
56
+ end
57
+
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module/delegation'
4
+
5
+ module Enumerize
6
+ class Set
7
+ include Enumerable
8
+ include Predicatable
9
+
10
+ attr_reader :values
11
+
12
+ def initialize(obj, attr, values)
13
+ @obj = obj
14
+ @attr = attr
15
+ @values = []
16
+
17
+ if values.respond_to?(:each)
18
+ values.each do |input|
19
+ value = @attr.find_value(input)
20
+
21
+ if value && !@values.include?(value)
22
+ @values << value
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def <<(value)
29
+ @values << value
30
+ mutate!
31
+ end
32
+
33
+ alias_method :push, :<<
34
+
35
+ delegate :each, :empty?, :size, to: :values
36
+
37
+ def to_ary
38
+ @values.to_a
39
+ end
40
+
41
+ def texts
42
+ @values.map(&:text)
43
+ end
44
+
45
+ delegate :join, to: :to_ary
46
+
47
+ def ==(other)
48
+ return false unless other.respond_to?(:each)
49
+ other.size == size && other.all? { |v| @values.include?(@attr.find_value(v)) }
50
+ end
51
+
52
+ alias_method :eql?, :==
53
+
54
+ def include?(value)
55
+ @values.include?(@attr.find_value(value))
56
+ end
57
+
58
+ def delete(value)
59
+ @values.delete(@attr.find_value(value))
60
+ mutate!
61
+ end
62
+
63
+ def inspect
64
+ "#<Enumerize::Set {#{join(', ')}}>"
65
+ end
66
+
67
+ def encode_with(coder)
68
+ coder.represent_object(Array, @values)
69
+ end
70
+
71
+ private
72
+
73
+ def predicate_call(value)
74
+ include?(value)
75
+ end
76
+
77
+ def mutate!
78
+ @values = @obj.public_send("#{@attr.name}=", @values).values
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumerize
4
+ module Utils
5
+ class << self
6
+ def call_if_callable(value, param = nil)
7
+ return value unless value.respond_to?(:call)
8
+ value.arity == 0 ? value.call : value.call(param)
9
+ end
10
+ end
11
+ end
12
+ end