activemodel-interdependence 0.0.1

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +97 -0
  3. data/.rspec +4 -0
  4. data/.rubocop.yml +208 -0
  5. data/.rubocop_todo.yml +34 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +83 -0
  8. data/Gemfile.lock +336 -0
  9. data/Guardfile +68 -0
  10. data/LICENSE +21 -0
  11. data/README.md +76 -0
  12. data/Rakefile +95 -0
  13. data/activemodel-interdependence.gemspec +34 -0
  14. data/circle.yml +7 -0
  15. data/config/devtools.yml +2 -0
  16. data/config/flay.yml +3 -0
  17. data/config/flog.yml +2 -0
  18. data/config/mutant.yml +9 -0
  19. data/config/reek.yml +120 -0
  20. data/config/rubocop.yml +1 -0
  21. data/config/yardstick.yml +2 -0
  22. data/lib/activemodel/interdependence.rb +12 -0
  23. data/lib/activemodel/model/interdependence.rb +97 -0
  24. data/lib/activemodel/validator/interdependence.rb +107 -0
  25. data/lib/interdependence.rb +87 -0
  26. data/lib/interdependence/activemodel/class_methods.rb +50 -0
  27. data/lib/interdependence/activemodel/validates_with.rb +128 -0
  28. data/lib/interdependence/common_mixin.rb +84 -0
  29. data/lib/interdependence/dependency/base.rb +177 -0
  30. data/lib/interdependence/dependency/model.rb +61 -0
  31. data/lib/interdependence/dependency/validator.rb +43 -0
  32. data/lib/interdependence/dependency_resolver/base.rb +114 -0
  33. data/lib/interdependence/dependency_resolver/model.rb +76 -0
  34. data/lib/interdependence/dependency_resolver/validator.rb +34 -0
  35. data/lib/interdependence/dependency_set.rb +15 -0
  36. data/lib/interdependence/dependency_set_graph.rb +66 -0
  37. data/lib/interdependence/graph.rb +103 -0
  38. data/lib/interdependence/model.rb +70 -0
  39. data/lib/interdependence/model/validator.rb +99 -0
  40. data/lib/interdependence/observable_dependency_set_graph.rb +23 -0
  41. data/lib/interdependence/types.rb +199 -0
  42. data/lib/interdependence/validator.rb +67 -0
  43. data/lib/interdependence/validator/validator.rb +105 -0
  44. data/lib/interdependence/version.rb +3 -0
  45. metadata +213 -0
@@ -0,0 +1 @@
1
+ inherit_from: ../.rubocop.yml
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 100
@@ -0,0 +1,12 @@
1
+ require 'interdependence'
2
+ require 'activemodel/model/interdependence'
3
+ require 'activemodel/validator/interdependence'
4
+
5
+ ActiveModel::Model.include(ActiveModel::Model::Interdependence)
6
+ ActiveModel::Validator.extend(ActiveModel::Validator::Interdependence::ClassMethods)
7
+ ActiveModel::EachValidator.extend(ActiveModel::Validator::Interdependence::ClassMethods)
8
+
9
+ ActiveModel::Validations.constants.grep(/Validator\Z/).each do |name|
10
+ validator = ActiveModel::Validations.const_get(name)
11
+ validator.include(::Interdependence::Validator)
12
+ end
@@ -0,0 +1,97 @@
1
+ module ActiveModel
2
+ module Model
3
+ # Methods for monkey patching and extending ActiveModel::Model
4
+ #
5
+ module Interdependence
6
+ # Patch ActiveModel::Model directly
7
+ #
8
+ # @return [undefined]
9
+ #
10
+ # @api private
11
+ #
12
+ def self.included(base)
13
+ super
14
+ base.extend(ActiveModel::Validations::ClassMethods)
15
+ base.singleton_class.prepend(ClassMethods)
16
+ end
17
+
18
+ # Extend ActiveModel::Model modules
19
+ #
20
+ module ClassMethods
21
+ include ::Interdependence::ActiveModel::ClassMethods
22
+
23
+ # handle ActiveModel::Model includes
24
+ #
25
+ # @example usage is the same as active model
26
+ #
27
+ # class MyModel
28
+ # include ActiveModel::Model
29
+ # validates :foo, bar: true
30
+ # end
31
+ #
32
+ # @return [undefined]
33
+ #
34
+ # @api semipublic
35
+ #
36
+ def included(base)
37
+ super
38
+ base.include(::Interdependence::Model)
39
+ end
40
+
41
+ # Expose {#validates_with}'s supermethod
42
+ #
43
+ # @return [undefined]
44
+ #
45
+ # @api private
46
+ #
47
+ def active_model_validates_with(*args)
48
+ super_method(:validates_with, *args)
49
+ end
50
+
51
+ # Expose {#validate}'s supermethod
52
+ #
53
+ # @return [undefined]
54
+ #
55
+ # @api private
56
+ #
57
+ def active_model_validate(*args)
58
+ super_method(:validate, *args)
59
+ end
60
+
61
+ private
62
+
63
+ # Call super of the specified method
64
+ #
65
+ # @example super_method usage
66
+ # class Parent
67
+ # def bar(val)
68
+ # puts "parent! #{val}"
69
+ # end
70
+ # end
71
+ #
72
+ # class Child < Parent
73
+ # def bar(val)
74
+ # puts "child! #{val}"
75
+ # end
76
+ #
77
+ # def cool
78
+ # super_method(:bar, :hello)
79
+ # end
80
+ # end
81
+ #
82
+ # Child.new.cool # => prints "parent! hello"
83
+ #
84
+ # @param name [Symbol] method name
85
+ # @param *args [Array] arguments passed to super method
86
+ #
87
+ # @return [return of super method]
88
+ #
89
+ # @api private
90
+ #
91
+ def super_method(name, *args)
92
+ method(name).super_method.call(*args)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,107 @@
1
+ module ActiveModel
2
+ class Validator
3
+ # Methods for monkey patching and extending ActiveModel::Validator
4
+ #
5
+ module Interdependence
6
+ ACTIVE_MODEL_VALIDATORS = ActiveModel::EachValidator.descendants.select do |validator|
7
+ validator.name =~ /\AActiveModel::Validations::/
8
+ end
9
+
10
+ # Methods extracted from ActiveModel::Validations::ClassMethods
11
+ # that allow us to define validators on other validators
12
+ #
13
+ module CherryPickedClassMethods
14
+ CHERRY_PICK_METHODS = %i(
15
+ validates
16
+ validates!
17
+ _validates_default_keys
18
+ _parse_validates_options
19
+ )
20
+
21
+ # Cherry pick methods from ActiveModel::Validations so that
22
+ # validations can be registered on validators
23
+ CHERRY_PICK_METHODS.each do |method|
24
+ define_method(method) do |*args|
25
+ ActiveModel::Validations::ClassMethods
26
+ .instance_method(method)
27
+ .bind(self)
28
+ .call(*args)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Methods to extend ActiveModel::Validator
34
+ #
35
+ module ClassMethods
36
+ include ::Interdependence::ActiveModel::ClassMethods
37
+ include CherryPickedClassMethods
38
+
39
+ # Handle an ActiveModel validator inheritance
40
+ #
41
+ # @return [undefined]
42
+ #
43
+ # @api private
44
+ #
45
+ def inherited(base)
46
+ super
47
+ return if base == EachValidator
48
+ base.include(::Interdependence::Validator)
49
+ end
50
+
51
+ # Rewrite validator kinds before ActiveModel receives it
52
+ #
53
+ # Since we pluck the methods from ActiveModel, the inflection
54
+ # done in the method (`"#{key.to_s.camelize}Validator"`) does not
55
+ # resolve like it would in the `ActiveModel` namespace.
56
+ #
57
+ # @see http://git.io/vYp2k ActiveModel::Validations::ClassMethods#validates
58
+ #
59
+ # @return [undefined]
60
+ #
61
+ # @api private
62
+ #
63
+ def validates(*args, &blk)
64
+ options = args.extract_options!
65
+ new_options = options.each.with_object({}) do |(key, value), memo|
66
+ key = ensure_namespace(key) if key.is_a?(String) || key.is_a?(Symbol)
67
+
68
+ memo[key] = value
69
+ end
70
+
71
+ super(*args, new_options, &blk)
72
+ end
73
+
74
+ private
75
+
76
+ # Rewrite the namespace ActiveModel validator symbols
77
+ #
78
+ # @example when kind is from active_model
79
+ # ensure_namespace(:presence) # => :"active_model/validations/:presence"
80
+ #
81
+ # @example otherwise
82
+ # ensure_namespace(:custom) # => :custom
83
+ #
84
+ # @param arg [Symbol] kind
85
+ #
86
+ # @return [Symbol] rewritten kind
87
+ #
88
+ # @api private
89
+ #
90
+ def ensure_namespace(arg)
91
+ return arg unless active_model_validator_keys.include?(arg.to_sym)
92
+ :"active_model/validations/#{arg}"
93
+ end
94
+
95
+ # Symbol names from ActiveModel
96
+ #
97
+ # @return [Array<Symbol>] array of symbol kinds
98
+ #
99
+ # @api private
100
+ #
101
+ def active_model_validator_keys
102
+ @active_model_validator_keys ||= ACTIVE_MODEL_VALIDATORS.map(&:kind)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,87 @@
1
+ require 'tsort'
2
+ require 'set'
3
+ require 'observer'
4
+
5
+ require 'active_support'
6
+ require 'active_support/core_ext'
7
+ require 'active_model'
8
+ require 'virtus'
9
+ require 'abstract_type'
10
+ require 'adamantium'
11
+
12
+ require 'interdependence/activemodel/validates_with'
13
+ require 'interdependence/activemodel/class_methods'
14
+
15
+ require 'interdependence/dependency_set'
16
+ require 'interdependence/graph'
17
+ require 'interdependence/dependency_set_graph'
18
+ require 'interdependence/observable_dependency_set_graph'
19
+ require 'interdependence/common_mixin'
20
+ require 'interdependence/types'
21
+ require 'interdependence/dependency/base'
22
+ require 'interdependence/dependency/validator'
23
+ require 'interdependence/dependency/model'
24
+ require 'interdependence/model/validator'
25
+ require 'interdependence/validator/validator'
26
+ require 'interdependence/dependency_resolver/base'
27
+ require 'interdependence/dependency_resolver/model'
28
+ require 'interdependence/dependency_resolver/validator'
29
+ require 'interdependence/model'
30
+ require 'interdependence/validator'
31
+
32
+ # Specify that validations depend on the validation of other fields
33
+ #
34
+ # Lets classes that implement ActiveModel::Model or ActiveModel::Validator
35
+ # specify that they are dependent on other fields being valid. These specifications
36
+ # are translated into a dependency graph, sorted, and applied in order to a model.
37
+ # As a result, fields are only validated once the fields that they depend on are
38
+ # validated.
39
+ #
40
+ # @example usage
41
+ #
42
+ # class DayValidator < ActiveModel::EachValidator
43
+ # validates :month_field, inclusion: 1..12
44
+ # validates :year_field, inclusion: 0..2015
45
+ #
46
+ # def validate_each(record, attribute, value)
47
+ # month = dependency(record, :month_field)
48
+ # year = dependency(record, :year_field)
49
+ #
50
+ # return if (1..Time.days_in_month(month, year)).cover?(value)
51
+ # record.errors[attribute] << "is not valid for the month #{Date::MONTHNAMES[month]}"
52
+ # end
53
+ #
54
+ # def dependency(record, proxy_name)
55
+ # name = options.fetch(:dependencies, {}).fetch(proxy_name)
56
+ #
57
+ # record.send(name)
58
+ # end
59
+ # end
60
+ #
61
+ # class Birthday
62
+ # include ActiveModel::Model
63
+ # attr_accessor :day, :month, :year
64
+ #
65
+ # validates :day, day: {
66
+ # dependencies: {
67
+ # month_field: :month,
68
+ # year_field: :year
69
+ # }
70
+ # }
71
+ # end
72
+ #
73
+ # leap_day = Birthday.new(day: 29, month: 2, year: 2000)
74
+ # leap_day.valid? # => true
75
+ #
76
+ # not_a_leap_day = Birthday.new(day: 29, month: 2, year: 1999)
77
+ # not_a_leap_day.valid? # => false
78
+ # not_a_leap_day.errors.full_messages # => ["Day is not valid for the February"]
79
+ #
80
+ # bad_month = Birthday.new(day: 29, month: 0, year: 1999)
81
+ # bad_month.valid? # => false
82
+ # bad_month.errors.full_messages # => ["Month is not included in the list"]
83
+ #
84
+ # @api public
85
+ #
86
+ module Interdependence
87
+ end
@@ -0,0 +1,50 @@
1
+ module Interdependence
2
+ module ActiveModel
3
+ # Class methods prepended to ActiveModel's class methods
4
+ #
5
+ # Prepends methods on ActiveModel::Model and defines the methods
6
+ # on ActiveModel::Validator
7
+ #
8
+ module ClassMethods
9
+ # Monkey patch of `ActiveModel::Validations::ClassMethods#validates_with`
10
+ #
11
+ # Catches ActiveModel `#validates_with` calls (and {#validates})
12
+ # and registers the validator with {Interdependence}. {Interdependence} separately
13
+ # registers the validator later on using the original AM validates_with method.
14
+ #
15
+ # @see http://git.io/vYpWh ActiveModel #validates_with
16
+ # @see ActiveModel::Validations::ClassMethods#validates
17
+ #
18
+ # @return [undefined]
19
+ #
20
+ # @api semipublic
21
+ #
22
+ def validates_with(*args)
23
+ with = ValidatesWith.new(*args)
24
+ super unless with.attributes?
25
+
26
+ with.each do |(attribute, validator), options|
27
+ add_interdependent_validator(attribute, validator, options)
28
+ end
29
+ end
30
+
31
+ # Monkey patch of {ActiveModel::Validations::ClassMethods#validate}
32
+ #
33
+ # Catches ActiveModel {#validate} calls and then passes
34
+ # the arguments along to super. We track validate calls so that
35
+ # our dependency management doesn't drop the validate calls when
36
+ # dependencies are changed. Should behave the same as the ActiveModel method
37
+ #
38
+ # @see http://git.io/vYp4L ActiveModel #validate
39
+ #
40
+ # @return [undefined]
41
+ #
42
+ # @api semipublic
43
+ #
44
+ def validate(*args, &blk)
45
+ validate_calls << [args, blk] if args.first.instance_of?(Symbol)
46
+ super
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,128 @@
1
+ module Interdependence
2
+ module ActiveModel
3
+ # Handler for `validates_with` method calls intended for ActiveModel
4
+ #
5
+ # Makes it easy to iterate over attributes, options, and validators.
6
+ #
7
+ # @example when given attributes
8
+ # class FancyValidator < ActiveModel::EachValidator; end
9
+ #
10
+ # with = ValidatesWith.new(
11
+ # FancyValidator,
12
+ # thing: :yes,
13
+ # attributes: %i(foo bar)
14
+ # )
15
+ #
16
+ # with.attributes? # => true
17
+ # >> with.each # => #<Enumerator: ...>
18
+ #
19
+ # with.each.to_a # => [
20
+ # # [[:foo, FancyValidator], {:thing=>:yes}],
21
+ # # [[:bar, FancyValidator], {:thing=>:yes}]
22
+ # # ]
23
+ #
24
+ # @example when no attributes
25
+ # with = ValidatesWith.new(
26
+ # FancyValidator,
27
+ # thing: :yes,
28
+ # attributes: []
29
+ # )
30
+ #
31
+ # >> with.attributes? # => false
32
+ # >> with.each.to_a # => []
33
+ #
34
+ class ValidatesWith
35
+ # Handle arguments intended for {#validates_with}
36
+ #
37
+ # @return [undefined]
38
+ #
39
+ # @api private
40
+ #
41
+ def initialize(*args)
42
+ @validators = args
43
+ @options = @validators.extract_options!
44
+ end
45
+
46
+ # Iterate over fields and validators
47
+ #
48
+ # @example usage
49
+ # with = ValidatesWith.new(
50
+ # PresenceValidator,
51
+ # NumericalityValidator,
52
+ # thing: :yes,
53
+ # attributes: %i(foo bar)
54
+ # )
55
+ #
56
+ # with.each do |(attribute, validator), options|
57
+ # puts [attribute, validator.kind, options].inspect
58
+ # end
59
+ #
60
+ # #=> [:foo, :presence, {:thing=>:yes}]
61
+ # #=> [:foo, :numericality, {:thing=>:yes}]
62
+ # #=> [:bar, :presence, {:thing=>:yes}]
63
+ # #=> [:bar, :numericality, {:thing=>:yes}]
64
+ #
65
+ # @return [undefined]
66
+ #
67
+ # @api semipublic
68
+ #
69
+ def each(&blk)
70
+ attributes_with_validators
71
+ .each
72
+ .with_object(options, &blk)
73
+ end
74
+
75
+ # Any attributes present
76
+ #
77
+ # @return [boolean] if any attributes were given
78
+ #
79
+ # @api private
80
+ #
81
+ def attributes?
82
+ attributes.any?
83
+ end
84
+
85
+ private
86
+
87
+ # List of validator classes
88
+ #
89
+ # @return [Array<Class>] list of validators
90
+ #
91
+ # @api private
92
+ #
93
+ attr_reader :validators
94
+
95
+ # Options hash without attributes
96
+ #
97
+ # @return [Hash] options hash without attibutes
98
+ #
99
+ # @api private
100
+ #
101
+ def options
102
+ @options.except(:attributes)
103
+ end
104
+
105
+ # Fetch array of attributes from options passed in
106
+ #
107
+ # @return [Array] array of attributes
108
+ #
109
+ # @api private
110
+ #
111
+ def attributes
112
+ @options.fetch(:attributes, [])
113
+ end
114
+
115
+ # Zips up each validator with each attribute
116
+ #
117
+ # @return [Array<Array>] array of field, validator pairs
118
+ #
119
+ # @api private
120
+ #
121
+ def attributes_with_validators
122
+ attributes.flat_map do |attribute|
123
+ ([attribute] * validators.size).zip(validators)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end