kanal 0.3.0 → 0.4.1

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