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.
- checksums.yaml +7 -0
- data/.gitignore +97 -0
- data/.rspec +4 -0
- data/.rubocop.yml +208 -0
- data/.rubocop_todo.yml +34 -0
- data/.yardopts +1 -0
- data/Gemfile +83 -0
- data/Gemfile.lock +336 -0
- data/Guardfile +68 -0
- data/LICENSE +21 -0
- data/README.md +76 -0
- data/Rakefile +95 -0
- data/activemodel-interdependence.gemspec +34 -0
- data/circle.yml +7 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +9 -0
- data/config/reek.yml +120 -0
- data/config/rubocop.yml +1 -0
- data/config/yardstick.yml +2 -0
- data/lib/activemodel/interdependence.rb +12 -0
- data/lib/activemodel/model/interdependence.rb +97 -0
- data/lib/activemodel/validator/interdependence.rb +107 -0
- data/lib/interdependence.rb +87 -0
- data/lib/interdependence/activemodel/class_methods.rb +50 -0
- data/lib/interdependence/activemodel/validates_with.rb +128 -0
- data/lib/interdependence/common_mixin.rb +84 -0
- data/lib/interdependence/dependency/base.rb +177 -0
- data/lib/interdependence/dependency/model.rb +61 -0
- data/lib/interdependence/dependency/validator.rb +43 -0
- data/lib/interdependence/dependency_resolver/base.rb +114 -0
- data/lib/interdependence/dependency_resolver/model.rb +76 -0
- data/lib/interdependence/dependency_resolver/validator.rb +34 -0
- data/lib/interdependence/dependency_set.rb +15 -0
- data/lib/interdependence/dependency_set_graph.rb +66 -0
- data/lib/interdependence/graph.rb +103 -0
- data/lib/interdependence/model.rb +70 -0
- data/lib/interdependence/model/validator.rb +99 -0
- data/lib/interdependence/observable_dependency_set_graph.rb +23 -0
- data/lib/interdependence/types.rb +199 -0
- data/lib/interdependence/validator.rb +67 -0
- data/lib/interdependence/validator/validator.rb +105 -0
- data/lib/interdependence/version.rb +3 -0
- metadata +213 -0
data/config/rubocop.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
inherit_from: ../.rubocop.yml
|
@@ -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
|