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,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
|