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
@@ -0,0 +1,84 @@
|
|
1
|
+
module Interdependence
|
2
|
+
# Mixin behavior for managing modules which specify
|
3
|
+
# that they depend on other validators.
|
4
|
+
#
|
5
|
+
module CommonMixin
|
6
|
+
# Extends {MixinMethods} when included
|
7
|
+
#
|
8
|
+
# @return [undefined]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
#
|
12
|
+
def self.extended(base)
|
13
|
+
base.extend(MixinMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
# methods mixed into {Interdependence::Model} and {Interdependence::Validator}
|
17
|
+
#
|
18
|
+
module MixinMethods
|
19
|
+
# Extends ClassMethods, adds and clears `dependencies` class attributes
|
20
|
+
#
|
21
|
+
# @return [undefined]
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
#
|
25
|
+
def included(base)
|
26
|
+
base.extend(ClassMethods)
|
27
|
+
base.extend(self::ClassMethods)
|
28
|
+
|
29
|
+
base.class_attribute :dependencies, :validate_calls
|
30
|
+
base.validate_calls = []
|
31
|
+
base.clear_dependencies!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Common class methods for {Interdependence::Model} and {Interdependence::Validator}
|
36
|
+
#
|
37
|
+
module ClassMethods
|
38
|
+
# Handle dependencies on inheritance
|
39
|
+
#
|
40
|
+
# resets depenencies on base and then merges in
|
41
|
+
# a clone of the parent dependencies
|
42
|
+
#
|
43
|
+
# @return [undefined]
|
44
|
+
#
|
45
|
+
# @api private
|
46
|
+
#
|
47
|
+
def inherited(base)
|
48
|
+
super
|
49
|
+
|
50
|
+
base.clear_dependencies!
|
51
|
+
base.dependencies.merge!(dependencies.clone)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add validator and merge its dependencies
|
55
|
+
#
|
56
|
+
# @param field [Symbol] [name of field to validate]
|
57
|
+
# @param validator [Class] [descendant of ActiveModel::Validator]
|
58
|
+
# @param options [Hash,TrueClass] [ActiveModel::Validator options]
|
59
|
+
#
|
60
|
+
# @return [undefined]
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
#
|
64
|
+
def add_interdependent_validator(field, validator, options)
|
65
|
+
self::Validator.new(
|
66
|
+
owner: self,
|
67
|
+
field: field,
|
68
|
+
validator_class: validator,
|
69
|
+
options: options
|
70
|
+
).save
|
71
|
+
end
|
72
|
+
|
73
|
+
# Resets dependencies to a new {ObservableDependencySetGraph}
|
74
|
+
#
|
75
|
+
# @return [undefined]
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
#
|
79
|
+
def clear_dependencies!
|
80
|
+
self.dependencies = ObservableDependencySetGraph.new
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module Dependency
|
3
|
+
# Base dependency wrapper class
|
4
|
+
#
|
5
|
+
class Base
|
6
|
+
include Adamantium
|
7
|
+
include Virtus.model
|
8
|
+
|
9
|
+
# @!method field
|
10
|
+
# Field of dependency
|
11
|
+
#
|
12
|
+
# @return [Field] [field wrapper]
|
13
|
+
# @return [UnsetField] [unset field abstraction]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
#
|
17
|
+
# @!method field=(field)
|
18
|
+
# Set field of dependency
|
19
|
+
#
|
20
|
+
# @param field [Symbol, nil] field name. nil becomes {Types::UnsetField}
|
21
|
+
#
|
22
|
+
# @return [undefined]
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
attribute :field, Types::Field, coercer: Types::FieldCoercer
|
27
|
+
|
28
|
+
# @!method validator_class
|
29
|
+
# Validator class of dependency (coerced with {Types::ValidatorClassCoercer})
|
30
|
+
#
|
31
|
+
# @return [Class] [descendant of ActiveModel::Validator]
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
#
|
35
|
+
# @!method validator_class=(validator_class)
|
36
|
+
# Set validator class
|
37
|
+
#
|
38
|
+
# @param validator_class [Class] descendant of ActiveModel::Validator
|
39
|
+
#
|
40
|
+
# @return [undefined]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
#
|
44
|
+
attribute :validator_class, Class, coercer: Types::ValidatorClassCoercer, strict: true
|
45
|
+
|
46
|
+
# @!method options
|
47
|
+
# Options for {#validator_class} (coerced with {Types::OptionsCoercer}). Memoized
|
48
|
+
#
|
49
|
+
# @return [Hash] [options]
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
#
|
53
|
+
# @!method options=(options)
|
54
|
+
# Set options
|
55
|
+
#
|
56
|
+
# @param options [Hash, true, nil]
|
57
|
+
# set options. true becomes {}. nil becomes {Types::UnsetOptions}
|
58
|
+
#
|
59
|
+
# @return [undefined]
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
attribute :options, Hash, coercer: Types::OptionsCoercer
|
64
|
+
|
65
|
+
memoize :options
|
66
|
+
|
67
|
+
# Clone class
|
68
|
+
#
|
69
|
+
# @return [Dependency::Base] [clone]
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
#
|
73
|
+
def clone
|
74
|
+
self.class.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Make dependencies more readable
|
78
|
+
#
|
79
|
+
# @return [String] [inspection string]
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
#
|
83
|
+
def inspect
|
84
|
+
inspect_field = unspecified_field? ? '*' : field.name.inspect
|
85
|
+
|
86
|
+
"\e[1;36m#<#{self.class} #{validator_class}(#{inspect_field}, #{options.inspect})>\e[0m"
|
87
|
+
end
|
88
|
+
|
89
|
+
# is {field} unset?
|
90
|
+
#
|
91
|
+
# @return [true] if field is a {UnsetField}
|
92
|
+
# @return [false] otherwise
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
#
|
96
|
+
def unspecified_field?
|
97
|
+
field.instance_of?(Types::UnsetField)
|
98
|
+
end
|
99
|
+
|
100
|
+
# is {options} unset?
|
101
|
+
#
|
102
|
+
# @return [true] if field is a {UnsetOptions}
|
103
|
+
# @return [false] otherwise
|
104
|
+
#
|
105
|
+
# @api private
|
106
|
+
#
|
107
|
+
def unspecified_options?
|
108
|
+
options.instance_of?(Types::UnsetOptions)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Is this instance the equivalent of `other` with unset attributes
|
112
|
+
#
|
113
|
+
# @param other [instance of same class]
|
114
|
+
#
|
115
|
+
# @return [true] if field and options are unset and other has the same {#validator_class}
|
116
|
+
# @return [true] if field is unset and other has the same options and {#validator_class}
|
117
|
+
# @return [false] otherwise
|
118
|
+
#
|
119
|
+
# @api private
|
120
|
+
#
|
121
|
+
def unset_equivalent_of?(other)
|
122
|
+
unspecified_field? &&
|
123
|
+
(unspecified_options? || options.eql?(other.options)) &&
|
124
|
+
validator_class.equal?(other.validator_class)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Is the other instance equivalent to this instance
|
128
|
+
#
|
129
|
+
# @param other [instance of same class]
|
130
|
+
#
|
131
|
+
# @return [true] if descendant of {Base} with the same validator, field, and options
|
132
|
+
# @return [false] otherwise
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
#
|
136
|
+
def ==(other)
|
137
|
+
other.is_a?(Base) &&
|
138
|
+
validator_class.equal?(other.validator_class) &&
|
139
|
+
field.eql?(other.field) &&
|
140
|
+
options.eql?(other.options)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Cloned copy of {#validator_class}'s dependencies
|
144
|
+
#
|
145
|
+
# @return [ObservableDependencySetGraph] cloned dependencies
|
146
|
+
#
|
147
|
+
# @api private
|
148
|
+
#
|
149
|
+
def dependencies
|
150
|
+
validator_class.dependencies.clone
|
151
|
+
end
|
152
|
+
memoize :dependencies, freezer: :noop
|
153
|
+
|
154
|
+
alias_method :eql?, :==
|
155
|
+
|
156
|
+
# @!method field_name
|
157
|
+
#
|
158
|
+
# Get name of field
|
159
|
+
#
|
160
|
+
# @return [Symbol] [field name]
|
161
|
+
#
|
162
|
+
# @api private
|
163
|
+
#
|
164
|
+
delegate :name, to: :field, prefix: true
|
165
|
+
|
166
|
+
# @!method field_name=
|
167
|
+
#
|
168
|
+
# Set name of field
|
169
|
+
#
|
170
|
+
# @return [undefined]
|
171
|
+
#
|
172
|
+
# @api private
|
173
|
+
#
|
174
|
+
delegate :name=, to: :field, prefix: true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module Dependency
|
3
|
+
# Model dependency wrapper
|
4
|
+
#
|
5
|
+
class Model < Base
|
6
|
+
# Map for substituting names on dependency
|
7
|
+
#
|
8
|
+
# @return [Hash{Symbol => Symbol}] [substitution map]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
attribute :substitutions, Hash
|
12
|
+
memoize :substitutions
|
13
|
+
|
14
|
+
# Is class a proxy field?
|
15
|
+
#
|
16
|
+
# @return [false]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
#
|
20
|
+
def proxy_field?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
# Dependencies clone with self replacing the unset equivalent dependency
|
25
|
+
#
|
26
|
+
# @return [ObservableDependencySetGraph] dependencies
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
def dependencies
|
31
|
+
super.change_owner(new_owner: self) do |current_owner|
|
32
|
+
current_owner.unset_equivalent_of?(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Formats dependency as parameters for ActiveModel::Model#validates
|
37
|
+
#
|
38
|
+
# @return [Array] active model parameters
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
#
|
42
|
+
def to_active_model
|
43
|
+
[validator_class, active_model_options.merge(attributes: [field_name])]
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# {#options} rewritten to include :if option dependent there being no errors
|
49
|
+
#
|
50
|
+
# @return [Hash] [adjusted options]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
def active_model_options
|
55
|
+
validator_if = Array.wrap(options[:if])
|
56
|
+
validator_if << proc { errors.empty? }
|
57
|
+
options.merge(if: validator_if)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module Dependency
|
3
|
+
# Validator dependency wrapper
|
4
|
+
#
|
5
|
+
class Validator < Base
|
6
|
+
# @!method options=(options)
|
7
|
+
# Set options
|
8
|
+
#
|
9
|
+
# @param options [Hash, true, nil]
|
10
|
+
# set options. true becomes {}. nil becomes {Types::UnsetOptions}
|
11
|
+
# defaults to nil
|
12
|
+
#
|
13
|
+
# @return [undefined]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
#
|
17
|
+
attribute :options, Hash, coercer: Types::OptionsCoercer, default: nil
|
18
|
+
|
19
|
+
# add an observer to the validator's dependency graph
|
20
|
+
#
|
21
|
+
# @param observer [Class] observer
|
22
|
+
# @param method [Symbol] message to notify observer with
|
23
|
+
#
|
24
|
+
# @return [undefined]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
def add_validator_observer(observer, method)
|
29
|
+
validator_class.dependencies.add_observer(observer, method)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Is class a proxy field?
|
33
|
+
#
|
34
|
+
# @return [false]
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
#
|
38
|
+
def proxy_field?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module DependencyResolver
|
3
|
+
# Base class that consolidates common functionality for dependency resolvers
|
4
|
+
#
|
5
|
+
class Base
|
6
|
+
include Adamantium
|
7
|
+
include Virtus.model
|
8
|
+
|
9
|
+
# Field name
|
10
|
+
#
|
11
|
+
# @return [Symbol] field name
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
#
|
15
|
+
attribute :field, Symbol, strict: true
|
16
|
+
|
17
|
+
# Descendant of ActiveModel::Model
|
18
|
+
#
|
19
|
+
# @return [Class] class that includes activemodel
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
attribute :validator_class, Class, coercer: Types::ValidatorClassCoercer, strict: true
|
24
|
+
|
25
|
+
# Options for validator
|
26
|
+
#
|
27
|
+
# @return [Hash] options hash
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
attribute :options, Hash, strict: true
|
32
|
+
|
33
|
+
# @!attribute [w] dependency_class
|
34
|
+
# Dependency class this resolver should use
|
35
|
+
#
|
36
|
+
# @return [Class] specified dependency class
|
37
|
+
#
|
38
|
+
# @!scope class
|
39
|
+
# @api private
|
40
|
+
#
|
41
|
+
class_attribute :dependency_class, instance_writer: false
|
42
|
+
|
43
|
+
# {ObservableDependencySetGraph} resolved dependencies
|
44
|
+
#
|
45
|
+
# @return [ObservableDependencySetGraph] resolved dependencies
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
#
|
49
|
+
def dependencies
|
50
|
+
dependency_graph_mapper.each do |(parent, children), graph|
|
51
|
+
new_parent, new_children = resolve_pair(parent, children)
|
52
|
+
|
53
|
+
graph[new_parent].replace(new_children)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# New dependency initialized with attributes of {self}
|
58
|
+
#
|
59
|
+
# @return [dependency_class] new dependency
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
def new_dependency
|
64
|
+
dependency_class.new(self)
|
65
|
+
end
|
66
|
+
memoize :new_dependency
|
67
|
+
|
68
|
+
# Resolve a dependency if it is resolvable
|
69
|
+
#
|
70
|
+
# @param dependency [dependency_class] dependency to resolve
|
71
|
+
#
|
72
|
+
# @return [dependency_class] resolved dependency or input
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
#
|
76
|
+
def resolve_dependency(dependency)
|
77
|
+
if resolvable?(dependency)
|
78
|
+
resolve_with(dependency)
|
79
|
+
else
|
80
|
+
dependency
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Resolve a parent-children pair
|
85
|
+
#
|
86
|
+
# @param parent [dependency_class] dependency owner
|
87
|
+
# @param children [Array<dependency_class>] dependencies of parent
|
88
|
+
#
|
89
|
+
# @return resolved parent-children pair
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
#
|
93
|
+
def resolve_pair(parent, children)
|
94
|
+
parent = resolve_dependency(parent)
|
95
|
+
children = children.map { |child| resolve_dependency(child) }
|
96
|
+
|
97
|
+
[parent, children]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Iterator for resolving dependencies
|
101
|
+
#
|
102
|
+
# @return [Enumerator] [description]
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
#
|
106
|
+
def dependency_graph_mapper
|
107
|
+
new_dependency
|
108
|
+
.dependencies
|
109
|
+
.each
|
110
|
+
.with_object(ObservableDependencySetGraph.new)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|