verifly 0.2.0.1 → 0.3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []