ni 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ module Ni::ExpressionsLanguageEngine
2
+ class Processor
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Ni::ExpressionsLanguageEngine
2
+ class Tokenizer
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Ni::Flows
2
+ class Base
3
+ def call
4
+ raise 'Not Implemented'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ module Ni::Flows
2
+ class BranchInteractor < Base
3
+ include Ni::Flows::Utils::HandleWait
4
+
5
+ attr_accessor :interactor_klass, :action, :condition
6
+
7
+ def initialize(top_level_interactor_klass, args, options, &block)
8
+ if options[:when].blank? || !options[:when].is_a?(Proc)
9
+ raise "Can't build brunch without a condition"
10
+ else
11
+ self.condition = options[:when]
12
+ end
13
+
14
+ if args.size == 2
15
+ interactor_klass, action = args
16
+ else
17
+ id_or_interactor = args.first
18
+ action = :perform
19
+
20
+ if id_or_interactor.is_a?(Symbol)
21
+ interactor_klass = build_anonymous_interactor(top_level_interactor_klass, id_or_interactor, &block)
22
+ else
23
+ interactor_klass = id_or_interactor
24
+ end
25
+ end
26
+
27
+ self.interactor_klass, self.action = interactor_klass, action
28
+ end
29
+
30
+ def call(context, params={})
31
+ return unless self.condition.call(context)
32
+
33
+ run(context, params)
34
+ end
35
+
36
+ def call_for_wait_continue(context, params={})
37
+ call(context, params)
38
+ end
39
+
40
+ def handle_current_wait?(context, name)
41
+ self.condition.call(context) && super
42
+ end
43
+
44
+ private
45
+
46
+ def run(context, params={})
47
+ interactor_klass.public_send(action, context, params)
48
+ end
49
+
50
+ def build_anonymous_interactor(top_level_interactor_klass, id, &block)
51
+ unless block_given?
52
+ raise "Can't build a branch without a block given"
53
+ end
54
+
55
+ klass = Class.new
56
+ klass.include Ni::Main
57
+ klass.unique_id id
58
+ klass.copy_storage_and_metadata_repository(top_level_interactor_klass)
59
+
60
+ klass.instance_eval &block
61
+
62
+ klass
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ module Ni::Flows
2
+ class InlineInteractor < Base
3
+ include Ni::Flows::Utils::HandleWait
4
+
5
+ attr_accessor :interactor_klass, :action, :on_cancel, :on_failure, :on_terminate
6
+
7
+ def initialize(interactor_klass, action, options={})
8
+ self.on_cancel = options[:on_cancel]
9
+ self.on_failure = options[:on_failure]
10
+ self.on_terminate = options[:on_terminate]
11
+
12
+ self.interactor_klass, self.action = interactor_klass, action
13
+ end
14
+
15
+ def call(context, params={})
16
+ interactor_klass.public_send(action, context, params)
17
+ end
18
+
19
+ def call_for_wait_continue(context, params={})
20
+ call(context, params)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Ni::Flows
2
+ class IsolatedInlineInteractor < Base
3
+ attr_accessor :interactor_klass, :action, :receive_list, :provide_list
4
+
5
+ def initialize(interactor_klass, action, options={})
6
+ self.interactor_klass, self.action = interactor_klass, action
7
+ self.receive_list, self.provide_list = Array(options[:receive]), Array(options[:provide])
8
+ end
9
+
10
+ def call(context)
11
+ isolated_context = Ni::Context.new(nil, action)
12
+ isolated_context.assign_data!(context.slice(*receive_list))
13
+
14
+ result = interactor_klass.public_send(action, isolated_context)
15
+
16
+ provide_list.each do |param_name|
17
+ context[param_name] = result.context[param_name]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module Ni::Flows::Utils
2
+ module HandleWait
3
+ def handle_current_wait?(context, name)
4
+ interactor_klass.units_by_interface(:waited_for_name?).any? { |unit| unit.waited_for_name?(name) } ||
5
+ check_all_subtree_for_wait(context, name, interactor_klass.units_by_interface(:handle_current_wait?))
6
+ end
7
+
8
+ def call_for_wait_continue(context, params={})
9
+ raise "Not implemented"
10
+ end
11
+
12
+ private
13
+
14
+ def check_all_subtree_for_wait(context, name, units)
15
+ units.any? do |unit|
16
+ next_level_units = unit.interactor_klass.units_by_interface(:handle_current_wait?)
17
+
18
+ unit.handle_current_wait?(context, name) || (next_level_units.any? && check_all_subtree_for_wait(context, name, next_level_units) )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ module Ni::Flows
2
+ class WaitForCondition
3
+ SKIP = :skip
4
+ WAIT = :wait
5
+ COMPLETED = :completed
6
+
7
+ METADATA_REPOSITORY_KEY = 'multiple_wait_for'
8
+
9
+ attr_accessor :condition, :timer, :wait_id
10
+
11
+ def initialize(condition, interactor_klass, options={})
12
+ self.timer = options[:timer]
13
+
14
+ condition = condition.interactor_id! if condition.is_a?(Class)
15
+
16
+ self.condition = condition
17
+ end
18
+
19
+ def waited_for_name?(name)
20
+ case condition
21
+ when Symbol # wait_for(:some_unique_name)
22
+ self.condition == name
23
+ when Class #wait_for(OtherInteractor)
24
+ self.condition.interactor_id! == name
25
+ when Hash #wait_for(:main_key => [:name, SomeClass, [:name, -> (ctx) { ctx.a == 1 }]])
26
+ self.condition.values
27
+ .flatten
28
+ .select { |item| item.is_a?(Symbol) || item.is_a?(Class) }
29
+ .map { |item| item.is_a?(Class) ? item.interactor_id! : item }
30
+ .include?(name)
31
+ else
32
+ raise "Wrong WaitFor options"
33
+ end
34
+ end
35
+
36
+ def wait_or_continue(check, context, metadata_repository_klass)
37
+ if condition.is_a?(Symbol)
38
+ single_condition(check)
39
+ elsif condition.is_a?(Hash)
40
+ multiple_condition(check, context, metadata_repository_klass)
41
+ else
42
+ raise "Condition format doesn't recognized"
43
+ end
44
+ end
45
+
46
+ def setup_timer!(context, metadata_repository_klass)
47
+ return unless self.timer.present?
48
+
49
+ datetime_proc, timer_klass, timer_action = self.timer
50
+ datetime = datetime_proc.call
51
+ timer_action ||= :perform
52
+
53
+ unique_timer_id = "#{self.wait_id}-#{context.system_uid}"
54
+
55
+ metadata_repository_klass.setup_timer!(unique_timer_id, datetime, timer_klass.name, timer_action.to_s, contex.system_uid)
56
+ end
57
+
58
+ def clear_timer!(context, metadata_repository_klass)
59
+ return unless self.timer.present?
60
+
61
+ unique_timer_id = "#{self.wait_id}-#{context.system_uid}"
62
+
63
+ metadata_repository_klass.clear_timer!(unique_timer_id)
64
+ end
65
+
66
+ private
67
+
68
+ def single_condition(check)
69
+ condition == check ? COMPLETED : SKIP
70
+ end
71
+
72
+ def multiple_condition(check, context, metadata_repository)
73
+ unless metadata_repository.present?
74
+ raise "Multiple waits expects the metadata repository definition"
75
+ end
76
+
77
+ global_name = condition.keys.first
78
+ conditions_list = condition.values.first
79
+
80
+ conditions_names = conditions_list.flatten.select { |name| name.is_a?(Symbol) }
81
+
82
+ unless conditions_names.include?(check)
83
+ return SKIP
84
+ end
85
+
86
+ passed_cheks = []
87
+
88
+ metadata = metadata_repository.fetch(context.system_uid, METADATA_REPOSITORY_KEY)
89
+ if metadata.present?
90
+ passed_cheks += metadata.map(&:to_sym)
91
+ end
92
+
93
+ conditions_list.each do |checked_condition|
94
+ if checked_condition.is_a?(Symbol)
95
+ if checked_condition == check
96
+ passed_cheks << check
97
+ break
98
+ else
99
+ next
100
+ end
101
+ elsif checked_condition.is_a?(Array) && checked_condition.first.is_a?(Symbol) && checked_condition.last.is_a?(Proc)
102
+ name, callback = checked_condition
103
+
104
+ if name == check
105
+ passed_cheks << check if context.current_interactor.instance_exec(context, &callback)
106
+ break
107
+ else
108
+ next
109
+ end
110
+ else
111
+ raise "Multiple waits can contain only symbols or arrays with symbol and proc"
112
+ end
113
+ end
114
+
115
+ unless passed_cheks.present?
116
+ return WAIT
117
+ end
118
+
119
+ metadata_repository.store(context.system_uid, METADATA_REPOSITORY_KEY, passed_cheks)
120
+
121
+ if (conditions_names - passed_cheks).empty?
122
+ COMPLETED
123
+ else
124
+ WAIT
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,69 @@
1
+ #require 'colorize'
2
+
3
+ module Ni
4
+ module Help
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def help(action=:perform, indent=0)
9
+ disp = -> (str) { puts '' * indent + str }
10
+
11
+ disp "#{'Interactor'.bold} #{('='*15).bold}> #{self.class.name.colorize(:blue)} <#{('='*15).bold} "
12
+ disp "#{'Description'.bold}: #{self.class.description}"
13
+ disp "#{'Pefrormed Action'.bold}: #{action.to_s.colorize(:green)} - #{self.class.defined_actions[:action].description.to_s.colorize(:green)}"
14
+
15
+ disp ''
16
+
17
+ if self.respond_to?(:select_contracts_for_action, true)
18
+ disp 'Input parameters:'.bold
19
+ select_contracts_for_action(self.class.pop_contracts, action).each do |name, contract|
20
+ disp "#{name.to_s.bold.yellow} - #{contract[:description] || 'No description'}"
21
+ end
22
+ disp ''
23
+
24
+ disp 'Mutated parameters:'.bold
25
+ select_contracts_for_action(self.class.mutate_contracts, action).each do |name, contract|
26
+ disp "#{name.to_s.bold.yellow} - #{contract[:description] || 'No description'}"
27
+ end
28
+ disp ''
29
+
30
+ disp 'Output parameters:'.bold
31
+ select_contracts_for_action(self.class.push_contracts, action).each do |name, contract|
32
+ disp "#{name.to_s.bold.yellow} - #{contract[:description] || 'No description'}"
33
+ end
34
+ disp ''
35
+ end
36
+
37
+ defined_actions[name].units.each do |unit|
38
+ if unit.is_a?(Proc)
39
+ disp 'Proc body, can not read'
40
+ elsif unit.is_a?(Array)
41
+ disp 'Call another interator in chain'
42
+ unit.first.help(unit.last, (indent + 1) * 2)
43
+ elsif unit.is_a?(Symbol)
44
+ disp "Call method #{unit}. Source can't be read"
45
+ else
46
+ disp 'Call another interator in chain'
47
+ unit.help(:perform, (indent + 1) * 2)
48
+ end
49
+ end
50
+ end
51
+
52
+ def desc(description)
53
+ @__ni_desription = description
54
+ end
55
+
56
+ def description
57
+ @__ni_desription
58
+ end
59
+
60
+ def title(title=nil)
61
+ @__ni_title ||= title
62
+ end
63
+
64
+ def title!
65
+ @__ni_title || raise("The title is required for #{self.name}")
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,319 @@
1
+ module Ni
2
+ module Main
3
+ extend ActiveSupport::Concern
4
+
5
+ include Ni::Params
6
+ include Ni::StoragesConfig
7
+ include Ni::Help
8
+
9
+ module ModuleMethods
10
+ def register_unique_interactor(id, interactor_klass)
11
+ @unique_ids_map ||= {}
12
+
13
+ # ruby has a strange behaviour here while comparing classes.
14
+ # Think it's an Rails autoload issue, but should compare the class names
15
+ if @unique_ids_map[id].present?
16
+ if interactor_klass.name.present? && @unique_ids_map[id].name != interactor_klass.name
17
+ raise "Try to register new interactor with the existing ID: #{id}"
18
+ end
19
+ end
20
+
21
+ @unique_ids_map[id] = interactor_klass
22
+ end
23
+ end
24
+
25
+ extend ModuleMethods
26
+
27
+
28
+ included do
29
+ attr_accessor :context
30
+
31
+ delegate :success?, :valid?, :can_perform_next_step?, :errors, to: :context
32
+
33
+ def failure?
34
+ !success?
35
+ end
36
+
37
+ def initialize
38
+ self.class.define_actions! if need_to_define_actions?
39
+ end
40
+
41
+ def need_to_define_actions?
42
+ self.class.defined_actions.present? &&
43
+ self.class.defined_actions.keys.map(&:first).any? { |name| !respond_to?(name) }
44
+ end
45
+
46
+ def returned_values(name)
47
+ keys = self.class.action_by_name(name)&.returned_values.presence ||
48
+ self.select_contracts_for_action(self.class.provide_contracts, name)&.keys
49
+
50
+ Array(keys).map { |key| context.raw_get(key) }
51
+ end
52
+
53
+ def safe_context(action)
54
+ context.within_interactor self, action do
55
+ ensure_received_params(action)
56
+ result = yield
57
+ ensure_provided_params(action)
58
+
59
+ result
60
+ end
61
+ end
62
+
63
+ def handle_exceptions(action_exceptions)
64
+ yield
65
+
66
+ nil
67
+ rescue Exception => ex
68
+ _, rescue_callback = action_exceptions.find { |(rescue_list, _)| rescue_list.include?(ex.class) }
69
+
70
+ rescue_callback ||= begin
71
+ callbacks_array = action_exceptions.find do |(rescue_list, _)|
72
+ rescue_list.any? { |exception_class| ex.class <= exception_class }
73
+ end
74
+ callbacks_array&.last
75
+ end
76
+
77
+ raise ex unless rescue_callback.present?
78
+
79
+ [rescue_callback, ex]
80
+ end
81
+
82
+ def before_action(action_name)
83
+ # Can be defined in ancestors
84
+ end
85
+
86
+ def after_action(action_name)
87
+ # Can be defined in ancestors
88
+ end
89
+
90
+ def on_success(action_name)
91
+ # Can be defined in ancestors
92
+ end
93
+
94
+ def on_cancel(action_name)
95
+ # Can be defined in ancestors
96
+ end
97
+
98
+ def on_terminate(action_name)
99
+ # Can be defined in ancestors
100
+ end
101
+
102
+ def on_failure(action_name)
103
+ # Can be defined in ancestors
104
+ end
105
+
106
+ def on_context_restored(action_name)
107
+ # Can be defined in ancestors
108
+ end
109
+
110
+ def on_checking_continue_signal(unit)
111
+ # Can be defined in ancestors
112
+ end
113
+
114
+ def on_continue_signal_checked(unit, wait_cheking_result)
115
+ # Can be defined in ancestors
116
+ end
117
+ end
118
+
119
+ module ClassMethods
120
+ def unique_id(id=nil)
121
+ @unique_interactor_id = id
122
+ Ni::Main.register_unique_interactor(interactor_id, self)
123
+ end
124
+
125
+ # without specified ID will use class name
126
+ def interactor_id
127
+ @unique_interactor_id || name
128
+ end
129
+
130
+ def interactor_id!
131
+ @unique_interactor_id || raise("The #{self.name} requires an explicit definition of the unique id")
132
+ end
133
+
134
+ def action(*args, &block)
135
+ self.defined_actions ||= {}
136
+
137
+ name, description = args
138
+ description ||= 'No description'
139
+
140
+ ActionChain.new(self, name, description, &block)
141
+ ensure
142
+ unless respond_to?(name)
143
+ define_singleton_method name do |*args, **params|
144
+ context = args.first
145
+
146
+ perform_custom(name, context, params)
147
+ end
148
+ end
149
+ end
150
+
151
+ attr_accessor :defined_actions
152
+
153
+ def perform(*args, **params)
154
+ context = args.first
155
+
156
+ perform_custom(:perform, context, params)
157
+ end
158
+
159
+ def perform_custom(*args, **params)
160
+ object = self.new
161
+
162
+ name, context = args
163
+
164
+ system_uid = params.delete(:system_uid)
165
+ wait_completed_for = params.delete(:wait_completed_for)
166
+
167
+ context ||= Ni::Context.new(object, name, system_uid)
168
+ context.continue_from!(wait_completed_for) if wait_completed_for.present?
169
+ context.assign_data!(params)
170
+ context.assign_current_interactor!(object)
171
+
172
+ object.context = context
173
+ object.public_send(name)
174
+
175
+ Ni::Result.new object.context.resultify!, object.returned_values(name)
176
+ end
177
+
178
+ def define_actions!
179
+ defined_actions.keys.map(&:first).each { |name| define_action!(name) }
180
+ end
181
+
182
+ def define_action!(name)
183
+ raise 'Action not described' unless action_by_name(name).present?
184
+
185
+ action_units = action_by_name(name).units
186
+ action_failure_callback = action_by_name(name).failure_callback
187
+ action_exceptions = action_by_name(name).rescues
188
+
189
+ define_method name do
190
+ if context.should_be_restored?
191
+ unless self.class.context_storage_klass.present? && self.class.metadata_repository_klass.present?
192
+ raise "Storages was not configured"
193
+ end
194
+
195
+ self.class.context_storage_klass.new(context, self.class.metadata_repository_klass).fetch
196
+ on_context_restored(name)
197
+ end
198
+
199
+ before_action(name)
200
+
201
+ action_units.each do |unit|
202
+ return if context.execution_halted?
203
+ return if context.chain_skipped?
204
+
205
+ if context.wait_for_execution?
206
+ # Send to other interactor chain
207
+ if unit.respond_to?(:handle_current_wait?) && unit.handle_current_wait?(context, context.continue_from)
208
+ rescue_callback, ex = safe_context name do
209
+ handle_exceptions action_exceptions do
210
+ unit.call_for_wait_continue(self.context, wait_completed_for: context.continue_from, system_uid: context.system_uid)
211
+ end
212
+ end
213
+ next unless rescue_callback.present?
214
+ elsif unit.is_a?(Ni::Flows::WaitForCondition)
215
+
216
+ on_checking_continue_signal(unit)
217
+ wait_cheking_result = unit.wait_or_continue(context.continue_from, context, self.class.metadata_repository_klass)
218
+ on_continue_signal_checked(unit, wait_cheking_result)
219
+
220
+ case wait_cheking_result
221
+ when Ni::Flows::WaitForCondition::SKIP
222
+ next
223
+ when Ni::Flows::WaitForCondition::WAIT
224
+ return
225
+ when Ni::Flows::WaitForCondition::COMPLETED
226
+ context.wait_completed!
227
+ unit.clear_timer!(context, self.class.metadata_repository_klass)
228
+ next
229
+ end
230
+ else
231
+ next
232
+ end
233
+ end
234
+
235
+
236
+ # This can't be replaced with if ... else
237
+ # The wait checking hooks can change the context so need to ensure if block can be performed
238
+ # And from other side the performing block is also able to change context and terminate execution
239
+ if can_perform_next_step?
240
+ # Previous step could send flow to existing chain
241
+ rescue_callback, ex = safe_context name do
242
+ handle_exceptions action_exceptions do
243
+ if unit.is_a?(Proc)
244
+ instance_eval(&unit)
245
+ elsif unit.is_a?(Symbol)
246
+ send(unit)
247
+ elsif unit.is_a?(String)
248
+ unit.to_s.split('.').reduce(self) {|memo, name| memo.send(name) }
249
+ elsif unit.kind_of?(Ni::Flows::Base)
250
+ unit.call(self.context)
251
+ elsif unit.kind_of?(Ni::Flows::WaitForCondition)
252
+ if self.class.context_storage_klass.present? && self.class.metadata_repository_klass.present?
253
+ self.class.context_storage_klass.new(context, self.class.metadata_repository_klass).store
254
+ else
255
+ raise "WaitFor require a store and metadata repository"
256
+ end
257
+
258
+ unit.setup_timer!(context, self.class.metadata_repository_klass)
259
+ context.halt_execution!
260
+
261
+ return
262
+ end
263
+ end
264
+ end
265
+
266
+ if rescue_callback.present?
267
+ instance_exec(ex, &rescue_callback)
268
+
269
+ context.failure!
270
+ end
271
+ end
272
+
273
+ unless can_perform_next_step?
274
+ instance_eval(&action_failure_callback) if context.failed? && action_failure_callback.present?
275
+
276
+ {
277
+ :failed? => :on_failure,
278
+ :canceled? => :on_cancel,
279
+ :terminated? => :on_terminate
280
+ }.each do |predicate, callback|
281
+ if context.public_send(predicate)
282
+ if unit.respond_to?(callback) && unit.public_send(callback).present?
283
+ if unit.public_send(callback).is_a?(Proc)
284
+ instance_exec(&unit.public_send(callback))
285
+ end
286
+
287
+ if unit.public_send(callback).is_a?(Class)
288
+ unit.public_send(callback).perform(context)
289
+ end
290
+ end
291
+
292
+ self.public_send(callback, name)
293
+ end
294
+ end
295
+
296
+ after_action(name)
297
+ return
298
+ end
299
+ end
300
+
301
+ after_action(name)
302
+ on_success(name)
303
+ end
304
+ end
305
+
306
+ def action_by_name(name)
307
+ return nil unless defined_actions.present?
308
+
309
+ action = defined_actions.find { |(action_name, _), _| action_name == name }
310
+
311
+ Array(action).last
312
+ end
313
+
314
+ def units_by_interface(interface)
315
+ defined_actions.values.map(&:units).flatten.select { |u| u.respond_to?(interface) }
316
+ end
317
+ end
318
+ end
319
+ end