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,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./conditions/condition_storage"
4
+ require_relative "./conditions/condition_pack_creator"
5
+ require_relative "./router/router_storage"
6
+ require_relative "./hooks/hook_storage"
7
+ require_relative "./helpers/parameter_registrator"
8
+ require_relative "./plugins/plugin"
9
+ require_relative "./input/input"
10
+ require_relative "./services/service_container"
11
+
12
+ module Kanal
13
+ module Core
14
+ #
15
+ # Main class that end users of Kanal will be using for
16
+ # initialization, plugin registration, conditions registration, etc
17
+ #
18
+ # TODO: consider hiding properties used inside
19
+ # and provide those dependencies explicitly
20
+ # e.g.: output_parameter_registrator is needed for router
21
+ # to create output. It provides the parameters registration
22
+ # info for the Output object. Can it be explicitly passed into the router
23
+ # instead of exposing in in Core?
24
+ #
25
+ # @!attribute [r] hooks
26
+ # @return [Kanal::Core::Hooks::HookStorage] storage which can be used to register hooks
27
+ # @!attribute [r] services
28
+ # @return [Kanal::Core::Services::ServiceContainer] service container for registering and getting services
29
+ #
30
+ class Core
31
+ include Conditions
32
+ include Router
33
+ include Helpers
34
+ include Plugins
35
+ include Hooks
36
+ include Services
37
+
38
+ # @return [Kanal::Core::Conditions::ConditionStorage]
39
+ attr_reader :condition_storage
40
+ # @return [Kanal::Core::Router::RouterStorage]
41
+ attr_reader :router_storage
42
+ # @return [Kanal::Core::Helpers::ParameterRegistrator]
43
+ attr_reader :input_parameter_registrator
44
+ # @return [Kanal::Core::Helpers::ParameterRegistrator]
45
+ attr_reader :output_parameter_registrator
46
+ # @return [Kanal::Core::Hooks::HookStorage]
47
+ attr_reader :hooks
48
+ # @return [Kanal::Core::Services::ServiceContainer]
49
+ attr_reader :services
50
+
51
+ def initialize
52
+ @hooks = HookStorage.new
53
+ register_hooks
54
+
55
+ @condition_storage = ConditionStorage.new
56
+ @router_storage = RouterStorage.new self
57
+
58
+ @input_parameter_registrator = ParameterRegistrator.new
59
+ @output_parameter_registrator = ParameterRegistrator.new
60
+
61
+ @plugins = []
62
+
63
+ @services = ServiceContainer.new
64
+ end
65
+
66
+ #
67
+ # Method for registering plugins. Plugins should be of type
68
+ # Kanal::Core::Plugins::Plugin. Meaning that any dervied types
69
+ # would be accepted
70
+ #
71
+ # @param [Kanal::Core::Plugins::Plugin] plugin
72
+ #
73
+ # @return [void]
74
+ #
75
+ def register_plugin(plugin)
76
+ unless plugin.is_a? Plugin
77
+ raise "Plugin must be of type Kanal::Core::Plugin or be a class that inherits base Plugin class"
78
+ end
79
+
80
+ begin
81
+ # Checking if name was provided.
82
+ name = plugin.name
83
+
84
+ # TODO: _log that plugin already registered with such name
85
+ return if !name.nil? && plugin_registered?(name)
86
+
87
+ plugin.setup(self)
88
+
89
+ @plugins.append plugin
90
+ # NOTE: Catching here Exception because metho.name can raise ScriptError (derived from Exception)
91
+ # and method .setup can raise ANY type of error.
92
+ # Despite the warnings from linters about "please catch explicitly error or catch StandardError"
93
+ # - sorry, no can't do here
94
+ rescue Exception => e
95
+ name = nil
96
+
97
+ begin
98
+ name = plugin.name
99
+ rescue Exception
100
+ name = "CANT_GET_NAME_DUE_TO_ERROR"
101
+ end
102
+
103
+ # TODO: _log this info in critical error instead of raising exception
104
+ raise "There was a problem while registering plugin named: #{name}. Error: `#{e}`.
105
+ Remember, plugin errors are often due to .name method not overriden or
106
+ having faulty code inside .setup overriden method"
107
+ end
108
+ end
109
+
110
+ #
111
+ # Get registered plugin for modification of some sort
112
+ #
113
+ # @param [Symbol] name <description>
114
+ #
115
+ # @return [<Type>] <description>
116
+ #
117
+ def get_plugin(name)
118
+ @plugins.find { |p| p.name == name }
119
+ end
120
+
121
+ #
122
+ # <Description>
123
+ #
124
+ # @param [Symbol] name <description>
125
+ #
126
+ # @return [Boolean] <description>
127
+ #
128
+ def plugin_registered?(name)
129
+ !get_plugin(name).nil?
130
+ end
131
+
132
+ #
133
+ # Method creates instance of Kanal::Core::Input::Input
134
+ # Note that right before input returned, hook :input_just_created
135
+ # called
136
+ #
137
+ # @return [Kanal::Core::Input::Input] <description>
138
+ #
139
+ def create_input
140
+ input = Input::Input.new @input_parameter_registrator
141
+
142
+ @hooks.call :input_just_created, input
143
+
144
+ input
145
+ end
146
+
147
+ #
148
+ # Method registers parameters that can be set/get in Kanal::Core::Input::Input
149
+ #
150
+ # @param [Symbol] name <description>
151
+ # @param [Boolean] readonly <description>
152
+ #
153
+ # @return [void] <description>
154
+ #
155
+ def register_input_parameter(name, readonly: false)
156
+ @input_parameter_registrator.register_parameter name, readonly: readonly
157
+ end
158
+
159
+ #
160
+ # The same as registering input, but for Kanal::Core::Output::Output
161
+ #
162
+ # @param [Symbol] name <description>
163
+ # @param [Boolean] readonly <description>
164
+ #
165
+ # @return [void] <description>
166
+ #
167
+ def register_output_parameter(name, readonly: false)
168
+ @output_parameter_registrator.register_parameter name, readonly: readonly
169
+ end
170
+
171
+ #
172
+ # Handy method with DSL (Domain Specific Language) that allows
173
+ # condition makers to easily create condition packs and
174
+ # conditions.
175
+ #
176
+ # @param [Symbol] name <description>
177
+ # @yield block with inner DSL for adding conditions to condition pack
178
+ #
179
+ # @return [void] <description>
180
+ #
181
+ def add_condition_pack(name, &block)
182
+ creator = ConditionPackCreator.new name
183
+
184
+ pack = creator.create(&block)
185
+
186
+ @condition_storage.register_condition_pack pack
187
+ end
188
+
189
+ #
190
+ # Gets router by the name or if no argument
191
+ # provided, creates and gets :default router
192
+ # This method usually used without arguments,
193
+ # only in special cases you might need to create separate routers
194
+ # TODO: is creating different routers actually needed? Within the same core
195
+ # What cases are possible for such behaviour? Maybe we should remove the ability...
196
+ #
197
+ # @param [Symbol] name <description>
198
+ #
199
+ # @return [Kanal::Core::Router::Router] <description>
200
+ #
201
+ def router(name = :default)
202
+ @router_storage.get_or_create_router name
203
+ end
204
+
205
+ def register_hooks
206
+ @hooks.register :input_just_created # input
207
+ @hooks.register :input_before_router # input
208
+ @hooks.register :output_before_returned # input, output
209
+ end
210
+
211
+ private :register_hooks
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,26 @@
1
+ module Kanal
2
+ module Core
3
+ module Helpers
4
+ module ConditionFinder
5
+ class ConditionFindResult
6
+ attr_reader :found_condition_pack,
7
+ :found_condition
8
+
9
+ def initialize(found_condition_pack: false, found_condition: false)
10
+ @found_condition_pack = found_condition_pack
11
+ @found_condition = found_condition
12
+ end
13
+
14
+ def found_anything?
15
+ @found_condition || @found_condition_pack
16
+ end
17
+ end
18
+
19
+ class ConditionFinder
20
+ def find_by_name(name)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanal
4
+ module Core
5
+ module Helpers
6
+ # Generic parameter bag class that stores named parameters
7
+ class ParameterBag
8
+ def initialize
9
+ @parameters = {}
10
+ end
11
+
12
+ def get(name)
13
+ @parameters[name]
14
+ end
15
+
16
+ def set(name, value)
17
+ @parameters[name] = value
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ require_relative "parameter_bag"
2
+
3
+ module Kanal
4
+ module Core
5
+ module Helpers
6
+ # Parameter bag but it checks registrator for existence of parameters
7
+ # and if they are has needed by registrator allowances, types etc, whatever
8
+ # registrator rules are stored for property
9
+ class ParameterBagWithRegistrator < ParameterBag
10
+ def initialize(registrator)
11
+ super()
12
+ @registrator = registrator
13
+ end
14
+
15
+ def get(name)
16
+ validate_parameter_registration name
17
+
18
+ super name
19
+ end
20
+
21
+ def set(name, value)
22
+ validate_parameter_registration name
23
+
24
+ readonly = @registrator.get_parameter_registration_if_exists(name).readonly?
25
+
26
+ if readonly
27
+ value_exists = !get(name).nil?
28
+
29
+ if value_exists
30
+ raise "Parameter #{name} is marked readonly! You tried to set it's value, but
31
+ it already has value."
32
+ end
33
+ end
34
+
35
+ super name, value
36
+ end
37
+
38
+ def validate_parameter_registration(name)
39
+ unless @registrator.parameter_registered? name
40
+ raise "Parameter #{name} was not registered! Did you forget to register that parameter?"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanal
4
+ module Core
5
+ module Helpers
6
+ # Module assumes that class where it was included has methods
7
+ # set(name, value)
8
+ # get(name)
9
+ # transforms unknown methods to setters/getters for parameters
10
+ module ParameterFinderWithMethodMissingMixin
11
+ def method_missing(symbol, *args)
12
+ parameter_name = symbol.to_s
13
+ parameter_name.sub! "=", ""
14
+
15
+ parameter_name = parameter_name.to_sym
16
+
17
+ # standard workflow with settings properties with
18
+ # input.prop = 123
19
+ if symbol.to_s.include? "="
20
+ @parameter_bag.set parameter_name, args.first
21
+ else
22
+ # this approach can be used also in dsl
23
+ # like that
24
+ # setters: prop value
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
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ module Kanal
2
+ module Core
3
+ module Helpers
4
+ # For registered property info,
5
+ # this class is used for future additions,
6
+ # maybe type validations or something
7
+ class ParameterRegistration
8
+ def initialize(readonly)
9
+ @readonly = readonly
10
+ end
11
+
12
+ def readonly?
13
+ @readonly
14
+ end
15
+ end
16
+
17
+ # Class holds parameter names that are allowed
18
+ # to be used.
19
+ class ParameterRegistrator
20
+ def initialize
21
+ @parameters_by_name = {}
22
+ end
23
+
24
+ # readonly paramaeter means that once it was initialized - it cannot
25
+ # be changed. handy for input parameters populated by interface or
26
+ # whatever
27
+ def register_parameter(name, readonly: false)
28
+ raise "Parameter named #{name} already registered!" if @parameters_by_name.key? name
29
+
30
+ registration = ParameterRegistration.new readonly
31
+
32
+ @parameters_by_name[name] = registration
33
+ end
34
+
35
+ # returns nil if no parameter registered
36
+ def get_parameter_registration_if_exists(name)
37
+ return nil unless @parameters_by_name.key? name
38
+
39
+ @parameters_by_name[name]
40
+ end
41
+
42
+ def parameter_registered?(name)
43
+ !get_parameter_registration_if_exists(name).nil?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ require "method_source"
2
+
3
+ module Kanal
4
+ module Core
5
+ module Helpers
6
+ # Class helps with parsing router procs for
7
+ # helping forming handy DSL without commas
8
+ class RouterProcParser
9
+ def get_conditions_method_names_from_block(&block)
10
+ source = block.source.to_s
11
+
12
+ method_names = []
13
+
14
+ lines = source.split "\n"
15
+
16
+ lines.each do |l|
17
+ names = get_method_names_from_line l
18
+
19
+ method_names.concat names
20
+ end
21
+
22
+ method_names.uniq
23
+ end
24
+
25
+ def get_method_names_from_line(line)
26
+ method_names = []
27
+
28
+ line = line.lstrip
29
+
30
+ return method_names unless line.start_with? "on"
31
+
32
+ words = line.split
33
+
34
+ condition_pack = words[1]
35
+ condition = words[2]
36
+
37
+ method_names.append condition_pack
38
+ method_names.append condition
39
+
40
+ method_names
41
+ end
42
+
43
+ private :get_method_names_from_line
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanal
4
+ module Core
5
+ module Hooks
6
+ #
7
+ # Allows hooks registration,
8
+ # attaching to hooks, calling hooks with arguments
9
+ #
10
+ class HookStorage
11
+ def initialize
12
+ @listeners = {}
13
+ end
14
+
15
+ #
16
+ # Registers hook in storage. All you need is name
17
+ #
18
+ # @example Registering a hook
19
+ # hook_storage.register(:my_hook) # That is all
20
+ #
21
+ # @param [Symbol] name <description>
22
+ #
23
+ # @return [void] <description>
24
+ #
25
+ # TODO: think about requiring optional string about hook arguments?
26
+ # like register(:my_hook, "name, last_name")
27
+ # or is it too weird and unneeded? I mean besides the documentation
28
+ # there is no way to learn which arguments are used in specific hook.
29
+ # Wondrous world of dynamic languages 🌈🦄
30
+ #
31
+ def register(name)
32
+ return if hook_exists? name
33
+
34
+ @listeners[name] = []
35
+ end
36
+
37
+ #
38
+ # Calling hook with any arguments you want.
39
+ # Well, when you registered hooks you basically had in mind
40
+ # which arguments should be used when calling them
41
+ #
42
+ # @example Calling hook with arguments
43
+ # # Considering names variables (old_name, new_name) are available when calling a hook
44
+ # # This one will call the :name_changed hook with passed arguments.
45
+ # # What does that mean? That possibly there is a listener attached to this hook
46
+ # # @see #attach for next step of this example
47
+ # hook_storage.call :name_changed, old_name, new_name
48
+ #
49
+ # @param [Symbol] name <description>
50
+ # @param [Array] args <description>
51
+ #
52
+ # @return [void] <description>
53
+ #
54
+ def call(name, *args)
55
+ raise "Cannot call hook that is not registered: #{name}" unless hook_exists? name
56
+
57
+ @listeners[name].each do |l|
58
+ l.method(:hook_block).call(*args)
59
+ end
60
+ rescue RuntimeError => e
61
+ raise "There was a problem with calling hooks #{name}. Args: #{args}. More info: #{e}"
62
+ end
63
+
64
+ #
65
+ # Attaches block to a specific hook.
66
+ # You can learn about available hooks by
67
+ # looking into documentation of the specific libraries,
68
+ # using this class.
69
+ #
70
+ # @example Attaching to name changing hook, the next step after @see #call example
71
+ # hook_storage.attach :name_changed do |old_name, new_name|
72
+ # # Here you do something with the provided arguments
73
+ # end
74
+ #
75
+ # NOTE: about weird saving objects with methods instead of blocks
76
+ # see more info in Condition class, you will learn why (hint: LocalJumpError)
77
+ #
78
+ # @param [Symbol] name <description>
79
+ # @yield block that will be executed upon calling hook it was registered to
80
+ #
81
+ # @return [void] <description>
82
+ #
83
+ 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
87
+
88
+ proc_to_lambda_object = Object.new
89
+ proc_to_lambda_object.define_singleton_method(:hook_block, &block)
90
+
91
+ @listeners[name].append proc_to_lambda_object
92
+ end
93
+
94
+ #
95
+ # Self explanatory name of method
96
+ #
97
+ # @param [Symbol] name <description>
98
+ #
99
+ # @return [Boolean] <description>
100
+ #
101
+ def hook_exists?(name)
102
+ !@listeners[name].nil?
103
+ end
104
+
105
+ private :hook_exists?
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/parameter_finder_with_method_missing_mixin"
4
+ require_relative "../helpers/parameter_bag_with_registrator"
5
+
6
+ module Kanal
7
+ module Core
8
+ module Input
9
+ # This class contains all the needed input properties
10
+ class Input
11
+ include Helpers
12
+ include Helpers::ParameterFinderWithMethodMissingMixin
13
+
14
+ def initialize(parameter_registrator)
15
+ @parameter_bag = ParameterBagWithRegistrator.new parameter_registrator
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core"
4
+
5
+ module Kanal
6
+ module Core
7
+ module Interfaces
8
+ # Basic class of interface - interface is basically
9
+ # what is creating inputs and consumes output from the routers
10
+ class Interface
11
+ attr_reader :core
12
+
13
+ #
14
+ # Interface makes all neded configuration, plugin registrations,
15
+ # properties registration in the constructor, which requires core.
16
+ # Originally, there was thoughts of interfaces as top level building
17
+ # blocks for ecosystem, this is why interfaces are doing stuff with core
18
+ # inside constructors. It was originally thought that main application
19
+ # file will host like multiple interfaces.
20
+ # This is also why interfaces had no argument core in constructor,
21
+ # because they created cores inside.
22
+ #
23
+ # @param [Kanal::Core::Core] core <description>
24
+ #
25
+ def initialize(core)
26
+ @core = core
27
+ end
28
+
29
+ #
30
+ # Returns default router from core
31
+ #
32
+ # @return [Kanal::Core::Router::Router] <description>
33
+ #
34
+ def router
35
+ @core.router
36
+ end
37
+
38
+ def modify_core(&block)
39
+ block.call @core
40
+ end
41
+
42
+ #
43
+ # Starting the interface, all the needed
44
+ # machinery for it to fire up goes here
45
+ #
46
+ # @return [void] <description>
47
+ #
48
+ def start
49
+ raise NotImplementedError
50
+ end
51
+
52
+ #
53
+ # Yet to be discovered how to use this.
54
+ # If method #start executes some synchronous code, how would
55
+ # we stop it from outside? I mean maybe with some kind of flag variable inside?
56
+ #
57
+ # @return [<Type>] <description>
58
+ #
59
+ def stop
60
+ raise NotImplementedError
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/parameter_finder_with_method_missing_mixin"
4
+ require_relative "../helpers/parameter_bag_with_registrator"
5
+
6
+ module Kanal
7
+ module Core
8
+ module Output
9
+ # Base class for constructing output that will be given
10
+ # from router node
11
+ class Output
12
+ include Helpers
13
+ include Helpers::ParameterFinderWithMethodMissingMixin
14
+
15
+ attr_reader :input, :core
16
+
17
+ def initialize(parameter_registrator, input, core)
18
+ @input = input
19
+ @core = core
20
+ @parameter_bag = ParameterBagWithRegistrator.new parameter_registrator
21
+ end
22
+
23
+ def configure_dsl(&block)
24
+ instance_eval(&block)
25
+ end
26
+
27
+ private :core
28
+ end
29
+ end
30
+ end
31
+ end