kanal 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/Gemfile.lock +2 -2
  4. data/kanal.gemspec +1 -0
  5. data/lib/kanal/core/conditions/condition.rb +11 -1
  6. data/lib/kanal/core/conditions/condition_creator.rb +21 -2
  7. data/lib/kanal/core/conditions/condition_pack.rb +23 -3
  8. data/lib/kanal/core/conditions/condition_pack_creator.rb +17 -2
  9. data/lib/kanal/core/conditions/condition_storage.rb +18 -2
  10. data/lib/kanal/core/core.rb +19 -1
  11. data/lib/kanal/core/helpers/condition_finder.rb +3 -2
  12. data/lib/kanal/core/helpers/parameter_bag_with_registrator.rb +10 -0
  13. data/lib/kanal/core/helpers/parameter_finder_with_method_missing_mixin.rb +6 -8
  14. data/lib/kanal/core/helpers/parameter_registrator.rb +13 -1
  15. data/lib/kanal/core/helpers/queue.rb +38 -0
  16. data/lib/kanal/core/helpers/response_block.rb +20 -0
  17. data/lib/kanal/core/helpers/response_execution_block.rb +77 -0
  18. data/lib/kanal/core/helpers/router_proc_parser.rb +2 -0
  19. data/lib/kanal/core/hooks/hook_storage.rb +11 -4
  20. data/lib/kanal/core/interfaces/interface.rb +13 -0
  21. data/lib/kanal/core/logger/logging.rb +26 -0
  22. data/lib/kanal/core/output/output_creator.rb +3 -3
  23. data/lib/kanal/core/plugins/plugin.rb +1 -1
  24. data/lib/kanal/core/router/router.rb +104 -10
  25. data/lib/kanal/core/router/router_node.rb +21 -11
  26. data/lib/kanal/core/router/router_storage.rb +2 -0
  27. data/lib/kanal/core/services/service_container.rb +24 -7
  28. data/lib/kanal/plugins/batteries/attachments/attachment.rb +163 -0
  29. data/lib/kanal/plugins/batteries/batteries_plugin.rb +16 -0
  30. data/lib/kanal/version.rb +1 -1
  31. metadata +8 -3
  32. data/lib/kanal/core/logger/logger.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45f85bfae55ce0c37d296113ba2af2933667fd3fd7ec9e181ff12dd17105b524
4
- data.tar.gz: 9f160009467a4cbf0412d266ae693b279064286ca91ec53e5ad55bb93582d31a
3
+ metadata.gz: 22224712e17434c60d3b437d44a35a5a997dd739cddcf53eb365d588fdf19fcd
4
+ data.tar.gz: f6677a4a8bc6eacf2e41469ba4a36da1b324d3b7d9092ae9271aa1b9f3fa8be6
5
5
  SHA512:
6
- metadata.gz: f4275c770b3607a4617e5413a4634e2436fcb245af8dd3c6d3c5a86b39b64a70020f87ddb4c23323b9fcf3b640fd45bf572992edee3041bb5b918d6989fc79ab
7
- data.tar.gz: 9c11421ecd4b230ab8d994b17a016b9e9649c9820ae9ad04503c8bbfa77798fd323fda5249c47cc6bd712d19493a7373d68dbb78fe559f08f53d4a36194776d3
6
+ metadata.gz: 5e1416fb809513b53627cb120d8b09dfb76cae30f952de0212a1c2edde48e01d733afb122189230d0e6426dba812d143a8b2fc35b8561fba95954ea0b0bccbe2
7
+ data.tar.gz: 3dcc90d49fa16f4df0482d77a94f3f21a7ddc12742983a8925714199100284bfb4d84a96bef19aa289c3ad1ebcc21996d3a27fb9874837b24ca343ba53f700d6
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.3.0]
3
+ ## [0.4.1] 2023-03-16
4
+ - Added Attachment that can be used inside new input parameters in Batteries plugin
5
+ - New input and output parameters in Batteries plugin: image, audio, file
6
+
7
+ ## [0.4.0] 2023-03-10
8
+ - Added logging feature to Kanal, for a time being - just stdout
9
+ - Error response, now Kanal Router accepts error response block, when things go haywire inside constructing output
10
+ - Async response available, now developers can utilize respond_async(&block) which will be executed in separate thread
11
+
12
+ ## [0.3.0] - 2023-01-10
4
13
  - Kanal::Core::Core.get_plugin method added to get plugins for additional configuration by other plugins or developer code, if needed
5
14
  - Kanal::Core::Plugins::Plugin.rake_tasks method introduced, for plugins to have their own rake tasks that can be merged inside
6
15
  some kind of parent rake tasks, whether it's users Rakefile or kanal framework/interface or something Rakefile
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kanal (0.3.0)
4
+ kanal (0.5.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -105,4 +105,4 @@ DEPENDENCIES
105
105
  yard
106
106
 
107
107
  BUNDLED WITH
108
- 2.3.12
108
+ 2.3.19
data/kanal.gemspec CHANGED
@@ -36,4 +36,5 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  # For more information and examples about making a new gem, check out our
38
38
  # guide at: https://bundler.io/guides/creating_gem.html
39
+ spec.metadata["rubygems_mfa_required"] = "true"
39
40
  end
@@ -1,15 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logger/logging"
4
+
1
5
  module Kanal
2
6
  module Core
3
7
  module Conditions
4
8
  # Base class for conditions
5
9
  # with this class you can
6
10
  class Condition
11
+ include Logging
12
+
7
13
  attr_reader :name
8
14
 
9
15
  def initialize(name, with_argument: false, &met_block)
10
16
  @name = name
11
17
 
12
- raise "Cannot create condition without block" unless met_block
18
+ unless met_block
19
+ logger.fatal "Attempted to create condition #{name} without block"
20
+
21
+ raise "Cannot create condition without block"
22
+ end
13
23
 
14
24
  @with_argument = with_argument
15
25
 
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logger/logging"
4
+
1
5
  module Kanal
2
6
  module Core
3
7
  module Conditions
4
8
  # This class helps creating conditions in dsl way,
5
9
  # with using helper methods
6
10
  class ConditionCreator
11
+ include Logging
12
+
7
13
  def initialize(name)
8
14
  @name = name
9
15
  @met_block = nil
@@ -11,10 +17,23 @@ module Kanal
11
17
  end
12
18
 
13
19
  def create(&block)
20
+ logger.info "Attempting to create condition '#{@name}'"
21
+
14
22
  instance_eval(&block)
15
23
 
16
- raise "Please provide name for condition" unless @name
17
- raise "Please provide met? block for condition #{@name}" unless @met_block
24
+ unless @name
25
+ logger.fatal "Attempted to create condition without name"
26
+
27
+ raise "Please provide name for condition"
28
+ end
29
+
30
+ unless @met_block
31
+ logger.fatal "Attempted to create condition without met block"
32
+
33
+ raise "Please provide met? block for condition #{@name}"
34
+ end
35
+
36
+ logger.info "Creating condition '#{@name}'"
18
37
 
19
38
  Condition.new @name, with_argument: @with_argument, &@met_block
20
39
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "./condition"
2
4
  require_relative "./condition_creator"
5
+ require_relative "../logger/logging"
3
6
 
4
7
  module Kanal
5
8
  module Core
@@ -8,6 +11,8 @@ module Kanal
8
11
  # It is served as some kind of namespace for conditions, with specific
9
12
  # name of pack and helper methods
10
13
  class ConditionPack
14
+ include Logging
15
+
11
16
  attr_reader :name
12
17
 
13
18
  def initialize(name)
@@ -18,7 +23,11 @@ module Kanal
18
23
  def get_condition_by_name!(name)
19
24
  condition = get_condition_by_name name
20
25
 
21
- raise "Condition #{name} was not found in pack #{@name}. Maybe it was not added?" unless condition
26
+ unless condition
27
+ logger.fatal "Attempted to get condition #{name} in pack #{@name}"
28
+
29
+ raise "Condition #{name} was not found in pack #{@name}. Maybe it was not added?"
30
+ end
22
31
 
23
32
  condition
24
33
  end
@@ -28,9 +37,20 @@ module Kanal
28
37
  end
29
38
 
30
39
  def register_condition(condition)
31
- raise "Can register only conditions that inherit Condition class" unless condition.is_a? Condition
40
+ logger.info "Attempting to register condition '#{condition.name}'"
41
+
42
+ unless condition.is_a? Condition
43
+ logger.fatal "Attempted to register condition which isn't of Condition class"
44
+
45
+ raise "Can register only conditions that inherit Condition class"
46
+ end
47
+
48
+ if condition_registered? condition
49
+ logger.warn "Condition '#{condition.name}' already registered"
50
+ return self
51
+ end
32
52
 
33
- return self if condition_registered? condition
53
+ logger.info "Registering condition '#{condition.name}'"
34
54
 
35
55
  @conditions.append condition
36
56
 
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "./condition_pack"
2
4
  require_relative "./condition_creator"
5
+ require_relative "../logger/logging"
3
6
 
4
7
  module Kanal
5
8
  module Core
@@ -7,6 +10,8 @@ module Kanal
7
10
  # This class helps in condition pack creation
8
11
  # with the help of dsl
9
12
  class ConditionPackCreator
13
+ include Logging
14
+
10
15
  TEMP_NAME = :temp_name
11
16
 
12
17
  def initialize(name)
@@ -17,9 +22,19 @@ module Kanal
17
22
  def create(&block)
18
23
  instance_eval(&block)
19
24
 
20
- raise "Please provide condition pack name" unless @name
25
+ unless @name
26
+ logger.fatal "Attempted to create condition pack without name"
27
+
28
+ raise "Please provide condition pack name"
29
+ end
30
+
31
+ if @conditions.empty?
32
+ logger.fatal "Attempted to create condition pack #{@name} without conditions provided"
33
+
34
+ raise "Please provide conditions for condition pack #{@name}"
35
+ end
21
36
 
22
- raise "Please provide conditions for condition pack #{@name}" if @conditions.empty?
37
+ logger.info "Creating condition pack '#{@name}'"
23
38
 
24
39
  pack = ConditionPack.new(@name)
25
40
 
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logger/logging"
4
+
1
5
  module Kanal
2
6
  module Core
3
7
  module Conditions
4
8
  # This class contains all needed functionality to store,
5
9
  # search conditions
6
10
  class ConditionStorage
11
+ include Logging
12
+
7
13
  def initialize
8
14
  @condition_packs = []
9
15
  end
@@ -11,7 +17,11 @@ module Kanal
11
17
  def get_condition_pack_by_name!(name)
12
18
  pack = get_condition_pack_by_name name
13
19
 
14
- raise "Condition pack #{name} is not registered, but was requested from ConditionStorage" unless pack
20
+ unless pack
21
+ logger.fatal "Attempted to request unregistered condition pack #{name} from ConditionStorage"
22
+
23
+ raise "Condition pack #{name} is not registered, but was requested from ConditionStorage"
24
+ end
15
25
 
16
26
  pack
17
27
  end
@@ -44,7 +54,13 @@ module Kanal
44
54
  def register_condition_pack(pack)
45
55
  return if condition_pack_exists? pack
46
56
 
47
- raise "Condition pack should be descendant of ConditionPack class" unless pack.is_a? ConditionPack
57
+ unless pack.is_a? ConditionPack
58
+ logger.fatal "Attempted to register condition pack which isn't of ConditionPack class"
59
+
60
+ raise "Condition pack should be descendant of ConditionPack class"
61
+ end
62
+
63
+ logger.info "Registering condition pack '#{pack.name}'"
48
64
 
49
65
  @condition_packs.append pack
50
66
  end
@@ -8,6 +8,8 @@ require_relative "./helpers/parameter_registrator"
8
8
  require_relative "./plugins/plugin"
9
9
  require_relative "./input/input"
10
10
  require_relative "./services/service_container"
11
+ require_relative "./logger/logging"
12
+
11
13
 
12
14
  module Kanal
13
15
  module Core
@@ -34,6 +36,7 @@ module Kanal
34
36
  include Plugins
35
37
  include Hooks
36
38
  include Services
39
+ include Logging
37
40
 
38
41
  # @return [Kanal::Core::Conditions::ConditionStorage]
39
42
  attr_reader :condition_storage
@@ -74,6 +77,8 @@ module Kanal
74
77
  #
75
78
  def register_plugin(plugin)
76
79
  unless plugin.is_a? Plugin
80
+ logger.fatal "Attempted to register plugin that is not of type Kanal::Core::Plugin or a class that inherits base Plugin class"
81
+
77
82
  raise "Plugin must be of type Kanal::Core::Plugin or be a class that inherits base Plugin class"
78
83
  end
79
84
 
@@ -82,10 +87,16 @@ module Kanal
82
87
  name = plugin.name
83
88
 
84
89
  # TODO: _log that plugin already registered with such name
85
- return if !name.nil? && plugin_registered?(name)
90
+ if !name.nil? && plugin_registered?(name)
91
+ logger.warn "Plugin '#{name}' already registered"
92
+
93
+ return
94
+ end
86
95
 
87
96
  plugin.setup(self)
88
97
 
98
+ logger.info "Registering plugin '#{name}'"
99
+
89
100
  @plugins.append plugin
90
101
  # NOTE: Catching here Exception because metho.name can raise ScriptError (derived from Exception)
91
102
  # and method .setup can raise ANY type of error.
@@ -101,6 +112,8 @@ module Kanal
101
112
  end
102
113
 
103
114
  # TODO: _log this info in critical error instead of raising exception
115
+ logger.fatal "There was a problem while registering plugin named: #{name}. Error: `#{e}`."
116
+
104
117
  raise "There was a problem while registering plugin named: #{name}. Error: `#{e}`.
105
118
  Remember, plugin errors are often due to .name method not overriden or
106
119
  having faulty code inside .setup overriden method"
@@ -153,6 +166,8 @@ module Kanal
153
166
  # @return [void] <description>
154
167
  #
155
168
  def register_input_parameter(name, readonly: false)
169
+ logger.info "Registering input parameter: '#{name}', readonly: '#{readonly}'"
170
+
156
171
  @input_parameter_registrator.register_parameter name, readonly: readonly
157
172
  end
158
173
 
@@ -165,6 +180,7 @@ module Kanal
165
180
  # @return [void] <description>
166
181
  #
167
182
  def register_output_parameter(name, readonly: false)
183
+ logger.info "Registering output parameter: '#{name}', readonly: '#{readonly}'"
168
184
  @output_parameter_registrator.register_parameter name, readonly: readonly
169
185
  end
170
186
 
@@ -179,6 +195,8 @@ module Kanal
179
195
  # @return [void] <description>
180
196
  #
181
197
  def add_condition_pack(name, &block)
198
+ logger.info "Starting to create condition pack '#{name}'"
199
+
182
200
  creator = ConditionPackCreator.new name
183
201
 
184
202
  pack = creator.create(&block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kanal
2
4
  module Core
3
5
  module Helpers
@@ -17,8 +19,7 @@ module Kanal
17
19
  end
18
20
 
19
21
  class ConditionFinder
20
- def find_by_name(name)
21
- end
22
+ def find_by_name(name); end
22
23
  end
23
24
  end
24
25
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "parameter_bag"
4
+ require_relative "../logger/logging"
2
5
 
3
6
  module Kanal
4
7
  module Core
@@ -7,6 +10,8 @@ module Kanal
7
10
  # and if they are has needed by registrator allowances, types etc, whatever
8
11
  # registrator rules are stored for property
9
12
  class ParameterBagWithRegistrator < ParameterBag
13
+ include Logging
14
+
10
15
  def initialize(registrator)
11
16
  super()
12
17
  @registrator = registrator
@@ -27,6 +32,9 @@ module Kanal
27
32
  value_exists = !get(name).nil?
28
33
 
29
34
  if value_exists
35
+ logger.fatal "Parameter #{name} is marked readonly! Attempted to set it's value, but
36
+ it already has value."
37
+
30
38
  raise "Parameter #{name} is marked readonly! You tried to set it's value, but
31
39
  it already has value."
32
40
  end
@@ -37,6 +45,8 @@ module Kanal
37
45
 
38
46
  def validate_parameter_registration(name)
39
47
  unless @registrator.parameter_registered? name
48
+ logger.fatal "Parameter #{name} was not registered! Did you forget to register that parameter?"
49
+
40
50
  raise "Parameter #{name} was not registered! Did you forget to register that parameter?"
41
51
  end
42
52
  end
@@ -18,18 +18,16 @@ module Kanal
18
18
  # input.prop = 123
19
19
  if symbol.to_s.include? "="
20
20
  @parameter_bag.set parameter_name, args.first
21
- else
21
+ elsif !args.empty?
22
22
  # this approach can be used also in dsl
23
23
  # like that
24
24
  # setters: prop value
25
25
  # getters: prop
26
- if !args.empty?
27
- # means it is used as setter in dsl,
28
- # method call with argument
29
- @parameter_bag.set(parameter_name, *args)
30
- else
31
- @parameter_bag.get parameter_name
32
- end
26
+ @parameter_bag.set(parameter_name, *args)
27
+ # means it is used as setter in dsl,
28
+ # method call with argument
29
+ else
30
+ @parameter_bag.get parameter_name
33
31
  end
34
32
  end
35
33
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logger/logging"
4
+
1
5
  module Kanal
2
6
  module Core
3
7
  module Helpers
@@ -17,6 +21,8 @@ module Kanal
17
21
  # Class holds parameter names that are allowed
18
22
  # to be used.
19
23
  class ParameterRegistrator
24
+ include Logging
25
+
20
26
  def initialize
21
27
  @parameters_by_name = {}
22
28
  end
@@ -25,7 +31,13 @@ module Kanal
25
31
  # be changed. handy for input parameters populated by interface or
26
32
  # whatever
27
33
  def register_parameter(name, readonly: false)
28
- raise "Parameter named #{name} already registered!" if @parameters_by_name.key? name
34
+ if @parameters_by_name.key? name
35
+ logger.fatal "Attempted to register already registered parameter '#{name}'"
36
+
37
+ raise "Parameter named #{name} already registered!"
38
+ end
39
+
40
+ logger.info "Registering parameter '#{name}'"
29
41
 
30
42
  registration = ParameterRegistration.new readonly
31
43
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../hooks/hook_storage"
4
+
5
+ module Kanal
6
+ module Core
7
+ module Helpers
8
+ class Queue
9
+ include Hooks
10
+
11
+ attr_reader :hooks
12
+
13
+ def initialize
14
+ @items = []
15
+ @hooks = HookStorage.new
16
+ hooks.register(:item_queued) # args arguments: item
17
+ end
18
+
19
+ def enqueue(element)
20
+ @items.append element
21
+ @hooks.call :item_queued, element
22
+ end
23
+
24
+ def dequeue
25
+ @items.shift
26
+ end
27
+
28
+ def empty?
29
+ @items.empty?
30
+ end
31
+
32
+ def remove(element)
33
+ @items.delete(element)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanal
4
+ module Core
5
+ module Helpers
6
+ class ResponseBlock
7
+ attr_reader :block
8
+
9
+ def initialize(block, async: false)
10
+ @block = block
11
+ @async = async
12
+ end
13
+
14
+ def async?
15
+ @async
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../output/output"
4
+ require_relative "../logger/logging"
5
+
6
+ module Kanal
7
+ module Core
8
+ module Helpers
9
+ class ResponseExecutionBlock
10
+ include Output
11
+ include Logging
12
+
13
+ attr_reader :response_block, :input
14
+
15
+ def initialize(response_block, input, default_error_node, error_node)
16
+ @response_block = response_block
17
+ @input = input
18
+ @default_error_node = default_error_node
19
+ @error_node = error_node
20
+ end
21
+
22
+ def execute(core, output_queue)
23
+ if response_block.async?
24
+ # NOTE: Thread doesnt just die here - it's execution is continued in output_queue.enqueue in router
25
+ # then :item_queued hook is called inside and subsequently output_ready_block gets called in this thread
26
+ # TODO: be aware that this can cause unexpected behaviour. Maybe think how to rework it.
27
+ Thread.new do
28
+ output_queue.enqueue construct_output(core)
29
+ end
30
+ else
31
+ output_queue.enqueue construct_output(core)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def construct_output(core)
38
+ logger.info "Constructing output for input ##{input.__id__}"
39
+
40
+ output = Output::Output.new core.output_parameter_registrator, input, core
41
+
42
+ begin
43
+ output.instance_eval(&@response_block.block)
44
+
45
+ core.hooks.call :output_before_returned, input, output
46
+ rescue => e
47
+ logger.error "Failed to construct output for input ##{input.__id__}. Error: '#{e}'"
48
+
49
+ output = Output::Output.new core.output_parameter_registrator, input, core
50
+
51
+ error_node = @error_node || @default_error_node
52
+
53
+ logger.info "Trying to construct error response for input ##{input.__id__}. Error response is default: #{@error_node.nil?}"
54
+
55
+ begin
56
+ output.instance_eval(&error_node.response_blocks.first.block)
57
+
58
+ core.hooks.call :output_before_returned, input, output
59
+ rescue => e
60
+ logger.error "Failed to construct error response for input ##{input.__id__}. Error: '#{e}'"
61
+
62
+ logger.info "Trying to construct default error response for input ##{input.__id__}"
63
+
64
+ output.instance_eval(&@default_error_node.response_blocks.first.block)
65
+
66
+ core.hooks.call :output_before_returned, input, output
67
+ end
68
+ end
69
+
70
+ logger.info "Output ##{output.__id__} for input ##{input.__id__} constructed"
71
+
72
+ output
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "method_source"
2
4
 
3
5
  module Kanal
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../logger/logging"
4
+
3
5
  module Kanal
4
6
  module Core
5
7
  module Hooks
@@ -8,6 +10,8 @@ module Kanal
8
10
  # attaching to hooks, calling hooks with arguments
9
11
  #
10
12
  class HookStorage
13
+ include Logging
14
+
11
15
  def initialize
12
16
  @listeners = {}
13
17
  end
@@ -29,7 +33,12 @@ module Kanal
29
33
  # Wondrous world of dynamic languages 🌈🦄
30
34
  #
31
35
  def register(name)
32
- return if hook_exists? name
36
+ if hook_exists? name
37
+ logger.warn "Hook '#{name}' already exists"
38
+ return
39
+ end
40
+
41
+ logger.info "Registering hook '#{name}'"
33
42
 
34
43
  @listeners[name] = []
35
44
  end
@@ -81,9 +90,7 @@ module Kanal
81
90
  # @return [void] <description>
82
91
  #
83
92
  def attach(name, &block)
84
- unless hook_exists? name
85
- raise "You cannot listen to hook that does not exist! Hook in question: #{name}"
86
- end
93
+ raise "You cannot listen to hook that does not exist! Hook in question: #{name}" unless hook_exists? name
87
94
 
88
95
  proc_to_lambda_object = Object.new
89
96
  proc_to_lambda_object.define_singleton_method(:hook_block, &block)
@@ -24,6 +24,11 @@ module Kanal
24
24
  #
25
25
  def initialize(core)
26
26
  @core = core
27
+
28
+ _this = self
29
+ @core.router.output_ready do |output|
30
+ _this.consume_output output
31
+ end
27
32
  end
28
33
 
29
34
  #
@@ -59,6 +64,14 @@ module Kanal
59
64
  def stop
60
65
  raise NotImplementedError
61
66
  end
67
+
68
+ def consume_input(input)
69
+ @core.router.consume_input input
70
+ end
71
+
72
+ def consume_output(output)
73
+ raise NotImplementedError
74
+ end
62
75
  end
63
76
  end
64
77
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Kanal
6
+ module Core
7
+ module Logging
8
+ # Logger instance will be saved inside class itself for future calls
9
+ @logger_instance
10
+
11
+ # Mixing in Logger in some class adds possibility to use logger instance method
12
+ def logger
13
+ @logger_instance ||= Logging.create_logger self.class.name
14
+ end
15
+
16
+ class << self
17
+ def create_logger(class_name)
18
+ logger = Logger.new STDOUT
19
+ logger.progname = class_name.rpartition(':').last
20
+ logger.datetime_format = "%d-%m-%Y %H:%M:%S"
21
+ logger
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kanal
2
4
  module Core
3
5
  module Output
@@ -8,9 +10,7 @@ module Kanal
8
10
  @input = input
9
11
  end
10
12
 
11
- def create(&block)
12
-
13
- end
13
+ def create(&block); end
14
14
  end
15
15
  end
16
16
  end
@@ -31,7 +31,7 @@ module Kanal
31
31
 
32
32
  #
33
33
  # If plugins does have rake tasks available for execution,
34
- # require them here. They will be used
34
+ # require them here. They will be used
35
35
  #
36
36
  # @return [Array<Rake::TaskLib>] <description>
37
37
  #
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "./router_node"
4
+ require_relative "../helpers/queue"
5
+ require_relative "../helpers/response_execution_block"
6
+ require_relative "../logger/logging"
4
7
 
5
8
  module Kanal
6
9
  module Core
@@ -10,17 +13,50 @@ module Kanal
10
13
  # namespace. Basically class router stores all the
11
14
  # router nodes and have a name.
12
15
  class Router
13
- attr_reader :name,
14
- :core
16
+ include Helpers
17
+ include Logging
18
+
19
+ attr_reader :name, :core, :output_ready_block
15
20
 
16
21
  def initialize(name, core)
22
+ logger.info "Initializing"
23
+
17
24
  @name = name
18
25
  @core = core
19
26
  @root_node = nil
20
27
  @default_node = nil
28
+ @default_error_node = nil
29
+ default_error_response do
30
+ if core.plugin_registered? :batteries
31
+ body "Unfortunately, error happened. Please consider contacting the creator of this bot to provide information about the circumstances of this error."
32
+ else
33
+ raise "Error occurred and there is no way to inform end user about it. You can override error response with router.error_response method or register :batteries plugin so default response will populate the .body output parameter"
34
+ end
35
+ end
36
+ @error_node = nil
37
+ @response_execution_queue = Queue.new
38
+ @output_queue = Queue.new
39
+ @output_ready_block = nil
40
+ @core.hooks.register(:output_ready) # arg
41
+
42
+ _this = self
43
+ _output_queue = @output_queue
44
+ @output_queue.hooks.attach :item_queued do |output|
45
+ _this.logger.info "Calling output_ready block for input ##{output.input.__id__} and output #{output.__id__}. Output body is: '#{output.body}'"
46
+
47
+ begin
48
+ _this.output_ready_block.call output
49
+ _output_queue.remove(output)
50
+ rescue
51
+ _output_queue.remove(output)
52
+ raise "Error in output_ready block!"
53
+ end
54
+ end
21
55
  end
22
56
 
23
57
  def configure(&block)
58
+ logger.info "Configuring"
59
+
24
60
  # Root node does not have parent
25
61
  @root_node ||= RouterNode.new router: self, parent: nil, root: true
26
62
 
@@ -28,24 +64,56 @@ module Kanal
28
64
  end
29
65
 
30
66
  def default_response(&block)
31
- raise "default node for router #{@name} already defined" if @default_node
67
+ logger.info "Setting default response"
68
+
69
+ if @default_node
70
+ logger.fatal "Attempted to set default_response for a second time"
71
+
72
+ raise "default node for router #{@name} already defined"
73
+ end
32
74
 
33
75
  @default_node = RouterNode.new parent: nil, router: self, default: true
34
76
 
35
77
  @default_node.respond(&block)
36
78
  end
37
79
 
38
- # Main method for creating output if it is found or going to default output
39
- def create_output_for_input(input)
80
+ def error_response(&block)
81
+ raise "error node for router #{@name} already defined" if @error_node
82
+
83
+ @error_node = RouterNode.new parent: nil, router: self, error: true
84
+
85
+ @error_node.respond(&block)
86
+ end
87
+
88
+ # Main method for creating output(s) if it is found or going to default output
89
+ def consume_input(input)
90
+ logger.info "Consuming input #{input.__id__}."
91
+
40
92
  # Checking if default node with output exists throw error if not
41
- raise "Please provide default response for router before you try and throw input against it ;)" unless @default_node
93
+ unless @default_node
94
+ logger.fatal "Attempted to consume input with no default response set"
95
+
96
+ raise "Please provide default response for router before you try and throw input against it ;)"
97
+ end
98
+
99
+ unless @root_node
100
+ logger.fatal "Attempted to consume input but router is not configured"
42
101
 
43
- raise "You did not actually .configure router, didn't you? There is no even root node! Use .configure method" unless @root_node
102
+ raise "You did not actually .configure router, didn't you? There is no even root node! Use .configure method"
103
+ end
44
104
 
45
105
  unless @root_node.children?
106
+ logger.fatal "Attempted to consume input but router does not have any routes"
107
+
46
108
  raise "Hey your router actually does not have ANY routes to work with. Did you even try adding them?"
47
109
  end
48
110
 
111
+ unless @output_ready_block
112
+ logger.fatal "Attempted to consume input but output_ready block is not set"
113
+
114
+ raise "You must provide block via .output_ready for router to function properly"
115
+ end
116
+
49
117
  @core.hooks.call :input_before_router, input
50
118
 
51
119
  node = test_input_against_router_node input, @root_node
@@ -54,11 +122,31 @@ module Kanal
54
122
  # using default response
55
123
  node ||= @default_node
56
124
 
57
- output = node.construct_response input
125
+ response_blocks = node.response_blocks
126
+
127
+ error_node = @error_node || @default_error_node
128
+
129
+ response_execution_blocks = response_blocks.map { |rb| ResponseExecutionBlock.new rb, input, @default_error_node, @error_node }
130
+
131
+ response_execution_blocks.each do |reb|
132
+ @response_execution_queue.enqueue reb
133
+ end
134
+
135
+ process_response_execution_queue
136
+ end
58
137
 
59
- @core.hooks.call :output_before_returned, input, output
138
+ def process_response_execution_queue
139
+ until @response_execution_queue.empty?
140
+ response_execution = @response_execution_queue.dequeue
60
141
 
61
- output
142
+ response_execution.execute core, @output_queue
143
+ end
144
+ end
145
+
146
+ def output_ready(&block)
147
+ logger.info "Setting output_ready block"
148
+
149
+ @output_ready_block = block
62
150
  end
63
151
 
64
152
  # Recursive method for searching router nodes
@@ -91,6 +179,12 @@ module Kanal
91
179
  end
92
180
 
93
181
  private :test_input_against_router_node
182
+
183
+ def default_error_response(&block)
184
+ @default_error_node = RouterNode.new parent: nil, router: self, error: true
185
+
186
+ @default_error_node.respond(&block)
187
+ end
94
188
  end
95
189
  end
96
190
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../output/output"
4
+ require_relative "../helpers/response_block"
5
+ require_relative "../logger/logging"
4
6
 
5
7
  module Kanal
6
8
  module Core
@@ -11,6 +13,8 @@ module Kanal
11
13
  # tree, containing conditions and responses
12
14
  class RouterNode
13
15
  include Output
16
+ include Helpers
17
+ include Logging
14
18
 
15
19
  attr_reader :parent,
16
20
  :children
@@ -18,13 +22,13 @@ module Kanal
18
22
  # parameter default: is for knowing that this node
19
23
  # is for default response
20
24
  # default response cannot have child nodes
21
- def initialize(*args, router:, parent:, default: false, root: false)
25
+ def initialize(*args, router:, parent:, default: false, root: false, error: false)
22
26
  @router = router
23
27
  @parent = parent
24
28
 
25
29
  @children = []
26
30
 
27
- @response_block = nil
31
+ @response_blocks = []
28
32
 
29
33
  @condition_pack_name = nil
30
34
  @condition_name = nil
@@ -32,7 +36,7 @@ module Kanal
32
36
 
33
37
  # We omit setting conditions because default router node does not need any conditions
34
38
  # Also root node does not have conditions so we basically omit them if arguments are empty
35
- return if default || root
39
+ return if default || root || error
36
40
 
37
41
  # With this we attach names of condition pack and condition to this router
38
42
  # node, so we will be able to find them later at runtime and use them
@@ -48,24 +52,28 @@ module Kanal
48
52
  child.instance_eval(&block)
49
53
  end
50
54
 
51
- def construct_response(input)
52
- raise "no response block configured for this node. router: #{@router.name}. debug: #{debug_info}" unless @response_block
55
+ def response_blocks
56
+ if @response_blocks.empty?
57
+ raise "no response block configured for this node. router: #{@router.name}. debug: #{debug_info}"
58
+ end
53
59
 
54
- output = Output::Output.new @router.core.output_parameter_registrator, input, @router.core
60
+ @response_blocks
61
+ end
55
62
 
56
- output.instance_eval(&@response_block)
63
+ def respond(&block)
64
+ raise "Router node with children cannot have response" unless @children.empty?
57
65
 
58
- output
66
+ @response_blocks.append ResponseBlock.new(block)
59
67
  end
60
68
 
61
- def respond(&block)
69
+ def respond_async(&block)
62
70
  raise "Router node with children cannot have response" unless @children.empty?
63
71
 
64
- @response_block = block
72
+ @response_blocks.append ResponseBlock.new(block, async: true)
65
73
  end
66
74
 
67
75
  def response?
68
- !@response_block.nil?
76
+ !@response_blocks.empty?
69
77
  end
70
78
 
71
79
  # This method processes args to populate condition and condition pack
@@ -88,6 +96,8 @@ module Kanal
88
96
  condition = pack.get_condition_by_name! condition_name
89
97
 
90
98
  if condition.with_argument? && !@condition_argument
99
+ logger.fatal "Condition requires argument, but was provided as :symbol, not as positional_arg:"
100
+
91
101
  raise "Condition requires argument, though you wrote it as :symbol, not as positional_arg:
92
102
  Please check route with condition pack: #{condition_pack_name} and condition: #{condition_name}"
93
103
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "./router"
2
4
 
3
5
  module Kanal
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logger/logging"
4
+
1
5
  module Kanal
2
6
  module Core
3
7
  module Services
@@ -24,6 +28,8 @@ module Kanal
24
28
  # lifespan types.
25
29
  #
26
30
  class ServiceContainer
31
+ include Logging
32
+
27
33
  TYPE_SINGLETON = :singleton
28
34
  TYPE_TRANSIENT = :transient
29
35
 
@@ -45,12 +51,24 @@ module Kanal
45
51
  # @return [void] <description>
46
52
  #
47
53
  def register_service(name, service_class, type: TYPE_SINGLETON, &block)
48
- return if @registrations.key? name
54
+ logger.info "Trying to register service '#{name}'"
49
55
 
50
- raise "Unrecognized service type #{type}. Allowed types: #{allowed_types}" unless allowed_types.include? type
56
+ if @registrations.key? name
57
+ logger.warn "Attempted to register service '#{name}', but it is already registered"
58
+
59
+ return
60
+ end
61
+
62
+ unless allowed_types.include? type
63
+ logger.fatal "Attempted to register service type '#{type}'."
64
+
65
+ raise "Unrecognized service type #{type}. Allowed types: #{allowed_types}"
66
+ end
51
67
 
52
68
  registration = ServiceRegistration.new service_class, type, block
53
69
 
70
+ logger.info "Registering service '#{name}'"
71
+
54
72
  @registrations[name] = registration
55
73
  end
56
74
 
@@ -66,14 +84,13 @@ module Kanal
66
84
 
67
85
  registration = @registrations[name]
68
86
 
69
- if registration.type == TYPE_SINGLETON
87
+ case registration.type
88
+ when TYPE_SINGLETON
70
89
  # Created once and reused after creation
71
- if @services[name].nil?
72
- @services[name] = create_service_from_registration registration
73
- end
90
+ @services[name] = create_service_from_registration registration if @services[name].nil?
74
91
 
75
92
  @services[name]
76
- elsif registration.type == TYPE_TRANSIENT
93
+ when TYPE_TRANSIENT
77
94
  # Created every time
78
95
  create_service_from_registration registration
79
96
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "uri"
5
+ require 'open-uri'
6
+
7
+ module Kanal
8
+ module Plugins
9
+ module Batteries
10
+ module Attachments
11
+ class Attachment
12
+ attr_reader :url
13
+
14
+ def initialize(url)
15
+ @url = url
16
+ end
17
+
18
+ def audio?
19
+ mp3? || wav? || ogg?
20
+ end
21
+
22
+ def mp3?
23
+ extension == "mp3"
24
+ end
25
+
26
+ def wav?
27
+ extension == "wav"
28
+ end
29
+
30
+ def ogg?
31
+ extension == "ogg"
32
+ end
33
+
34
+ def document?
35
+ doc? || docx? || odf?
36
+ end
37
+
38
+ def doc?
39
+ extension == "doc"
40
+ end
41
+
42
+ def docx?
43
+ extension == "docx"
44
+ end
45
+
46
+ def odf?
47
+ extension == "odf"
48
+ end
49
+
50
+ def image?
51
+ jpg? || jpeg? || png? || bpm? || gif?
52
+ end
53
+
54
+ def jpg?
55
+ extension == "jpg"
56
+ end
57
+
58
+ def jpeg?
59
+ extension == "jpeg"
60
+ end
61
+
62
+ def png?
63
+ extension == "png"
64
+ end
65
+
66
+ def bpm?
67
+ extension == "bpm"
68
+ end
69
+
70
+ def gif?
71
+ extension == "gif"
72
+ end
73
+
74
+ def video?
75
+ mp4? || mov? || mkv?
76
+ end
77
+
78
+ def mp4?
79
+ extension == "mp4"
80
+ end
81
+
82
+ def mov?
83
+ extension == "mov"
84
+ end
85
+
86
+ def mkv?
87
+ extension == "mkv"
88
+ end
89
+
90
+ #
91
+ # Method that returns extension of url file if possible
92
+ # For example calling extension https://123.txt?something=1 will return 'txt'
93
+ #
94
+ # @return [String, nil]
95
+ #
96
+ def extension
97
+ uri = URI.parse(@url)
98
+ return nil if uri.path.nil?
99
+
100
+ File.extname(uri.path).split(".").last if File.basename(uri.path).include? "."
101
+ end
102
+
103
+ #
104
+ # Saves file to specified path. End user provides full filepath.
105
+ #
106
+ # @param [String] <Full filepath>
107
+ # @param [Boolean] <Should directories be created or not>
108
+ #
109
+ # @return [void]
110
+ #
111
+ def save(filepath, create_dirs = false)
112
+ stream = URI.open(@url)
113
+
114
+ save_stream_to_file stream, filepath, create_dirs
115
+ end
116
+
117
+ #
118
+ # Saves file. End user provides directory only. Filename gets generated, extension is read from url.
119
+ #
120
+ # @param [String] <Directory>
121
+ # @param [Boolean] <Should directories be created or not>
122
+ # @param [Integer] <Length of filename to generate>
123
+ #
124
+ # @return [String] Full filepath to saved file
125
+ #
126
+ def quick_save(directory, create_dir = false, filename_length = 32)
127
+ filename = generate_filename filename_length, extension
128
+
129
+ return quick_save directory, create_dir, filename_length if File.exist? filename
130
+
131
+ save directory + filename, create_dir
132
+
133
+ directory + filename
134
+ end
135
+
136
+ private
137
+
138
+ def generate_filename(filename_length, extension = nil)
139
+ alphanumeric = "abcdefghijkmnopQRSTUVWNXYZW12345676789-".chars
140
+
141
+ name = ""
142
+
143
+ filename_length.times do
144
+ name += alphanumeric.sample
145
+ end
146
+
147
+ extension ? "#{name}.#{extension}" : name
148
+ end
149
+
150
+ def save_stream_to_file(stream, filepath, create_dirs)
151
+ raise "File with that name already exists!" if File.exist? filepath
152
+
153
+ if create_dirs == true
154
+ FileUtils.mkdir_p(File.dirname(filepath)) unless File.directory?(File.dirname(filepath))
155
+ end
156
+
157
+ IO.copy_stream stream, filepath
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -16,6 +16,8 @@ module Kanal
16
16
  source_batteries core
17
17
  body_batteries core
18
18
  flow_batteries core
19
+ attachments_batteries core
20
+ reply_markup_batteries core
19
21
  end
20
22
 
21
23
  def flow_batteries(core)
@@ -110,6 +112,20 @@ module Kanal
110
112
  end
111
113
  end
112
114
  end
115
+
116
+ def attachments_batteries(core)
117
+ core.register_input_parameter :image
118
+ core.register_input_parameter :audio
119
+ core.register_input_parameter :file
120
+
121
+ core.register_output_parameter :image
122
+ core.register_output_parameter :audio
123
+ core.register_output_parameter :file
124
+ end
125
+
126
+ def reply_markup_batteries(core)
127
+ core.register_output_parameter :reply_markup
128
+ end
113
129
  end
114
130
  end
115
131
  end
data/lib/kanal/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kanal
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kanal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - idchlife
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-29 00:00:00.000000000 Z
11
+ date: 2023-03-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Thanks to the core library, ecosystem of Kanal tools can be extendted
14
14
  to use with input-output bot-like behaviour, with routing
@@ -42,11 +42,14 @@ files:
42
42
  - lib/kanal/core/helpers/parameter_bag_with_registrator.rb
43
43
  - lib/kanal/core/helpers/parameter_finder_with_method_missing_mixin.rb
44
44
  - lib/kanal/core/helpers/parameter_registrator.rb
45
+ - lib/kanal/core/helpers/queue.rb
46
+ - lib/kanal/core/helpers/response_block.rb
47
+ - lib/kanal/core/helpers/response_execution_block.rb
45
48
  - lib/kanal/core/helpers/router_proc_parser.rb
46
49
  - lib/kanal/core/hooks/hook_storage.rb
47
50
  - lib/kanal/core/input/input.rb
48
51
  - lib/kanal/core/interfaces/interface.rb
49
- - lib/kanal/core/logger/logger.rb
52
+ - lib/kanal/core/logger/logging.rb
50
53
  - lib/kanal/core/output/output.rb
51
54
  - lib/kanal/core/output/output_creator.rb
52
55
  - lib/kanal/core/plugins/plugin.rb
@@ -55,6 +58,7 @@ files:
55
58
  - lib/kanal/core/router/router_storage.rb
56
59
  - lib/kanal/core/services/service_container.rb
57
60
  - lib/kanal/interfaces/simple_cli/simple_cli_interface.rb
61
+ - lib/kanal/plugins/batteries/attachments/attachment.rb
58
62
  - lib/kanal/plugins/batteries/batteries_plugin.rb
59
63
  - lib/kanal/version.rb
60
64
  - sig/kanal.rbs
@@ -68,6 +72,7 @@ metadata:
68
72
  homepage_uri: https://idchlife.github.io/kanal-documentation/
69
73
  source_code_uri: https://github.com/idchlife/kanal
70
74
  changelog_uri: https://github.com/idchlife/kanal/CHANGELOG.md
75
+ rubygems_mfa_required: 'true'
71
76
  post_install_message:
72
77
  rdoc_options: []
73
78
  require_paths:
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "logger"
4
-
5
- module Kanal
6
- module Core
7
- module Logger
8
- class Logger < ::Logger
9
- end
10
- end
11
- end
12
- end