activemodel-interdependence 0.0.1

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