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
@@ -0,0 +1,76 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module DependencyResolver
|
3
|
+
# resolver for model dependencies
|
4
|
+
#
|
5
|
+
class Model < Base
|
6
|
+
# @!parse include Virtus.model
|
7
|
+
|
8
|
+
CACHE = {}
|
9
|
+
|
10
|
+
# @!attribute [rw] dependency_class
|
11
|
+
# @!scope class
|
12
|
+
self.dependency_class = Dependency::Model
|
13
|
+
|
14
|
+
# Map for substituting names on dependency
|
15
|
+
#
|
16
|
+
# @return [Hash{Symbol=>Symbol}] [substitution map]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
#
|
20
|
+
attribute :substitutions, Hash, strict: true
|
21
|
+
|
22
|
+
# Resolve a dependency
|
23
|
+
#
|
24
|
+
# @param dependency [Dependency::Base] kind of dependency
|
25
|
+
#
|
26
|
+
# @return [Dependency::Model] dependency resolution
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
def resolve_with(dependency)
|
31
|
+
validation_dependency(
|
32
|
+
substitutions.fetch(dependency.field_name),
|
33
|
+
dependency.validator_class,
|
34
|
+
dependency.options
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Determine if a dependency is resolvable
|
41
|
+
#
|
42
|
+
# @param dependency [Dependency::Base] dependency to resolve
|
43
|
+
#
|
44
|
+
# @return [true] if dependency is a proxy field
|
45
|
+
# @return [false] otherwise
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
#
|
49
|
+
def resolvable?(dependency)
|
50
|
+
dependency.proxy_field?
|
51
|
+
end
|
52
|
+
|
53
|
+
# create a validation dependency or fetch from cache if already created
|
54
|
+
#
|
55
|
+
# @param field_name [Symbol] field name
|
56
|
+
# @param validator_kind [Class] validator of dependency
|
57
|
+
# @param options [Hash] options for validation
|
58
|
+
#
|
59
|
+
# @return [Dependency::Model] new or cached dependency
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
def validation_dependency(field_name, validator_kind, options)
|
64
|
+
key = [field_name, validator_kind, options]
|
65
|
+
|
66
|
+
CACHE.fetch(key) do
|
67
|
+
CACHE[key] = dependency_class.new(
|
68
|
+
field: field_name,
|
69
|
+
validator_class: validator_kind,
|
70
|
+
options: options
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module DependencyResolver
|
3
|
+
# resolver for validator dependencies
|
4
|
+
#
|
5
|
+
class Validator < DependencyResolver::Base
|
6
|
+
self.dependency_class = Dependency::Validator
|
7
|
+
|
8
|
+
# Replaces a resolvable dependency with {#new_dependency}
|
9
|
+
#
|
10
|
+
# @return [Dependency::Validator] {#new_dependency}
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
def resolve_with(*)
|
15
|
+
new_dependency
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Can a validator dependency be resolved
|
21
|
+
#
|
22
|
+
# @param dependency [Types::Field, Types::UnsetField] dependency field
|
23
|
+
#
|
24
|
+
# @return [true] if dependency has not set a field and shares the same validator_class
|
25
|
+
# @return [false] otherwise
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
#
|
29
|
+
def resolvable?(dependency)
|
30
|
+
dependency.unspecified_field? && dependency.validator_class.equal?(validator_class)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Interdependence
|
2
|
+
# Extension of Set that manages our dependencies
|
3
|
+
#
|
4
|
+
class DependencySet < Set
|
5
|
+
# Clone a DependencySet and its members
|
6
|
+
#
|
7
|
+
# @return [DependencySet] [clone]
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
def clone
|
12
|
+
self.class.new(map(&:clone))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Interdependence
|
2
|
+
# DependencySetGraph - extension of graph structure back by DependencySet
|
3
|
+
#
|
4
|
+
# - Extends our graph data structure
|
5
|
+
# - Overrides `#merge!` to merge both keys and set values
|
6
|
+
#
|
7
|
+
# @see Interdependence::Graph
|
8
|
+
#
|
9
|
+
# @example Merge
|
10
|
+
#
|
11
|
+
# dep1 # => { a: #<DependencyGraphSet: {1,2}> }
|
12
|
+
# dep2 # => { a: #<DependencyGraphSet: {2,3}> }
|
13
|
+
# dep1.merge(dep2) # => { a: #<DependencyGraphSet: {1,2,3}> }
|
14
|
+
#
|
15
|
+
class DependencySetGraph < Graph
|
16
|
+
self.default_value_class = DependencySet
|
17
|
+
|
18
|
+
# clone a DependencySetGraph
|
19
|
+
#
|
20
|
+
# @return [DependencySetGraph] clone graph by cloning each set
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
#
|
24
|
+
def clone
|
25
|
+
each_with_object(self.class.new) do |(parent, dependencies), clone|
|
26
|
+
clone[parent] = dependencies.clone
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Replace the owner of a certain set in the graph
|
31
|
+
#
|
32
|
+
# @param new_owner: [Dependency::Base] dependency key that will replace another
|
33
|
+
# @param &blk [#call] block used to detect owner to replace
|
34
|
+
#
|
35
|
+
# @return [self] self with new owner
|
36
|
+
#
|
37
|
+
# @api private
|
38
|
+
#
|
39
|
+
def change_owner(new_owner:, &blk)
|
40
|
+
old_owner = keys.detect(&blk)
|
41
|
+
|
42
|
+
tap do |graph|
|
43
|
+
graph[new_owner] = delete(old_owner) do
|
44
|
+
fail 'could not detect or delete key for replacement'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Merge two dependency set graphs
|
50
|
+
#
|
51
|
+
# Passes a block to `Hash#merge!` then instructs how two sets
|
52
|
+
# from different dependency graphs should be joined
|
53
|
+
#
|
54
|
+
# @see `Hash#merge!`
|
55
|
+
#
|
56
|
+
# @return [DependencySetGraph] [merged dependency graph]
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
#
|
60
|
+
def merge!(*args)
|
61
|
+
super do |_, dependencies, other_dependencies|
|
62
|
+
dependencies.merge(other_dependencies)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Interdependence
|
2
|
+
# Graph data structure - Topologically sortable
|
3
|
+
#
|
4
|
+
# - Accessing any key will initialize that key to a new empty set.
|
5
|
+
# - Topologically sortable using `tsort`
|
6
|
+
#
|
7
|
+
# @example Usage
|
8
|
+
#
|
9
|
+
# dependencies = Interdependence::Graph.new # => {}
|
10
|
+
# dependencies['foo'] # => #<Set: {}>
|
11
|
+
# dependencies['foo'] << 1 # => # => #<Set: {1}>
|
12
|
+
#
|
13
|
+
class Graph < Hash
|
14
|
+
include TSort
|
15
|
+
|
16
|
+
class_attribute :default_value_class, instance_writer: false
|
17
|
+
self.default_value_class = Set
|
18
|
+
|
19
|
+
# Create a new graph utilizing Hash#default_proc
|
20
|
+
#
|
21
|
+
# #default_proc usage initializes new keys to {#default_value}
|
22
|
+
#
|
23
|
+
# @return [undefined]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
#
|
27
|
+
def initialize
|
28
|
+
super do |hash, key|
|
29
|
+
hash[key] = default_value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Register a new key in the graph
|
34
|
+
#
|
35
|
+
# The default_proc implementation initializes values for keys
|
36
|
+
# when they are first accessed. Sometimes we don't want to do anything
|
37
|
+
# beyond initializing a new key, but `graph[:key]` by itself does not
|
38
|
+
# do a good job of revealing intention, so we alias `#[]` so that
|
39
|
+
# `graph[:key]` can be written as `graph.register(:key)`
|
40
|
+
#
|
41
|
+
# @see {#initialize} to understand default_proc
|
42
|
+
#
|
43
|
+
# @return [{#default_value}] the default value of the current class
|
44
|
+
#
|
45
|
+
# @api private
|
46
|
+
#
|
47
|
+
alias_method :register, :[]
|
48
|
+
|
49
|
+
# Iterate over each key of the graph for TSort
|
50
|
+
#
|
51
|
+
# @return [self]
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
#
|
55
|
+
alias_method :tsort_each_node, :each_key
|
56
|
+
|
57
|
+
# Iterate over each child in the graph for TSort
|
58
|
+
#
|
59
|
+
# @param node key to fetch
|
60
|
+
#
|
61
|
+
# @yield children of `node`
|
62
|
+
#
|
63
|
+
# @return [undefined]
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
#
|
67
|
+
def tsort_each_child(node, &block)
|
68
|
+
tsort_fetch(node).each(&block)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Fetch a key for tsort and fallback to {#default_value}
|
72
|
+
#
|
73
|
+
# `tsort_fetch` returns a new {#default_value} like `#[]` when
|
74
|
+
# `key` is not found. The fallback value is not added to the graph
|
75
|
+
# though since we do not want to mutate the graph during iteration
|
76
|
+
#
|
77
|
+
# @param key key being looked up
|
78
|
+
#
|
79
|
+
# @see Hash#fetch
|
80
|
+
# @see default_value
|
81
|
+
#
|
82
|
+
# @return value stored at self.fetch(key) if key exists
|
83
|
+
# @return {#default_value} otherwise
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
#
|
87
|
+
def tsort_fetch(key)
|
88
|
+
fetch(key) { default_value }
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# New instance of {#default_value_class}
|
94
|
+
#
|
95
|
+
# @return [{#default_value_class}] instance of {#default_value_class}
|
96
|
+
#
|
97
|
+
# @api private
|
98
|
+
#
|
99
|
+
def default_value
|
100
|
+
default_value_class.new
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Interdependence
|
2
|
+
# Mixin behavior for managing validators which specify
|
3
|
+
# that they depend on other validators.
|
4
|
+
#
|
5
|
+
module Model
|
6
|
+
extend CommonMixin
|
7
|
+
|
8
|
+
# Class methods mixed into any class that includes `ActiveModel::Model`
|
9
|
+
#
|
10
|
+
module ClassMethods
|
11
|
+
# Clone `validate_calls` on class inherited by class
|
12
|
+
#
|
13
|
+
# @return [undefined]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
#
|
17
|
+
def inherited(base)
|
18
|
+
super
|
19
|
+
|
20
|
+
base.validate_calls = validate_calls.clone
|
21
|
+
end
|
22
|
+
|
23
|
+
# Update validators attached to the current model
|
24
|
+
#
|
25
|
+
# @see ActiveModel::Validations::ClassMethods#clear_validators!
|
26
|
+
#
|
27
|
+
# @return [Void]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
def update_validators(*)
|
32
|
+
clear_validators!
|
33
|
+
validate_calls.each do |args, _|
|
34
|
+
active_model_validate(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
dependency_chain.each do |validator|
|
38
|
+
active_model_validates_with(*validator.to_active_model)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# adds an observer that expects {#update_validators} to be called
|
43
|
+
#
|
44
|
+
# @see CommonMixin::ClassMethods#clear_dependencies
|
45
|
+
# @return [Void]
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
#
|
49
|
+
def clear_dependencies!
|
50
|
+
super
|
51
|
+
dependencies.add_observer(self, :update_validators)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Topologically sort our dependencies
|
57
|
+
#
|
58
|
+
# Memoizes sorted dependencies using the current dependency set
|
59
|
+
# as the key
|
60
|
+
#
|
61
|
+
# @return [Array] dependency chain
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
#
|
65
|
+
def dependency_chain
|
66
|
+
dependencies.tsort
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Interdependence
|
2
|
+
module Model
|
3
|
+
# Validator wrapper for validators added to model classes
|
4
|
+
#
|
5
|
+
class Validator
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
# Model that owns this validator
|
9
|
+
#
|
10
|
+
# @return [Class] class that implements ActiveModel::Model
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
attribute :model, Class
|
15
|
+
|
16
|
+
# Field of validator
|
17
|
+
#
|
18
|
+
# @return [undefined]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
#
|
22
|
+
attribute :field, Symbol
|
23
|
+
|
24
|
+
# Validator class
|
25
|
+
#
|
26
|
+
# @return [Class] [descendant of ActiveModel::Validator]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
attribute :validator_class, Class
|
31
|
+
|
32
|
+
# Validator options hash
|
33
|
+
#
|
34
|
+
# @return [Hash] options passed to validator
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
#
|
38
|
+
attribute :options, Hash, default: {}
|
39
|
+
|
40
|
+
# Map for substituting names on validator
|
41
|
+
#
|
42
|
+
# @return [Hash{Symbol => Symbol}] [substitution map]
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
#
|
46
|
+
attribute :substitutions, Hash[Symbol => Symbol], default: {}
|
47
|
+
|
48
|
+
# Generic setter to unify with {Model::Validator}
|
49
|
+
#
|
50
|
+
# @return [owner]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
alias_method :owner=, :model=
|
55
|
+
|
56
|
+
# Set validator options hash
|
57
|
+
#
|
58
|
+
# @param options [Hash] options passed to validator
|
59
|
+
#
|
60
|
+
# @return [undefined]
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
#
|
64
|
+
def options=(options)
|
65
|
+
self.substitutions = options.fetch(:dependencies, {})
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!method dependencies
|
70
|
+
# Dependencies from resolver
|
71
|
+
#
|
72
|
+
# @return [ObservableDependencySetGraph] resolveddependencies
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
#
|
76
|
+
delegate :dependencies, to: :resolver
|
77
|
+
|
78
|
+
# Merge model dependencies with resolver dependencies
|
79
|
+
#
|
80
|
+
# @return [undefined]
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
#
|
84
|
+
def save
|
85
|
+
model.dependencies.merge!(dependencies)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Memoized resolver
|
89
|
+
#
|
90
|
+
# @return [DependencyResolver::Model] resolver from instance attributes
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
#
|
94
|
+
def resolver
|
95
|
+
@resolver ||= DependencyResolver::Model.new(self)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|