activemodel-interdependence 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +97 -0
  3. data/.rspec +4 -0
  4. data/.rubocop.yml +208 -0
  5. data/.rubocop_todo.yml +34 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +83 -0
  8. data/Gemfile.lock +336 -0
  9. data/Guardfile +68 -0
  10. data/LICENSE +21 -0
  11. data/README.md +76 -0
  12. data/Rakefile +95 -0
  13. data/activemodel-interdependence.gemspec +34 -0
  14. data/circle.yml +7 -0
  15. data/config/devtools.yml +2 -0
  16. data/config/flay.yml +3 -0
  17. data/config/flog.yml +2 -0
  18. data/config/mutant.yml +9 -0
  19. data/config/reek.yml +120 -0
  20. data/config/rubocop.yml +1 -0
  21. data/config/yardstick.yml +2 -0
  22. data/lib/activemodel/interdependence.rb +12 -0
  23. data/lib/activemodel/model/interdependence.rb +97 -0
  24. data/lib/activemodel/validator/interdependence.rb +107 -0
  25. data/lib/interdependence.rb +87 -0
  26. data/lib/interdependence/activemodel/class_methods.rb +50 -0
  27. data/lib/interdependence/activemodel/validates_with.rb +128 -0
  28. data/lib/interdependence/common_mixin.rb +84 -0
  29. data/lib/interdependence/dependency/base.rb +177 -0
  30. data/lib/interdependence/dependency/model.rb +61 -0
  31. data/lib/interdependence/dependency/validator.rb +43 -0
  32. data/lib/interdependence/dependency_resolver/base.rb +114 -0
  33. data/lib/interdependence/dependency_resolver/model.rb +76 -0
  34. data/lib/interdependence/dependency_resolver/validator.rb +34 -0
  35. data/lib/interdependence/dependency_set.rb +15 -0
  36. data/lib/interdependence/dependency_set_graph.rb +66 -0
  37. data/lib/interdependence/graph.rb +103 -0
  38. data/lib/interdependence/model.rb +70 -0
  39. data/lib/interdependence/model/validator.rb +99 -0
  40. data/lib/interdependence/observable_dependency_set_graph.rb +23 -0
  41. data/lib/interdependence/types.rb +199 -0
  42. data/lib/interdependence/validator.rb +67 -0
  43. data/lib/interdependence/validator/validator.rb +105 -0
  44. data/lib/interdependence/version.rb +3 -0
  45. 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