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