kanal 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +14 -0
  5. data/.ruby-version +1 -0
  6. data/.vscode/settings.json +8 -0
  7. data/CHANGELOG.md +15 -0
  8. data/Gemfile +26 -0
  9. data/Gemfile.lock +108 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +45 -0
  12. data/Rakefile +12 -0
  13. data/kanal.gemspec +39 -0
  14. data/lib/kanal/core/conditions/condition.rb +36 -0
  15. data/lib/kanal/core/conditions/condition_creator.rb +32 -0
  16. data/lib/kanal/core/conditions/condition_pack.rb +52 -0
  17. data/lib/kanal/core/conditions/condition_pack_creator.rb +41 -0
  18. data/lib/kanal/core/conditions/condition_storage.rb +58 -0
  19. data/lib/kanal/core/core.rb +214 -0
  20. data/lib/kanal/core/helpers/condition_finder.rb +26 -0
  21. data/lib/kanal/core/helpers/parameter_bag.rb +22 -0
  22. data/lib/kanal/core/helpers/parameter_bag_with_registrator.rb +46 -0
  23. data/lib/kanal/core/helpers/parameter_finder_with_method_missing_mixin.rb +38 -0
  24. data/lib/kanal/core/helpers/parameter_registrator.rb +48 -0
  25. data/lib/kanal/core/helpers/router_proc_parser.rb +47 -0
  26. data/lib/kanal/core/hooks/hook_storage.rb +109 -0
  27. data/lib/kanal/core/input/input.rb +20 -0
  28. data/lib/kanal/core/interfaces/interface.rb +65 -0
  29. data/lib/kanal/core/logger/logger.rb +12 -0
  30. data/lib/kanal/core/output/output.rb +31 -0
  31. data/lib/kanal/core/output/output_creator.rb +17 -0
  32. data/lib/kanal/core/plugins/plugin.rb +44 -0
  33. data/lib/kanal/core/router/router.rb +97 -0
  34. data/lib/kanal/core/router/router_node.rb +131 -0
  35. data/lib/kanal/core/router/router_storage.rb +33 -0
  36. data/lib/kanal/core/services/service_container.rb +97 -0
  37. data/lib/kanal/interfaces/simple_cli/simple_cli_interface.rb +51 -0
  38. data/lib/kanal/plugins/batteries/batteries_plugin.rb +116 -0
  39. data/lib/kanal/version.rb +5 -0
  40. data/lib/kanal.rb +9 -0
  41. data/sig/kanal/core/conditions/condition_pack.rbs +9 -0
  42. data/sig/kanal/core/conditions/condition_storage.rbs +9 -0
  43. data/sig/kanal.rbs +4 -0
  44. metadata +90 -0
@@ -0,0 +1,17 @@
1
+ module Kanal
2
+ module Core
3
+ module Output
4
+ # This class helps creating output with the help
5
+ # of handy dsl format
6
+ class OutputCreator
7
+ def initialize(input)
8
+ @input = input
9
+ end
10
+
11
+ def create(&block)
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+
5
+ module Kanal
6
+ module Core
7
+ module Plugins
8
+ # Base class for plugins that can be registered in the core
9
+ class Plugin
10
+ #
11
+ # Name of the plugin, for it to be available
12
+ # for finding and getting
13
+ #
14
+ # @return [Symbol] <description>
15
+ #
16
+ def name
17
+ raise NotImplementedError
18
+ end
19
+
20
+ #
21
+ # This method is for the setting up, it will be executed when plugin
22
+ # is being added to the core
23
+ #
24
+ # @param [Kanal::Core::Core] core <description>
25
+ #
26
+ # @return [void] <description>
27
+ #
28
+ def setup(core)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ #
33
+ # If plugins does have rake tasks available for execution,
34
+ # require them here. They will be used
35
+ #
36
+ # @return [Array<Rake::TaskLib>] <description>
37
+ #
38
+ def rake_tasks
39
+ []
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./router_node"
4
+
5
+ module Kanal
6
+ module Core
7
+ module Router
8
+ # Router serves as a container class for
9
+ # root node of router nodes, also as somewhat
10
+ # namespace. Basically class router stores all the
11
+ # router nodes and have a name.
12
+ class Router
13
+ attr_reader :name,
14
+ :core
15
+
16
+ def initialize(name, core)
17
+ @name = name
18
+ @core = core
19
+ @root_node = nil
20
+ @default_node = nil
21
+ end
22
+
23
+ def configure(&block)
24
+ # Root node does not have parent
25
+ @root_node ||= RouterNode.new router: self, parent: nil, root: true
26
+
27
+ @root_node.instance_eval(&block)
28
+ end
29
+
30
+ def default_response(&block)
31
+ raise "default node for router #{@name} already defined" if @default_node
32
+
33
+ @default_node = RouterNode.new parent: nil, router: self, default: true
34
+
35
+ @default_node.respond(&block)
36
+ end
37
+
38
+ # Main method for creating output if it is found or going to default output
39
+ def create_output_for_input(input)
40
+ # 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
42
+
43
+ raise "You did not actually .configure router, didn't you? There is no even root node! Use .configure method" unless @root_node
44
+
45
+ unless @root_node.children?
46
+ raise "Hey your router actually does not have ANY routes to work with. Did you even try adding them?"
47
+ end
48
+
49
+ @core.hooks.call :input_before_router, input
50
+
51
+ node = test_input_against_router_node input, @root_node
52
+
53
+ # No result means no route node was found for that input
54
+ # using default response
55
+ node ||= @default_node
56
+
57
+ output = node.construct_response input
58
+
59
+ @core.hooks.call :output_before_returned, input, output
60
+
61
+ output
62
+ end
63
+
64
+ # Recursive method for searching router nodes
65
+ def test_input_against_router_node(input, router_node)
66
+ # Allow root node because it does not have any conditions and does not have
67
+ # any responses, but it does have children. Well, it should have children...
68
+ # Basically:
69
+ # if router_node is root - proceed with code
70
+ # if router_node is not root and condition is not met - stop right here.
71
+ # Cannot proceed inside of this node.
72
+ return if !router_node.root? && !router_node.condition_met?(input, @core)
73
+
74
+ # Check if node has children first. Router node with children SHOULD NOT HAVE RESPONSE.
75
+ # There is an exception for this case so don't worry, it's not protected only by
76
+ # this comment
77
+ if router_node.children?
78
+ node = nil
79
+
80
+ router_node.children.each do |c|
81
+ node = test_input_against_router_node input, c
82
+
83
+ break if node
84
+ end
85
+
86
+ node
87
+ elsif router_node.response?
88
+ # Router node without children can have response
89
+ router_node
90
+ end
91
+ end
92
+
93
+ private :test_input_against_router_node
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../output/output"
4
+
5
+ module Kanal
6
+ module Core
7
+ # This module stores RouterNode and helper classes which are
8
+ # important for building router nodes tree
9
+ module Router
10
+ # This class is used as a node in router
11
+ # tree, containing conditions and responses
12
+ class RouterNode
13
+ include Output
14
+
15
+ attr_reader :parent,
16
+ :children
17
+
18
+ # parameter default: is for knowing that this node
19
+ # is for default response
20
+ # default response cannot have child nodes
21
+ def initialize(*args, router:, parent:, default: false, root: false)
22
+ @router = router
23
+ @parent = parent
24
+
25
+ @children = []
26
+
27
+ @response_block = nil
28
+
29
+ @condition_pack_name = nil
30
+ @condition_name = nil
31
+ @condition_argument = nil
32
+
33
+ # We omit setting conditions because default router node does not need any conditions
34
+ # Also root node does not have conditions so we basically omit them if arguments are empty
35
+ return if default || root
36
+
37
+ # With this we attach names of condition pack and condition to this router
38
+ # node, so we will be able to find them later at runtime and use them
39
+ assign_condition_pack_and_condition_names_from_args!(*args)
40
+ end
41
+
42
+ def on(*args, &block)
43
+ raise "You cannot add children to nodes with response ready. Response is a final line" if response?
44
+
45
+ child = RouterNode.new(*args, router: @router, parent: self)
46
+ add_child child
47
+
48
+ child.instance_eval(&block)
49
+ end
50
+
51
+ def construct_response(input)
52
+ raise "no response block configured for this node. router: #{@router.name}. debug: #{debug_info}" unless @response_block
53
+
54
+ output = Output::Output.new @router.core.output_parameter_registrator, input, @router.core
55
+
56
+ output.instance_eval(&@response_block)
57
+
58
+ output
59
+ end
60
+
61
+ def respond(&block)
62
+ raise "Router node with children cannot have response" unless @children.empty?
63
+
64
+ @response_block = block
65
+ end
66
+
67
+ def response?
68
+ !@response_block.nil?
69
+ end
70
+
71
+ # This method processes args to populate condition and condition pack
72
+ # in this router node
73
+ def assign_condition_pack_and_condition_names_from_args!(*args)
74
+ condition_pack_name = args[0]
75
+ condition_name = args[1]
76
+
77
+ # We assume we got condition that requires argument
78
+ if condition_name.is_a? Hash
79
+ # We search for arguments inside kwargs
80
+ @condition_argument = condition_name.values.first
81
+
82
+ condition_name = condition_name.keys.first
83
+ end
84
+
85
+ # This calls will raise errors if there is problem with pack or condition
86
+ # inside of it
87
+ pack = @router.core.condition_storage.get_condition_pack_by_name! condition_pack_name
88
+ condition = pack.get_condition_by_name! condition_name
89
+
90
+ if condition.with_argument? && !@condition_argument
91
+ raise "Condition requires argument, though you wrote it as :symbol, not as positional_arg:
92
+ Please check route with condition pack: #{condition_pack_name} and condition: #{condition_name}"
93
+ end
94
+
95
+ @condition_pack_name = condition_pack_name
96
+ @condition_name = condition_name
97
+ end
98
+
99
+ def debug_info
100
+ "RouterNode with condition pack: #{@condition_pack_name}, condition: #{@condition_name}, condition argument: #{@condition_argument}"
101
+ end
102
+
103
+ def condition_met?(input, core)
104
+ c = condition
105
+
106
+ c.met? input, core, @condition_argument
107
+ end
108
+
109
+ def condition
110
+ pack = @router.core.condition_storage.get_condition_pack_by_name! @condition_pack_name
111
+
112
+ pack.get_condition_by_name! @condition_name
113
+ end
114
+
115
+ def root?
116
+ parent.nil?
117
+ end
118
+
119
+ def children?
120
+ !@children.empty?
121
+ end
122
+
123
+ def add_child(node)
124
+ @children.append node
125
+ end
126
+
127
+ private :add_child
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "./router"
2
+
3
+ module Kanal
4
+ module Core
5
+ module Router
6
+ # Class with helper methods for creating and getting routers
7
+ class RouterStorage
8
+ def initialize(core)
9
+ @routers = []
10
+ @core = core
11
+ end
12
+
13
+ #
14
+ # Creates router by name and stores it for further access
15
+ #
16
+ # @param [Symbol] name <description>
17
+ #
18
+ # @return [Kanal::Core::Router::Router] <description>
19
+ #
20
+ def get_or_create_router(name)
21
+ router = @routers.find { |r| r.name == name }
22
+
23
+ unless router
24
+ router = Router.new name, @core
25
+ @routers.append router
26
+ end
27
+
28
+ router
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,97 @@
1
+ module Kanal
2
+ module Core
3
+ module Services
4
+ # Stores info about service registration
5
+ class ServiceRegistration
6
+ attr_reader :service_class,
7
+ :type,
8
+ :block
9
+
10
+ def initialize(service_class, type, block)
11
+ @service_class = service_class
12
+ @type = type
13
+ @block = block
14
+ end
15
+
16
+ def block?
17
+ !@block.nil?
18
+ end
19
+ end
20
+
21
+ #
22
+ # Container allows service registration as well as
23
+ # getting those services. You can register service with different
24
+ # lifespan types.
25
+ #
26
+ class ServiceContainer
27
+ TYPE_SINGLETON = :singleton
28
+ TYPE_TRANSIENT = :transient
29
+
30
+ def initialize
31
+ @registrations = {}
32
+ @services = {}
33
+ end
34
+
35
+ #
36
+ # Registering service so container knows about it and it's type and it's
37
+ # optional initialization block
38
+ #
39
+ # @param [Symbol] name <description>
40
+ # @param [class] service_class <description>
41
+ # @param [Symbol] type <description>
42
+ # @yield Initialization block. It should be used if your service
43
+ # requires postponed initialization
44
+ #
45
+ # @return [void] <description>
46
+ #
47
+ def register_service(name, service_class, type: TYPE_SINGLETON, &block)
48
+ return if @registrations.key? name
49
+
50
+ raise "Unrecognized service type #{type}. Allowed types: #{allowed_types}" unless allowed_types.include? type
51
+
52
+ registration = ServiceRegistration.new service_class, type, block
53
+
54
+ @registrations[name] = registration
55
+ end
56
+
57
+ #
58
+ # Gets the registered service by name
59
+ #
60
+ # @param [Symbol] name <description>
61
+ #
62
+ # @return [Object] <description>
63
+ #
64
+ def get(name)
65
+ raise "Service named #{name} was not registered in container" unless @registrations.key? name
66
+
67
+ registration = @registrations[name]
68
+
69
+ if registration.type == TYPE_SINGLETON
70
+ # Created once and reused after creation
71
+ if @services[name].nil?
72
+ @services[name] = create_service_from_registration registration
73
+ end
74
+
75
+ @services[name]
76
+ elsif registration.type == TYPE_TRANSIENT
77
+ # Created every time
78
+ create_service_from_registration registration
79
+ end
80
+ end
81
+
82
+ def create_service_from_registration(registration)
83
+ return registration.block.call if registration.block?
84
+
85
+ registration.service_class.new
86
+ end
87
+
88
+ def allowed_types
89
+ [TYPE_SINGLETON, TYPE_TRANSIENT]
90
+ end
91
+
92
+ private :create_service_from_registration,
93
+ :allowed_types
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/interfaces/interface"
4
+ require_relative "../../plugins/batteries/batteries_plugin"
5
+
6
+ module Kanal
7
+ module Interfaces
8
+ module SimpleCli
9
+ # This interface provides input/output with the cli
10
+ class SimpleCliInterface < Kanal::Core::Interfaces::Interface
11
+ #
12
+ # <Description>
13
+ #
14
+ # @param [Kanal::Core::Core] core <description>
15
+ #
16
+ def initialize(core)
17
+ super
18
+
19
+ # For simple cli we need body
20
+ @core.register_plugin Kanal::Plugins::Batteries::BatteriesPlugin.new
21
+
22
+ @core.register_output_parameter :quit
23
+ end
24
+
25
+ def start
26
+ loop do
27
+ puts ">>>"
28
+ input = @core.create_input
29
+ input.body = gets
30
+
31
+ output = router.create_output_for_input input
32
+
33
+ if output.quit
34
+ puts "Undestood! Quitting"
35
+ break
36
+ end
37
+
38
+ puts "[bot]: #{output.body}"
39
+ rescue Interrupt
40
+ puts "Got it! Hard stop. Bye bye!"
41
+ break
42
+ end
43
+
44
+ puts "End of conversation!"
45
+ end
46
+
47
+ def stop; end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kanal/core/plugins/plugin"
4
+
5
+ module Kanal
6
+ module Plugins
7
+ # This module stores needed classes and helpers for batteries plugin
8
+ module Batteries
9
+ # Plugin with some batteries like .body property etc
10
+ class BatteriesPlugin < Core::Plugins::Plugin
11
+ def name
12
+ :batteries
13
+ end
14
+
15
+ def setup(core)
16
+ source_batteries core
17
+ body_batteries core
18
+ flow_batteries core
19
+ end
20
+
21
+ def flow_batteries(core)
22
+ core.add_condition_pack :flow do
23
+ add_condition :any do
24
+ met? do |_, _, _|
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def source_batteries(core)
32
+ # This parameter can be filled by different plugins/interfaces
33
+ # to point from which source message came
34
+ core.register_input_parameter :source
35
+
36
+ core.add_condition_pack :source do
37
+ add_condition :from do
38
+ with_argument
39
+
40
+ met? do |input, _, argument|
41
+ input.source == argument
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def body_batteries(core)
48
+ core.register_input_parameter :body
49
+ core.register_output_parameter :body
50
+
51
+ core.add_condition_pack :body do
52
+ add_condition :starts_with do
53
+ with_argument
54
+
55
+ met? do |input, _, argument|
56
+ if input.body.is_a? String
57
+ input.body.start_with? argument
58
+ else
59
+ false
60
+ end
61
+ end
62
+ end
63
+
64
+ add_condition :ends_with do
65
+ with_argument
66
+
67
+ met? do |input, _, argument|
68
+ if input.body.is_a? String
69
+ input.body.end_with? argument
70
+ else
71
+ false
72
+ end
73
+ end
74
+ end
75
+
76
+ add_condition :contains do
77
+ with_argument
78
+
79
+ met? do |input, _, argument|
80
+ if input.body.is_a? String
81
+ input.body.include? argument
82
+ else
83
+ false
84
+ end
85
+ end
86
+ end
87
+
88
+ add_condition :contains_one_of do
89
+ with_argument
90
+
91
+ met? do |input, _, argument|
92
+ met = false
93
+
94
+ argument.each do |word|
95
+ met = input.body.include? word
96
+
97
+ break if met
98
+ end
99
+
100
+ met
101
+ end
102
+ end
103
+
104
+ add_condition :equals do
105
+ with_argument
106
+
107
+ met? do |input, _, argument|
108
+ input.body == argument
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanal
4
+ VERSION = "0.3.0"
5
+ end
data/lib/kanal.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kanal/version"
4
+ require_relative "kanal/core/core"
5
+ require_relative "kanal/core/plugins/plugin"
6
+
7
+ # This module used as a main entry point into kanal-core library
8
+ module Kanal
9
+ end
@@ -0,0 +1,9 @@
1
+ module Kanal
2
+ module Core
3
+ module Conditions
4
+ class ConditionPack
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Kanal
2
+ module Core
3
+ module Conditions
4
+ class ConditionStorage
5
+ def get_condition_pack_by_name: (name: String) -> ConditionPack
6
+ end
7
+ end
8
+ end
9
+ end
data/sig/kanal.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Kanal
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end