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