verifly 0.2.0.1 → 0.3.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7171d204c82a0f84dd0b8780dda21a0c527687e1
4
- data.tar.gz: cec080bcb1b9e2e38a3dd3434f8f18c498293f64
2
+ SHA256:
3
+ metadata.gz: ef4ba187f3fbc61190750de9fe19bf780c8a17e980b5efe4658c748ab1578fc7
4
+ data.tar.gz: a879d55708639831374f646889db8b8385d769737b378906183776f49cafdd53
5
5
  SHA512:
6
- metadata.gz: 3f8f1bcaf78681238fd4935cc831e4b5716aa937fe92caafffb4aa1762837d0091da69a891d4b33ce25126a40d3193cacb930a23e7c9ec1adf33dbfb2b73b850
7
- data.tar.gz: 2ede721d805ba64742b50743b16952609e6c82529d0a634f82bf32b7152660becd766c687147e9ae369aa8e02211cb12b80a4da7284d950abdd147ad8f6a2fea
6
+ metadata.gz: 12191c302d7adf9081af6ccb818a20439ae387a5509b85f7e80d7457689669975ca78fc3ac4e4971a21c85664dede77c9a9744bd783bd73d124fcf675a1e1ffb
7
+ data.tar.gz: 7b28d9b3c44a3a8453c0eaeecea85eee02e9275a1d4db308afb0ec977cd2c30e039ab2ec959003509f5f41cac4b76eafb5d97b3404f7744eed6f429908136952
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Verifly v0.2
2
- [![Build Status](https://travis-ci.org/umbrellio/verifly.svg?branch=master)](https://travis-ci.org/umbrellio/verifly)
2
+ [![Build Status](https://travis-ci.org/umbrellio/verifly.svg?branch=master)](https://travis-ci.org/umbrellio/verifly)[![Coverage Status](https://coveralls.io/repos/github/umbrellio/verifly/badge.svg)](https://coveralls.io/github/umbrellio/verifly)
3
3
 
4
4
  This gem provides an api to run sequential checks like
5
5
  'ActiveModel::Validations' do, but with generic messages instead of errors.
@@ -4,10 +4,12 @@
4
4
  # important one, while others depend on it.
5
5
  # See README.md or in-code documentation for more info.
6
6
  module Verifly
7
- autoload :VERSION, 'verifly/version'
7
+ autoload :VERSION, "verifly/version"
8
8
 
9
- autoload :Applicator, 'verifly/applicator'
10
- autoload :ApplicatorWithOptions, 'verifly/applicator_with_options'
11
- autoload :ClassBuilder, 'verifly/class_builder'
12
- autoload :Verifier, 'verifly/verifier'
9
+ autoload :Applicator, "verifly/applicator"
10
+ autoload :ApplicatorWithOptions, "verifly/applicator_with_options"
11
+ autoload :ClassBuilder, "verifly/class_builder"
12
+ autoload :DependentCallbacks, "verifly/dependent_callbacks"
13
+ autoload :HasLogger, "verifly/has_logger"
14
+ autoload :Verifier, "verifly/verifier"
13
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Verifly
4
- # @abstract implement `#call`
4
+ # @abstract implement `#call`, `#source` and `#source_location`
5
5
  # Applies "applicable" objects to given bindings
6
6
  # (applicable objects are named based on their use,
7
7
  # currently any object is applicable).
@@ -14,8 +14,8 @@ module Verifly
14
14
  # Proxy is used when applicable itself is an instance of Applicator.
15
15
  # It just delegates #call method to applicable
16
16
  # @example
17
- # Applicator.call(Applicator.build(:foo), binding_, context)
18
- # # => Applicator.call(:foo, binding_, context)
17
+ # Applicator.call(Applicator.build(:foo), binding_, *context)
18
+ # # => Applicator.call(:foo, binding_, *context)
19
19
  class Proxy < self
20
20
  # @param applicable [Applicator]
21
21
  # @return Proxy if applicable is an instance of Applicator
@@ -27,8 +27,20 @@ module Verifly
27
27
  # @param binding_ [#instance_exec] target to apply applicable
28
28
  # @param context additional info to send it to applicable
29
29
  # @return application result
30
- def call(binding_, context)
31
- applicable.call(binding_, context)
30
+ def call(binding_, *context)
31
+ applicable.call(binding_, *context)
32
+ end
33
+
34
+ # @param binding_ [#instance_exec] binding to find relative source
35
+ # @return [[String, Integer]?] (file, line) location of calblack source (if exists)
36
+ def source_location(binding_)
37
+ applicable.source_location(binding_)
38
+ end
39
+
40
+ # @param binding_ [#instance_exec] binding to find relative source
41
+ # @return [String?] callback (not it's defenition) source code
42
+ def source(binding_)
43
+ applicable.source(binding_)
32
44
  end
33
45
  end
34
46
 
@@ -51,25 +63,45 @@ module Verifly
51
63
  # @param binding_ [#instance_exec] target to apply applicable to
52
64
  # @param context additional info to send to applicable
53
65
  # @return application result
54
- def call(binding_, context)
66
+ def call(binding_, *context)
55
67
  if binding_.is_a?(Binding)
56
- call_on_binding(binding_, context)
68
+ call_on_binding(binding_, *context)
57
69
  else
58
- invoke_lambda(binding_.method(applicable), binding_, context)
70
+ invoke_lambda(binding_.method(applicable), binding_, *context)
59
71
  end
60
72
  end
61
73
 
74
+ # @param binding_ [#instance_exec] binding to find relative source
75
+ # @return [[String, Integer]] (file, line) location of calblack source (if exists)
76
+ # @raise [NameError] if method does not exist on binding_
77
+ def source_location(binding_)
78
+ binding_method(binding_).method(applicable).source_location
79
+ end
80
+
81
+ # @param binding_ [#instance_exec] binding to find relative source
82
+ # @return [String] relative method source defenition
83
+ # @raise [NameError] if method does not exist on binding_
84
+ def source(binding_)
85
+ binding_method(binding_).method(applicable).source
86
+ end
87
+
88
+ # @param binding_ [#instance_exec] binding to find relative source
89
+ # @return [Method] method, extracted from binding
90
+ def binding_method(binding_)
91
+ binding_.is_a?(Binding) ? binding_.receiver : binding_
92
+ end
93
+
62
94
  private
63
95
 
64
96
  # When Binding is a target, we have to respect both methods and variables
65
97
  # @param binding_ [Binding] target to apply applicable to
66
98
  # @param context additional info to send to applicable
67
99
  # @return application result
68
- def call_on_binding(binding_, context)
69
- if binding_.receiver.respond_to?(applicable)
70
- invoke_lambda(binding_.receiver.method(applicable), binding_, context)
71
- else
100
+ def call_on_binding(binding_, *context)
101
+ if binding_.local_variable_defined?(applicable)
72
102
  binding_.local_variable_get(applicable)
103
+ else
104
+ invoke_lambda(binding_.receiver.method(applicable), binding_, *context)
73
105
  end
74
106
  end
75
107
  end
@@ -77,7 +109,7 @@ module Verifly
77
109
  # InstanceEvaluator is used for strings. It works like instance_eval or
78
110
  # Binding#eval depending on binding_ class
79
111
  # @example
80
- # Applicator.call('foo if context[:foo]', binding_, context)
112
+ # Applicator.call('foo if context[:foo]', binding_, *context)
81
113
  # # => foo if context[:foo]
82
114
  class InstanceEvaluator < self
83
115
  # @param applicable [String]
@@ -90,7 +122,7 @@ module Verifly
90
122
  # @param binding_ [#instance_exec] target to apply applicable to
91
123
  # @param context additional info to send to applicable
92
124
  # @return application result
93
- def call(binding_, context)
125
+ def call(binding_, *context)
94
126
  if binding_.is_a?(Binding)
95
127
  binding_ = binding_.dup
96
128
  binding_.local_variable_set(:context, context)
@@ -100,6 +132,17 @@ module Verifly
100
132
  end
101
133
  end
102
134
 
135
+ # Source location is not available
136
+ # @return [nil]
137
+ def source_location(*); end
138
+
139
+ # @return [String] exactly it's defenition
140
+ def source(*)
141
+ applicable
142
+ end
143
+
144
+ private
145
+
103
146
  # @return [String, Integer]
104
147
  # file and line where `Applicator.call` was called
105
148
  def caller_line
@@ -113,7 +156,7 @@ module Verifly
113
156
  # ProcApplicatior is used when #to_proc is available.
114
157
  # It works not only with procs, but also with hashes etc
115
158
  # @example with a proc
116
- # Applicator.call(-> { foo }, binding_, context) # => foo
159
+ # Applicator.call(-> { foo }, binding_, *context) # => foo
117
160
  # @example with a hash
118
161
  # Applicator.call(Hash[foo: true], binding_, :foo) # => true
119
162
  # Applicator.call(Hash[foo: true], binding_, :bar) # => nil
@@ -128,19 +171,38 @@ module Verifly
128
171
  # @param binding_ [#instance_exec] target to apply applicable to
129
172
  # @param context additional info to send to applicable
130
173
  # @return application result
131
- def call(binding_, context)
132
- invoke_lambda(applicable.to_proc, binding_, context)
174
+ def call(binding_, *context)
175
+ invoke_lambda(applicable.to_proc, binding_, *context)
176
+ end
177
+
178
+ # @return [String, Integer] Proc#source_location
179
+ def source_location(*)
180
+ applicable.to_proc.source_location
181
+ end
182
+
183
+ # @return [String] Proc#source
184
+ def source(*)
185
+ applicable.to_proc.source
133
186
  end
134
187
  end
135
188
 
136
189
  # Quoter is used when there is no other way to apply applicatable.
137
190
  # @example
138
- # Applicator.call(true, binding_, context) # => true
191
+ # Applicator.call(true, binding_, *context) # => true
139
192
  class Quoter < self
140
193
  # @return applicable without changes
141
194
  def call(*)
142
195
  applicable
143
196
  end
197
+
198
+ # No source location available
199
+ # @return [nil]
200
+ def source_location(*); end
201
+
202
+ # @return [String] quoted value's inspect
203
+ def source
204
+ applicable.inpsect
205
+ end
144
206
  end
145
207
 
146
208
  extend ClassBuilder::Mixin
@@ -151,7 +213,6 @@ module Verifly
151
213
  attr_accessor :applicable
152
214
 
153
215
  # Applies applicable on binding_ with context
154
- # @todo add @see #initialize when its todo is done
155
216
  # @param applicable [applicable]
156
217
  # see examples in definitions of subclasses
157
218
  # @param binding_ [#instance_exec]
@@ -161,8 +222,8 @@ module Verifly
161
222
  # geneneric data you want to pass to applicable function.
162
223
  # If applicable cannot accept params, context will not be sent
163
224
  # @return application result
164
- def self.call(applicable, binding_, context)
165
- build(applicable).call(binding_, context)
225
+ def self.call(applicable, binding_, *context)
226
+ build(applicable).call(binding_, *context)
166
227
  end
167
228
 
168
229
  # Always use build instead of new
@@ -180,13 +241,23 @@ module Verifly
180
241
  applicable == other.applicable
181
242
  end
182
243
 
183
- # @!method call(binding_, context)
244
+ # @!method call(binding_, *context)
184
245
  # @abstract
185
246
  # Applies applicable on binding_ with context
186
247
  # @param binding_ [#instance_exec] binding to be used for applying
187
248
  # @param context param that will be passed if requested
188
249
  # @return application result
189
250
 
251
+ # @!method source_location(binding_)
252
+ # @abstract
253
+ # @param binding_ [#instance_exec] binding to find relative source
254
+ # @return [[String, Integer]?] (file, line) location of calblack source (if exists)
255
+
256
+ # @!method source(binding_)
257
+ # @abstract
258
+ # @param binding_ [#instance_exec] binding to find relative source
259
+ # @return [String?] callback (not it's defenition) source code
260
+
190
261
  private
191
262
 
192
263
  # invokes lambda respecting its arity
@@ -194,11 +265,11 @@ module Verifly
194
265
  # @param binding_ [#instance_exec] binding_ would be used in application
195
266
  # @param context param would be passed if lambda arity > 0
196
267
  # @return invocation result
197
- def invoke_lambda(lambda, binding_, context)
268
+ def invoke_lambda(lambda, binding_, *context)
198
269
  if lambda.arity.zero?
199
270
  binding_.instance_exec(&lambda)
200
271
  else
201
- binding_.instance_exec(context, &lambda)
272
+ binding_.instance_exec(*context, &lambda)
202
273
  end
203
274
  end
204
275
  end
@@ -24,9 +24,7 @@ module Verifly
24
24
  # @raise [ArgumentError] if there is more than two arguments and block
25
25
  # @raise [ArgumentError] if there is zero arguments and no block
26
26
  def initialize(*args, &block)
27
- action, options, *rest = block ? [block, *args] : args
28
- options ||= {}
29
- raise ArgumentError unless action && rest.empty?
27
+ action, options = normalize_options(*args, &block)
30
28
 
31
29
  self.action = Applicator.build(action)
32
30
  self.if_condition = Applicator.build(options.fetch(:if, true))
@@ -41,10 +39,20 @@ module Verifly
41
39
  # generic context to apply (see Applicator)
42
40
  # @return main action application result
43
41
  # @return [nil] if condition checks failed
44
- def call(binding_, context)
45
- return unless if_condition.call(binding_, context)
46
- return if unless_condition.call(binding_, context)
47
- action.call(binding_, context)
42
+ def call(binding_, *context)
43
+ return unless if_condition.call(binding_, *context)
44
+ return if unless_condition.call(binding_, *context)
45
+ action.call(binding_, *context)
46
+ end
47
+
48
+ private
49
+
50
+ def normalize_options(*args, &block)
51
+ action, options, *rest = block ? [block, *args] : args
52
+ options ||= {}
53
+ raise ArgumentError unless action && rest.empty?
54
+
55
+ [action, options]
48
56
  end
49
57
  end
50
58
  end
@@ -4,18 +4,18 @@ module Verifly
4
4
  # ClassBuilder is similar to Uber::Builder, but it
5
5
  # allows child classes to decide whether they will be used.
6
6
  # I find it much more object-oriented
7
- # @attr klasses [Array(Class)]
7
+ # @attr klasses [[Class]]
8
8
  # classes to iterate during search of most suitable
9
9
  class ClassBuilder
10
10
  # Mixin provides useful methods to integrate into builder subsystem.
11
11
  # Feel free to override or just never include it.
12
- # @attr_writer [Array(Class)] buildable_classes
12
+ # @attr_writer [[Class]] buildable_classes
13
13
  # Array of classes which will be checked if they
14
14
  # suite constructor arguments. Order matters
15
15
  module Mixin
16
16
  # Array of classes which will be checked if they
17
17
  # suite constructor arguments. Order matters
18
- # @param klasses [Array(Class)]
18
+ # @param klasses [[Class]]
19
19
  def buildable_classes=(klasses)
20
20
  @class_builder = ClassBuilder.new(klasses).freeze
21
21
  end
@@ -40,7 +40,7 @@ module Verifly
40
40
 
41
41
  attr_accessor :klasses
42
42
 
43
- # @param klasses [Array(Classes)]
43
+ # @param klasses [[Class]]
44
44
  def initialize(klasses)
45
45
  self.klasses = klasses
46
46
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifly
4
+ # DependentCallbacks interface is similar to ActiveSupport::Callbacks, but it has few differences
5
+ #
6
+ # 1) `extend` it, not `include`
7
+ #
8
+ # 2) Use `.callback_groups` do tefine callbacks instead of define_callbacks
9
+ #
10
+ # 3) Better define callbacks in separate module, wich should extend DependentCallbacks::Storage
11
+ #
12
+ # 4) Use merge_callbacks_from(Module) instead of including it
13
+ #
14
+ # 5) There is no run_callbacks method.
15
+ #
16
+ # You can either use self.class.dependent_callbacks.invoke(group, *context) {}
17
+ # or use .export_callbacks_to(:active_support) / .export_callbacks_to(:wrap_method)
18
+ module DependentCallbacks
19
+ autoload :Callback, "verifly/dependent_callbacks/callback"
20
+ autoload :CallbackGroup, "verifly/dependent_callbacks/callback_group"
21
+ autoload :Invoker, "verifly/dependent_callbacks/invoker"
22
+ autoload :Service, "verifly/dependent_callbacks/service"
23
+ autoload :Storage, "verifly/dependent_callbacks/storage"
24
+
25
+ extend HasLogger
26
+ include Storage
27
+
28
+ # @api stdlib
29
+ # Allows children to inherit callbacks from parent
30
+ # @param [Class] child
31
+ def inherited(child)
32
+ super
33
+
34
+ child.instance_exec(dependent_callbacks_service) do |service|
35
+ @dependent_callbacks_service = Service.new(service)
36
+ end
37
+ end
38
+
39
+ # Exports callbacks to another callback system / something like that
40
+ # @param [:active_support, :wrap_method] target
41
+ # Target selection.
42
+ # * :active_support exports each group to correspoding
43
+ # ActiveSupport callback (via set_callback)
44
+ # * :wrap_method defines / redefines methods, named same as each group.
45
+ # If method was defined, on it's call callbacks would run around its previous defenition.
46
+ # If not, callbacks would run around nothing
47
+ # @param groups [[Symbol]] arra of groups to export. Defaults to all groups
48
+ def export_callbacks_to(target, groups: nil)
49
+ (groups || dependent_callbacks_service.group_names).each do |group|
50
+ case target
51
+ when :active_support then _export_callback_group_to_active_support(group)
52
+ when :action_controller then _export_callback_group_to_action_controller(group)
53
+ when :wrap_method then _export_callback_group_to_method_wapper(group)
54
+ else
55
+ raise "#{target.inspect} export target unavailable. " \
56
+ "available targets are :active_support, :action_controller, :wrap_method"
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Exports callbacks to ActiveSupport
64
+ # @see export_callbacks_to
65
+ # @param group [Symbol] name of group
66
+ def _export_callback_group_to_active_support(group)
67
+ exports_name = :"__verifly_dependent_callbacks_exports_#{group}"
68
+
69
+ define_method(exports_name) do |*context, &block|
70
+ self.class.dependent_callbacks_service.invoke(group) do |invoker|
71
+ invoker.context = context
72
+ invoker.inner_block = block
73
+ invoker.run(self)
74
+ end
75
+ end
76
+
77
+ private(exports_name)
78
+ set_callback(group, :around, exports_name)
79
+ end
80
+
81
+ # Exports callbacks to ActionController::Base
82
+ # @see export_callbacks_to
83
+ # @param group [Symbol] name of group
84
+ def _export_callback_group_to_action_controller(group)
85
+ raise unless group == :action
86
+
87
+ around_action do |request, sequence|
88
+ self.class.dependent_callbacks_service.invoke(group) do |invoker|
89
+ invoker.context << request
90
+ invoker.inner_block = sequence
91
+ invoker.break_if { response_body }
92
+ invoker.run(self)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Exports callbacks to methods
98
+ # @see export_callbacks_to
99
+ # @param group [Symbol] name of group
100
+ def _export_callback_group_to_method_wapper(group)
101
+ instance_method = instance_method(group) rescue nil
102
+
103
+ define_method(group) do |*args, &block|
104
+ self.class.dependent_callbacks_service.invoke(group) do |invoker|
105
+ invoker.around { instance_method&.bind(self)&.call(*args, &block) }
106
+ invoker.context = args
107
+ invoker.run(self)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifly
4
+ module DependentCallbacks
5
+ # ApplicatorWithOptions improved to handle everything needed in DependentCallbacks
6
+ # @attr name [Symbol?] callback name
7
+ # @attr position [:before, :after, :around] callback position
8
+ # @attr before [[Symbol]] names of calblacks before which this is
9
+ # @attr after [[Symbol]] names of calblacks after which this is
10
+ class Callback < ApplicatorWithOptions
11
+ # Available positions of calblack: before, after or around action
12
+ POSITIONS = %i[before after around].freeze
13
+
14
+ attr_accessor :name, :position, :before, :after
15
+
16
+ # @!method initialize(position, action = block, options = {}, &block)
17
+ # @see ApplicatorWithOptions#initialize
18
+ # @param position [:before, :after, :around] position
19
+ # @param action [applicable] main action
20
+ # @option options [applicable] :if
21
+ # main action is only applied if this evaluates to truthy value
22
+ # @option options [applicable] :unless
23
+ # main action is only applied if this evaluates to falsey value
24
+ # @option options [Symbol] :name
25
+ # name override for callback. By default, name is taken from applicable if it is a symbol
26
+ # or set to nil. This option allows to use named applicables like proc
27
+ # @option options [[Symbol]] :insert_before
28
+ # array of callback names which should be sequenced after current.
29
+ # Note, that if position == :after, sequence would go backwards
30
+ # @option options [[Symbol]] :require
31
+ # array of callback names which should be sequenced before current.
32
+ # @raise [ArgumentError] if there is more than three arguments and block
33
+ # @raise [ArgumentError] if there is one argument and no block
34
+ def initialize(position, *args, &block)
35
+ super(*args, &block)
36
+
37
+ action, options = normalize_options(*args, &block)
38
+
39
+ self.name = options.fetch(:name) { action if action.is_a?(Symbol) }
40
+
41
+ self.position = position
42
+ raise "#{position} should be one of #{POSITIONS}" unless POSITIONS.include?(position)
43
+
44
+ self.before = Array(options.fetch(:insert_before, []))
45
+ self.after = Array(options.fetch(:require, []))
46
+ end
47
+
48
+ # Converts callback to nice table in dot label format
49
+ # @param [#instance_exec] binding_
50
+ # @return [String] graphviz LabelHTML
51
+ def to_dot_label(binding_)
52
+ template_path = File.expand_path("callback.dothtml.erb", __dir__)
53
+ erb = ERB.new(File.read(template_path))
54
+ erb.filename = template_path
55
+ erb.result(binding)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tsort"
4
+
5
+ module Verifly
6
+ module DependentCallbacks
7
+ # Handles callbacks with same 'group' option, allowing to do sequential invokation of them
8
+ # @attr name [Symbol] name of callback group
9
+ # @attr index [{Symbol => Callback}] index for named callback lookup
10
+ # @attr list [[Callback]] all callbacks
11
+ class CallbackGroup
12
+ # Implements topoplogy sorting of callbacks.
13
+ # As far as CallbackGroup is designed to store Callbacks, it is unable to link them into
14
+ # graph immediately . Service can do it, because if some callbacks are missing on
15
+ # compilation stage, there should be an error
16
+ # @see http://ruby-doc.org/stdlib-2.3.4/libdoc/tsort/rdoc/TSort.html
17
+ # @attr dependencies [{ Callback => Callback }] dependency graph
18
+ class TSortService
19
+ include TSort
20
+
21
+ attr_accessor :dependencies
22
+
23
+ # @param callback_group [CallbackGroup] group to be tsorted
24
+ # @return [[Callback]] tsorted callbacks array (aka sequence)
25
+ def self.call(callback_group)
26
+ new(callback_group).tsort
27
+ end
28
+
29
+ # @param callback_group [CallbackGroup] group to be tsorted
30
+ def initialize(callback_group)
31
+ self.dependencies = Hash.new { |h, k| h[k] = Set[] }
32
+
33
+ callback_group.list.each do |callback|
34
+ dependencies[callback] ||= []
35
+
36
+ callback.before.each do |key|
37
+ dependencies[callback_group.index.fetch(key)] << callback
38
+ end
39
+
40
+ callback.after.each do |key|
41
+ dependencies[callback] << callback_group.index.fetch(key)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # @api stdlib
49
+ # @see TSort
50
+ def tsort_each_node(&block)
51
+ dependencies.keys.each(&block)
52
+ end
53
+
54
+ # @api stdlib
55
+ # @see TSort
56
+ def tsort_each_child(node, &block)
57
+ dependencies[node].each(&block)
58
+ end
59
+ end
60
+
61
+ attr_accessor :index, :list, :name
62
+
63
+ # @param name [Symbol] name of callback group
64
+ # @yield self if block given
65
+ def initialize(name)
66
+ self.name = name
67
+ self.index = {}
68
+ self.list = []
69
+
70
+ yield(self) if block_given?
71
+ end
72
+
73
+ # Adds callback to list and index, reset sequence
74
+ # @param callback [Callback] new callback
75
+ def add_callback(callback)
76
+ list << callback
77
+ index[callback.name] = callback if callback.name
78
+
79
+ @sequence = nil
80
+ end
81
+
82
+ # Merges with another group
83
+ # @param other [CallbackGroup]
84
+ # @raise if group names differ
85
+ def merge(other)
86
+ raise "Only groups with one name could be merged" unless name == other.name
87
+
88
+ [*list, *other.list].each_with_object(CallbackGroup.new(name)) do |callback, group|
89
+ group.add_callback(callback)
90
+ end
91
+ end
92
+
93
+ # Memoizes tsorted graph
94
+ # @return [[Callback]]
95
+ def sequence
96
+ @sequence ||= TSortService.call(self)
97
+ end
98
+
99
+ # Digest change forces recompilation of callback group in service
100
+ # @return [Numeric]
101
+ def digest
102
+ [name, list].hash
103
+ end
104
+
105
+ # Renders graphviz dot-representation of callback group
106
+ # @return graphviz dot
107
+ def to_dot(binding_)
108
+ template_path = File.expand_path("callback_group.dot.erb", __dir__)
109
+ erb = ERB.new(File.read(template_path))
110
+ erb.filename = template_path
111
+ erb.result(binding)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+
5
+ module Verifly
6
+ module DependentCallbacks
7
+ # Simple service to invoke callback groups
8
+ # @example simple invokation
9
+ # def invoke_callbacks
10
+ # Invoker.new(group) do |invoker|
11
+ # invoker.around {}
12
+ # invoker.run(self)
13
+ # end
14
+ # end
15
+ # @see DependentCallbacks#export_callbacks_to
16
+ # @!attribute flat_sequence
17
+ # @return [[Callback]] tsorted callbacks
18
+ # @!attribute context
19
+ # @return [[Object]] invokation context. It would be passed to all applicators
20
+ # @!attribute inner_block
21
+ # @return [Proc] block in the middle of middleware
22
+ # @!visibility private
23
+ # @!attribute break_if_proc
24
+ # @note does not affect 'after' callbacks
25
+ # @return [Proc] if this block evaluate to truthy, sequence would be halted.
26
+ # @!attribute binding_
27
+ # @return [#instance_exec] binding_ to evaluate on
28
+ class Invoker
29
+ attr_accessor :flat_sequence, :context, :inner_block
30
+
31
+ # @param callback_group [CallbackGroup]
32
+ # @yield self if block given
33
+ def initialize(callback_group)
34
+ self.flat_sequence = callback_group.sequence
35
+ self.context = []
36
+ self.break_if_proc = proc { false }
37
+ yield(self) if block_given?
38
+ end
39
+
40
+ # @yield in the middle of middleware, setting inner_block attribute
41
+ # @see inner_block
42
+ def around(&block)
43
+ self.inner_block = block
44
+ end
45
+
46
+ # @yield between callbacks halting chain if evaluated to true
47
+ # @see break_if_proc
48
+ def break_if(&block)
49
+ self.break_if_proc = block
50
+ end
51
+
52
+ # Sets binding_, reduces callbacks into big proc and evaluates it
53
+ # @param binding_ [#instance_exec] binding_ to be evaluated on
54
+ # @return inner_block call result
55
+ def run(binding_)
56
+ self.binding_ = binding_
57
+ result = nil
58
+ block_with_result_extractor = -> { result = inner_block&.call }
59
+
60
+ log!(:info, "Started chain processing")
61
+
62
+ sequence =
63
+ flat_sequence.reverse_each.reduce(block_with_result_extractor) do |sequence, callback|
64
+ -> { call_callback(callback, sequence) }
65
+ end
66
+
67
+ sequence.call
68
+ result
69
+ end
70
+
71
+ private
72
+
73
+ attr_accessor :break_if_proc, :binding_
74
+
75
+ # Invokes callback in context of invoker
76
+ # @param callback [Callback] current callbacks
77
+ # @param sequence [Proc] already built sequence of callbacks
78
+ def call_callback(callback, sequence)
79
+ log!(:debug, "Invokation", callback: callback)
80
+
81
+ case callback.position
82
+ when :before then call_callback_before(callback, sequence)
83
+ when :after then call_callback_after(callback, sequence)
84
+ when :around then call_callback_around(callback, sequence)
85
+ end
86
+
87
+ nil
88
+ end
89
+
90
+ # Invokes before_<name> callbacks
91
+ # @param callback [Callback] current callbacks
92
+ # @param sequence [Proc] already built sequence of callbacks
93
+ def call_callback_before(callback, sequence)
94
+ call_with_time_report!(callback, binding_, *context)
95
+
96
+ if break_if_proc.call(*context)
97
+ log!(:warn, "Chain halted", callback: callback)
98
+ else
99
+ sequence.call
100
+ end
101
+ end
102
+
103
+ # Invokes after_<name> callbacks
104
+ # @param callback [Callback] current callbacks
105
+ # @param sequence [Proc] already built sequence of callbacks
106
+ def call_callback_after(callback, sequence)
107
+ sequence.call
108
+ call_with_time_report!(callback, binding_, *context)
109
+ end
110
+
111
+ # Invokes around_<name> callbacks
112
+ # @param callback [Callback] current callbacks
113
+ # @param sequence [Proc] already built sequence of callbacks
114
+ def call_callback_around(callback, sequence)
115
+ inner_executed = false
116
+ inner = lambda do
117
+ inner_executed = true
118
+ if break_if_proc.call(*context)
119
+ log!(:warn, "Chain halted", callback: callback)
120
+ else
121
+ sequence.call
122
+ end
123
+ end
124
+
125
+ call_with_time_report!(callback, binding_, inner, *context)
126
+
127
+ unless inner_executed
128
+ log!(:warn, "Chain halted (sequential block not called)", callback: callback)
129
+ end
130
+ end
131
+
132
+ # Logger interface to decorate messages. Uses DependentCallbacks.logger
133
+ # @param severity [:debug, :info, :warn, :error, :fatal] severity level
134
+ # @param message [String] message
135
+ # @param callback [Callback?] callback to get extra context
136
+ def log!(severity, message, callback: nil)
137
+ DependentCallbacks.logger.public_send(severity, "Verifly::DependentCallbacks::Invoker") do
138
+ if callback
139
+ <<~TXT.squish if callback
140
+ #{message} callback #{callback.name || "(anonymous)"}
141
+ in #{callback.action.source_location(binding_)&.join(':')}
142
+ TXT
143
+ else
144
+ message
145
+ end
146
+ end
147
+ end
148
+
149
+ def call_with_time_report!(callback, *args) # :nodoc:
150
+ return callback.call(*args) unless DependentCallbacks.logger.info?
151
+ time_in_ms = Benchmark.realtime { callback.call(*args) } * 1000
152
+ log!(:info, "Run in #{time_in_ms.round(1)}ms", callback: callback)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifly
4
+ module DependentCallbacks
5
+ # A service to store all callbacks info and delegate methods fom DSLs
6
+ # @attr groups [Symbol => CallbackGroup] groups index
7
+ # @attr parents [[Service]] parents in service inheritance system
8
+ class Service
9
+ attr_accessor :groups, :parents
10
+
11
+ # @param parents [[Service]] is filled in by the parent
12
+ def initialize(*parents)
13
+ self.parents = parents
14
+ self.groups = Hash.new { |h, k| h[k] = CallbackGroup.new(k) }
15
+ end
16
+
17
+ # Merges another service into this
18
+ # @param other [Service]
19
+ def merge!(other)
20
+ parents << other
21
+ end
22
+
23
+ # Adds callback into matching group
24
+ # @see Callback#initialize
25
+ # @param position [:before, :after, :around]
26
+ # @param group [Symbol] group name
27
+ # @param args callback args
28
+ def add_callback(position, group, *args, &block)
29
+ groups[group].add_callback(Callback.new(position, *args, &block))
30
+ end
31
+
32
+ def invoke(group_name)
33
+ invoker = Invoker.new(compiled_group(group_name))
34
+ yield(invoker)
35
+ end
36
+
37
+ # @return [[Symbol]] names of all groups stored inside itself or parents
38
+ def group_names
39
+ [groups.keys, *parents.map(&:group_names)].flatten.uniq
40
+ end
41
+
42
+ # Compiles callback group from itself and parents callback groups.
43
+ # If nothing changed, cached value taken
44
+ # @param group_name [Symbol] group name
45
+ # @return [CallbackGroup] callback group joined from all relative callback groups
46
+ def compiled_group(group_name)
47
+ @compiled_groups_cache ||= Hash.new { |h, k| h[k] = {} }
48
+ cache_entry = @compiled_groups_cache[group_name]
49
+ return cache_entry[:group] if cache_entry[:digest] == digest
50
+
51
+ cache_entry[:digest] = digest
52
+ cache_entry[:group] = parents.map { |parent| parent.compiled_group(group_name) }
53
+ .reduce(groups[group_name], &:merge)
54
+ end
55
+
56
+ # Digest change forces recompilation of compiled_group
57
+ # @return [Numeric]
58
+ def digest
59
+ [
60
+ *parents.map(&:digest),
61
+ *groups.map { |k, v| [k, v.digest].join },
62
+ ].hash
63
+ end
64
+
65
+ # Exprorts selected group to graphiz .dot format
66
+ def to_dot(group, binding_)
67
+ compiled_group(group).to_dot(binding_)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifly
4
+ module DependentCallbacks
5
+ # Subset of DependentCallbacks dsl methods, which could be used in callbacks storage
6
+ module Storage
7
+ # Declares callback groups with given names. This creates before_ after_ and around_
8
+ # signleton methods for each group given
9
+ # @see Service#add_callback
10
+ # @param groups [[Symbol]]
11
+ def callback_groups(*groups)
12
+ groups.each do |group|
13
+ dependent_callbacks_service.groups[group] # Creates an empty group
14
+
15
+ %i[before after around].each do |position|
16
+ define_singleton_method("#{position}_#{group}") do |*args, &block|
17
+ dependent_callbacks_service.add_callback(position, group, *args, &block)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Merges all callbacks from given storage
24
+ # @param storage [Module { extend Storage }]
25
+ def merge_callbacks_from(storage)
26
+ include(storage)
27
+ dependent_callbacks_service.merge!(storage.dependent_callbacks_service)
28
+ end
29
+
30
+ # @return [Service] associated with current Class / Module
31
+ def dependent_callbacks_service
32
+ @dependent_callbacks_service ||= Service.new
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Verifly
6
+ # Mixin with logger attr_accessor and default value for it
7
+ # @!attribute logger
8
+ # @return [::Logger] logger to be used within target module
9
+ module HasLogger
10
+ # Does nothing, provides ::Logger api
11
+ class NullLogger < ::Logger
12
+ # @api stdlib
13
+ def initialize
14
+ super(nil)
15
+ end
16
+
17
+ # @api stdlib
18
+ # Logs nothing
19
+ def add(*); end
20
+ end
21
+
22
+ attr_writer :logger
23
+
24
+ def logger
25
+ @logger ||= NullLogger.new
26
+ end
27
+ end
28
+ end
@@ -11,7 +11,7 @@ module Verifly
11
11
  # Array with all messages yielded by the verifier
12
12
  class Verifier
13
13
  autoload :ApplicatorWithOptionsBuilder,
14
- 'verifly/verifier/applicator_with_options_builder'
14
+ "verifly/verifier/applicator_with_options_builder"
15
15
 
16
16
  attr_accessor :model, :messages
17
17
 
@@ -65,7 +65,7 @@ module Verifly
65
65
  end
66
66
  end
67
67
 
68
- # @return [Array(ApplicatorWithOptions)]
68
+ # @return [[ApplicatorWithOptions]]
69
69
  # List of applicators, bound by .verify
70
70
  def self.bound_applicators
71
71
  @bound_applicators ||= []
@@ -8,5 +8,5 @@ module Verifly
8
8
  # * a stands for public api changes
9
9
  # * b stands for private api changes
10
10
  # * c stands for patch changes (not touching public or private api)
11
- VERSION = '0.2.0.1'
11
+ VERSION = "0.3.1.0"
12
12
  end
metadata CHANGED
@@ -1,157 +1,171 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verifly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.1
4
+ version: 0.3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Smirnov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-10 00:00:00.000000000 Z
11
+ date: 2020-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pry
14
+ name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0.10'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0.10'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: coveralls
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.14'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: launchy
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '10.5'
47
+ version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '10.5'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '3.5'
61
+ version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '3.5'
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rubocop
70
+ name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '0.48'
75
+ version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0.48'
82
+ version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: yard
84
+ name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 0.9.8
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 0.9.8
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: launchy
98
+ name: rspec-its
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '2.4'
103
+ version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '2.4'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: coveralls
112
+ name: yard
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: 0.8.19
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: 0.8.19
124
+ version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: rubocop-rspec
126
+ name: actionpack
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '1.15'
131
+ version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - "~>"
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '1.15'
138
+ version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
- name: rspec-its
140
+ name: activesupport
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "~>"
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '1.1'
145
+ version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - "~>"
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-config-umbrellio
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
151
158
  - !ruby/object:Gem::Version
152
- version: '1.1'
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  description: 'An api to run sequential checks like ''ActiveModel::Validations'' do,
154
- but with generic messages instead of errors. See more info at [http://www.rubydoc.info/gems/verifly/0.2.0.1] '
168
+ but with generic messages instead of errors. See more info at https://www.rubydoc.info/gems/verifly/0.3.1.0 '
155
169
  email:
156
170
  - begdory4@gmail.com
157
171
  executables: []
@@ -163,6 +177,13 @@ files:
163
177
  - lib/verifly/applicator.rb
164
178
  - lib/verifly/applicator_with_options.rb
165
179
  - lib/verifly/class_builder.rb
180
+ - lib/verifly/dependent_callbacks.rb
181
+ - lib/verifly/dependent_callbacks/callback.rb
182
+ - lib/verifly/dependent_callbacks/callback_group.rb
183
+ - lib/verifly/dependent_callbacks/invoker.rb
184
+ - lib/verifly/dependent_callbacks/service.rb
185
+ - lib/verifly/dependent_callbacks/storage.rb
186
+ - lib/verifly/has_logger.rb
166
187
  - lib/verifly/verifier.rb
167
188
  - lib/verifly/version.rb
168
189
  homepage: https://github.com/umbrellio/verifly
@@ -170,24 +191,23 @@ licenses:
170
191
  - MIT
171
192
  metadata:
172
193
  yard.run: yri
173
- post_install_message:
194
+ post_install_message:
174
195
  rdoc_options: []
175
196
  require_paths:
176
197
  - lib
177
198
  required_ruby_version: !ruby/object:Gem::Requirement
178
199
  requirements:
179
- - - "~>"
200
+ - - ">="
180
201
  - !ruby/object:Gem::Version
181
- version: '2.3'
202
+ version: '2.5'
182
203
  required_rubygems_version: !ruby/object:Gem::Requirement
183
204
  requirements:
184
205
  - - ">="
185
206
  - !ruby/object:Gem::Version
186
207
  version: '0'
187
208
  requirements: []
188
- rubyforge_project:
189
- rubygems_version: 2.5.2
190
- signing_key:
209
+ rubygems_version: 3.1.2
210
+ signing_key:
191
211
  specification_version: 4
192
- summary: See more info at [http://www.rubydoc.info/gems/verifly/0.2.0.1]
212
+ summary: See more info at https://www.rubydoc.info/gems/verifly/0.3.1.0
193
213
  test_files: []