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