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