kanal 0.3.0

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