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,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