thunderchunk 0.2.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/rubocop/cop/thunderchunk/restricted_access.rb +38 -0
  4. data/lib/thunderchunk/chunk.rb +123 -0
  5. data/lib/thunderchunk/configuration.rb +127 -0
  6. data/lib/thunderchunk/database.rb +64 -0
  7. data/lib/thunderchunk/default_configuration.rb +29 -0
  8. data/lib/thunderchunk/dependency.rb +6 -0
  9. data/lib/thunderchunk/dependency_injector.rb +51 -0
  10. data/lib/thunderchunk/event.rb +40 -0
  11. data/lib/thunderchunk/exporter.rb +55 -0
  12. data/lib/thunderchunk/exporters/base.rb +88 -0
  13. data/lib/thunderchunk/exporters/events.rb +24 -0
  14. data/lib/thunderchunk/exporters/operations.rb +30 -0
  15. data/lib/thunderchunk/external_importer.rb +51 -0
  16. data/lib/thunderchunk/external_importers/base.rb +91 -0
  17. data/lib/thunderchunk/external_importers/events.rb +58 -0
  18. data/lib/thunderchunk/external_importers/operations.rb +28 -0
  19. data/lib/thunderchunk/loader/load.rb +62 -0
  20. data/lib/thunderchunk/loader/namespace.rb +47 -0
  21. data/lib/thunderchunk/loader.rb +66 -0
  22. data/lib/thunderchunk/model.rb +67 -0
  23. data/lib/thunderchunk/operation.rb +0 -0
  24. data/lib/thunderchunk/operations/adapters/use_dependecy.rb +42 -0
  25. data/lib/thunderchunk/operations/base.rb +12 -0
  26. data/lib/thunderchunk/operations/external.rb +7 -0
  27. data/lib/thunderchunk/operations/internal.rb +6 -0
  28. data/lib/thunderchunk/operations/remote.rb +50 -0
  29. data/lib/thunderchunk/operations/subscriber.rb +12 -0
  30. data/lib/thunderchunk/outbox.rb +10 -0
  31. data/lib/thunderchunk/registries/base.rb +76 -0
  32. data/lib/thunderchunk/registries/models.rb +26 -0
  33. data/lib/thunderchunk/registries/operations.rb +17 -0
  34. data/lib/thunderchunk/registry.rb +55 -0
  35. data/lib/thunderchunk/rubocop.rb +6 -0
  36. data/lib/thunderchunk/sender_dock.rb +49 -0
  37. data/lib/thunderchunk/type_names.rb +35 -0
  38. data/lib/thunderchunk/usage_importer/kinds/model.rb +14 -0
  39. data/lib/thunderchunk/usage_importer/use.rb +13 -0
  40. data/lib/thunderchunk/version.rb +5 -0
  41. data/lib/thunderchunk.rb +61 -0
  42. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c833649b47c00913dff97d2e720c98081377b18423134fe802b974684b0845fe
4
+ data.tar.gz: 3f361ec71acc5ad2da5c9768e7e9f6f5a0ae60c8ae8cfd22d94a3f0372b754af
5
+ SHA512:
6
+ metadata.gz: 61ffba944656899c69bb55224b4e15dec0158a6b8656a681095744420cbd6617c57350b6dae9048a75c9f0fb89a8aebbeae710bb636954bcd9d42f6105205e59
7
+ data.tar.gz: b0ccaacb0c416783f87c1e03fc0f1d7061463eb2cf2a172cf2c3f6f0d534fa8231640bd4c2d1f687e80744ff4f9edee75eedc6f369375fcd5940630801476167
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # thunderchunk
2
+
3
+ Manages a chunk: operations engine, surfaces, schema and migrations. Depends on `thunderfoundation`, never on `thunderbolt`.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThunderChunk
6
+ # Chunk classes live under an anonymous restrict module and everything in a chunk is reached only
7
+ # through `use` (ActiveRecord resolving associations and `class_name:` is the exception).
8
+ # Reflection reaches them anyway, so chunk code may not use it.
9
+ #
10
+ # # bad
11
+ # ThunderChunk::Model.descendants.find { |model| model.name.end_with?('Book') }
12
+ # Models.const_get(:Book)
13
+ # ObjectSpace.each_object(Class)
14
+ #
15
+ # # good
16
+ # use :model, 'Book'
17
+ class RestrictedAccess < Base
18
+ MSG = '`%<code>s` reaches chunk classes by reflection. ' \
19
+ 'Everything in a chunk is only accessible inside an operation via `use`.'
20
+
21
+ RESTRICT_ON_SEND = %i[
22
+ const_get constantize safe_constantize descendants subclasses instance_variable_get
23
+ ].freeze
24
+
25
+ def on_send(node)
26
+ add_offense(node.loc.selector, message: format(MSG, code: node.method_name))
27
+ end
28
+ alias on_csend on_send
29
+
30
+ def on_const(node)
31
+ return unless node.short_name == :ObjectSpace && (node.namespace.nil? || node.namespace.cbase_type?)
32
+
33
+ add_offense(node, message: format(MSG, code: 'ObjectSpace'))
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Chunk
5
+ CONFIGURATION_FILE = 'config/chunk.rb'
6
+ DEFAULT_CONFIGURATION_FILE = Pathname(__dir__).join('default_configuration.rb')
7
+
8
+ attr_reader :chunk_name, :root_path,
9
+ :loader, :registry, :exporter, :external_importer, :sender_dock,
10
+ :dependency_injector,
11
+ :configuration, :database
12
+
13
+ alias config configuration
14
+ alias name chunk_name
15
+
16
+ def initialize(
17
+ chunk_name: nil,
18
+ root_path: nil
19
+ )
20
+ @root_path = Pathname(root_path || bundle_root)
21
+ @chunk_name = chunk_name || @root_path.basename.to_s.camelize
22
+
23
+ @configuration = Configuration.new
24
+ @dependency_injector = DependencyInjector.new(self)
25
+ @database = Database.new(self)
26
+ @booted = false
27
+ end
28
+
29
+ def identifier = name.underscore.to_sym
30
+
31
+ def boot(extra_config = {})
32
+ previous_registry = @registry
33
+ previous_exporter = @exporter
34
+ previous_external_importer = @external_importer
35
+ previous_sender_dock = @sender_dock
36
+ previous_configuration = @configuration
37
+
38
+ @configuration = Configuration.read(DEFAULT_CONFIGURATION_FILE)
39
+ .merge(load_configuration)
40
+ .merge(extra_config)
41
+
42
+ registry = Registry.new(self)
43
+ exporter = Exporter.new(self)
44
+ external_importer = ExternalImporter.new(self)
45
+
46
+ registry.load
47
+ exporter.load
48
+
49
+ # The imports are resolved while the dependencies are injected, so they are in place before it.
50
+ @sender_dock = SenderDock.new(self)
51
+ external_importer.load
52
+
53
+ @registry = registry
54
+ @exporter = exporter
55
+ @external_importer = external_importer
56
+ registry.each_constant do |klass|
57
+ attach(klass, registry)
58
+ inject_dependencies(klass)
59
+ end
60
+ exporter.each_constant do |klass|
61
+ attach(klass, exporter)
62
+ inject_dependencies(klass)
63
+ end
64
+ # WARN: AI MADE
65
+ [registry, exporter].each { |source| source.each_constant { |klass| TypeNames.resolve(self, klass) } }
66
+
67
+ @booted = true
68
+ # WARN: AI MADE
69
+ # The engine has one setting for the process, which runs one chunk.
70
+ ThunderOperation.config.validate_returns = @configuration['operations.validate_returns'] == true
71
+ # WARN: AI MADE
72
+ Time.zone_default = Time.find_zone!(@configuration['time.zone'])
73
+
74
+ @database.connect
75
+ rescue StandardError, ScriptError
76
+ @registry = previous_registry
77
+ @exporter = previous_exporter
78
+ @external_importer = previous_external_importer
79
+ @sender_dock = previous_sender_dock
80
+ @configuration = previous_configuration
81
+ @booted = false
82
+ raise
83
+ end
84
+
85
+ def reload = boot
86
+
87
+ def booted? = @booted
88
+
89
+ private
90
+
91
+ # Bundler is loaded only here: a process started without it (a standalone bundle) does not pay for it when the
92
+ # chunk is given its root.
93
+ def bundle_root
94
+ require 'bundler'
95
+ Bundler.root
96
+ end
97
+
98
+ def load_configuration
99
+ file = @root_path.join(CONFIGURATION_FILE)
100
+ return Configuration.new unless file.file?
101
+
102
+ Configuration.read(file)
103
+ end
104
+
105
+ def attach(klass, origin)
106
+ chunk = self
107
+ klass.define_singleton_method(:chunk) { chunk }
108
+ end
109
+
110
+ def inject_dependencies(klass)
111
+ return unless klass.respond_to?(:dependencies)
112
+
113
+ klass.dependencies.each do |dependency|
114
+ dependency_injector.inject(
115
+ klass,
116
+ dependency.kind,
117
+ dependency.path,
118
+ **dependency.options
119
+ )
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WARN: AI MADE
4
+ module ThunderChunk
5
+ class Configuration < BasicObject
6
+ # Set while a config block runs: the method chain (database.port = 1) works only there; elsewhere config['path'].
7
+ BLOCK_KEY = :thunderchunk_configuration_block
8
+
9
+ def self.read(file)
10
+ configuration = ::Object.new.instance_eval(file.read, file.to_s, 1)
11
+ return configuration if Configuration === configuration
12
+
13
+ raise Error, "#{file} must return a ThunderChunk::Configuration; it returned #{configuration.inspect}"
14
+ end
15
+
16
+ # The block runs on first access, or on top of another configuration in merge, so `boot.registries << X` and
17
+ # `+=` act on the value the other configuration holds.
18
+ def initialize(values = {}, &block)
19
+ @values = {}
20
+ values.each { |key, value| _set(key, value) }
21
+ @block = block
22
+ end
23
+
24
+ def merge(other)
25
+ return Configuration.new(to_h.deep_merge(other.deep_symbolize_keys)) if ::Hash === other
26
+
27
+ merged = Configuration.new(to_h.deep_merge(other._to_h))
28
+ merged._run(other._block) if other._block
29
+ merged
30
+ end
31
+
32
+ def _block = @block
33
+
34
+ def _get(key)
35
+ _evaluate
36
+ @values[key.to_sym]
37
+ end
38
+
39
+ # A Hash becomes a group; a list is copied, so `<<` in a merge does not change the configuration merged from.
40
+ def _set(key, value)
41
+ _evaluate
42
+ value = Configuration.new(value) if ::Hash === value
43
+ value = value.dup if ::Array === value
44
+ @values[key.to_sym] = value
45
+ end
46
+
47
+ def _evaluate
48
+ return unless @block
49
+
50
+ block = @block
51
+ @block = nil
52
+ _run(block)
53
+ end
54
+
55
+ def _run(block)
56
+ previous = ::Fiber[BLOCK_KEY]
57
+ ::Fiber[BLOCK_KEY] = true
58
+ instance_exec(&block)
59
+ ensure
60
+ ::Fiber[BLOCK_KEY] = previous
61
+ end
62
+
63
+ def [](*path)
64
+ value = path.map(&:to_s).join('.').split('.').reduce(self) do |node, key|
65
+ break unless Configuration === node
66
+
67
+ node._get(key)
68
+ end
69
+
70
+ return value unless Configuration === value
71
+
72
+ hash = value.to_h
73
+ hash unless hash.empty?
74
+ end
75
+
76
+ # config['database.port'] = 5432: creates the groups on the way.
77
+ def []=(path, value)
78
+ *groups, key = path.to_s.split('.')
79
+ node = groups.reduce(self) { |group, name| group._get(name) || group._set(name, Configuration.new) }
80
+ ::Kernel.raise ::ArgumentError, "#{path} goes through a value, not a group" unless Configuration === node
81
+
82
+ node._set(key, value)
83
+ end
84
+
85
+ def to_h
86
+ _evaluate
87
+ _to_h
88
+ end
89
+
90
+ # The values without running the block.
91
+ def _to_h
92
+ @values.each_with_object({}) do |(key, value), hash|
93
+ next hash[key] = value unless Configuration === value
94
+
95
+ nested = value.to_h
96
+ hash[key] = nested unless nested.empty?
97
+ end
98
+ end
99
+
100
+ def inspect
101
+ "#<ThunderChunk::Configuration #{to_h.inspect}>"
102
+ end
103
+
104
+ def pretty_print(printer)
105
+ printer.text('#<ThunderChunk::Configuration ')
106
+ printer.pp(to_h)
107
+ printer.text('>')
108
+ end
109
+
110
+ def method_missing(name, *args, **kwargs, &block)
111
+ key = name.to_s
112
+
113
+ unless ::Fiber[BLOCK_KEY]
114
+ ::Kernel.raise ::NoMethodError.new(
115
+ "config.#{key} only works inside ThunderChunk::Configuration.new { ... }; use config['path'] instead", name
116
+ )
117
+ end
118
+
119
+ return _set(key.delete_suffix('='), args.first) if key.end_with?('=')
120
+ return _get(key) || _set(key, Configuration.new) if args.empty? && kwargs.empty? && !block
121
+
122
+ return ::Kernel.public_send(name, *args, **kwargs, &block) if ::Kernel.private_method_defined?(name)
123
+
124
+ ::Kernel.raise ::NoMethodError.new("undefined method '#{name}' for #{inspect}", name)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ # The chunk's database: ActiveRecord::Base, since a process runs one chunk.
5
+ class Database
6
+ REQUIRED_KEYS = %w[adapter].freeze
7
+
8
+ def initialize(chunk)
9
+ @chunk = chunk
10
+ end
11
+
12
+ def configuration
13
+ { database: "#{@chunk.chunk_name.underscore}_#{ENV.fetch('THUNDER_ENV', 'development')}" }
14
+ .merge(@chunk.config['database'] || {})
15
+ end
16
+
17
+ def connection_pool = ActiveRecord::Base.connection_pool
18
+
19
+ def connect
20
+ validate_configuration!
21
+ ActiveRecord::Base.establish_connection(configuration)
22
+ end
23
+
24
+ def connect!
25
+ connect
26
+ verify!
27
+ end
28
+
29
+ def verify!
30
+ connection_pool.with_connection(&:verify!)
31
+ end
32
+
33
+ def create!
34
+ validate_configuration!
35
+ ActiveRecord::Tasks::DatabaseTasks.create(configuration)
36
+ connect!
37
+ end
38
+
39
+ # The adapter's drop connects ActiveRecord::Base to the maintenance database; connect points it back.
40
+ def drop!
41
+ validate_configuration!
42
+ ActiveRecord::Base.connection_handler.clear_all_connections!
43
+ ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
44
+ connect
45
+ end
46
+
47
+ def migrate!
48
+ validate_configuration!
49
+ ActiveRecord::MigrationContext.new(@chunk.root_path.join('database/migrate').to_s).migrate
50
+ end
51
+
52
+ private
53
+
54
+ def validate_configuration!
55
+ raise Error, 'Application not booted' unless @chunk.booted?
56
+
57
+ missing = REQUIRED_KEYS.reject { |key| @chunk.config["database.#{key}"] }
58
+ return if missing.empty?
59
+
60
+ file = @chunk.root_path.join(Chunk::CONFIGURATION_FILE)
61
+ raise Error, "#{file} does not configure #{missing.map { |key| "database.#{key}" }.join(' and ')}"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The framework's defaults, written like a chunk's config/chunk.rb. The chunk reads this file on boot and merges its
4
+ # config/chunk.rb over it.
5
+ ThunderChunk::Configuration.new do
6
+ boot.registries = [
7
+ ThunderChunk::Registries::Models,
8
+ ThunderChunk::Registries::Operations
9
+ ]
10
+
11
+ boot.exporters = []
12
+
13
+ boot.external_importers = [
14
+ ThunderChunk::ExternalImporters::Operations,
15
+ ThunderChunk::ExternalImporters::Events
16
+ ]
17
+
18
+ # WARN: AI MADE
19
+ # Payloads checked against `returns` in development and test only: a mistake in a declaration does not fail an
20
+ # operation in production. A chunk turns it on or off in its config/chunk.rb.
21
+ operations.validate_returns = %w[development test].include?(ENV.fetch('THUNDER_ENV', 'development'))
22
+
23
+ # WARN: AI MADE
24
+ # Time.zone of the process; Time.zone.now and the times read from the database use it.
25
+ time.zone = 'UTC'
26
+
27
+ messaging.broker.type = 'rabbitmq'
28
+ messaging.publisher = 'outbox'
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Dependency < Data.define(:kind, :path, :config, :options)
5
+ end
6
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class DependencyInjector
5
+ MODULE_NAME = :InjectedMethods
6
+
7
+ class InjectedMethods < Module; end
8
+
9
+ def initialize(chunk)
10
+ @chunk = chunk
11
+ end
12
+
13
+ def inject(target_klass, kind, path, config: {}, **options)
14
+ methods_module = methods_module_for(target_klass)
15
+
16
+ depended_class = @chunk.registry.get(kind, path) ||
17
+ @chunk.external_importer.get(kind)&.get(path) ||
18
+ raise(Error, "#{kind} #{path} is not registered")
19
+
20
+ name = options[:as]&.to_s
21
+
22
+ if name&.match?(/\A[[:lower:]_]/)
23
+ methods_module.define_method(name, -> { depended_class })
24
+ else
25
+ set_path_constant(methods_module, name || path, depended_class)
26
+ end
27
+
28
+ target_klass.extend(methods_module) if config[:on_instance] || config[:on_class].blank?
29
+ target_klass.include(methods_module) if config[:on_class] || config[:on_instance].blank?
30
+ end
31
+
32
+ def set_path_constant(methods_module, path, depended_class)
33
+ *namespaces, constant = path.split('.').map(&:camelize)
34
+
35
+ owner = namespaces.reduce(methods_module) do |mod, name|
36
+ mod.const_defined?(name, false) ? mod.const_get(name, false) : mod.const_set(name, Module.new)
37
+ end
38
+
39
+ owner.const_set(constant, depended_class)
40
+ end
41
+
42
+ def methods_module_for(owner)
43
+ return owner.const_get(MODULE_NAME, false) if owner.const_defined?(MODULE_NAME, false)
44
+
45
+ InjectedMethods.new.tap do |methods_module|
46
+ owner.const_set(MODULE_NAME, methods_module)
47
+ owner.include(methods_module)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ # The contract of an event: the fields of its payload, declared as the fields of a `Hash do ... end` param
5
+ # (`field`, `association`, nested `Hash`/`Array`). A payload is published only with exactly that shape.
6
+ class Event
7
+ class << self
8
+ # WARN: AI MADE
9
+ delegate :field, :association, to: :schema
10
+
11
+ def schema
12
+ @schema ||= ThunderOperation::Parameters::HashParameter.new
13
+ end
14
+ end
15
+
16
+ def initialize(chunk, key, publisher)
17
+ @chunk = chunk
18
+ @publisher = publisher
19
+ @key = key
20
+ end
21
+
22
+ def schema
23
+ self.class.schema
24
+ end
25
+
26
+ # WARN: AI MADE
27
+ # A payload with a key the event does not declare, a declared key left out or a value of another type raises
28
+ # Error, whatever operations.validate_returns says: the payload leaves for another chunk.
29
+ def publish(params)
30
+ Console.info(self, 'Event Published', key: @key, params:)
31
+ begin
32
+ schema.check!(params, path: @key)
33
+ rescue ThunderOperation::Exceptions::ParameterError => e
34
+ raise Error, "#{self.class.name}: #{e.message}"
35
+ end
36
+
37
+ @publisher.publish(@key, params)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Exporter
5
+ def initialize(chunk = nil)
6
+ @chunk = chunk
7
+ @exporters = chunk.configuration['boot.exporters'].to_h do |klass|
8
+ [klass.kind, klass.new(chunk)]
9
+ end
10
+ end
11
+
12
+ def load
13
+ @exporters.each_value(&:load)
14
+ end
15
+
16
+ def unload
17
+ @exporters.each_value(&:unload)
18
+ end
19
+
20
+ def hash
21
+ @exporters.transform_values(&:items)
22
+ end
23
+
24
+ def get(kind)
25
+ @exporters[kind.to_sym]
26
+ end
27
+
28
+ def get!(kind, path)
29
+ get(kind, path) || raise(Error, "#{kind} #{path} is not exported")
30
+ end
31
+
32
+ def each_constant(*, &)
33
+ hash.each_value do |items|
34
+ items.each_value(*, &)
35
+ end
36
+ end
37
+
38
+ def clear
39
+ @exporters.each_value(&:clear)
40
+ end
41
+
42
+ def create_kind(exporter)
43
+ kind = exporter.class.kind
44
+
45
+ raise ArgumentError, 'This exporter already has a kind' if @exporters.has_value?(exporter)
46
+ raise ArgumentError, 'This kind already has an exporter' if @exporters.has_key?(kind)
47
+
48
+ @exporters[kind] = exporter
49
+ end
50
+
51
+ def [](value)
52
+ hash[value]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Exporters
5
+ class Base
6
+ def self.kind(value = nil)
7
+ return @kind = value if value
8
+
9
+ @kind || raise(NotImplementedError, "#{name} must declare `kind`")
10
+ end
11
+
12
+ def self.path(value = nil)
13
+ return @path = value if value
14
+
15
+ @path || @kind.to_s.pluralize || raise(NotImplementedError, "#{name} must declare `kind` or `path`")
16
+ end
17
+
18
+ def initialize(chunk)
19
+ @chunk = chunk
20
+ @items = {}
21
+ @loader = Loader.new(@chunk.name, @chunk.root_path)
22
+ end
23
+
24
+ attr_reader :items
25
+
26
+ def add_constant(constant)
27
+ return false unless constant.is_a?(Class)
28
+
29
+ key = key_for(constant)
30
+ return false unless key
31
+
32
+ add(key, constant)
33
+ true
34
+ end
35
+
36
+ def key_for(_constant)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def call(**)
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def exports?(path)
45
+ @items.key?(path)
46
+ end
47
+
48
+ def get(path)
49
+ @items[path]
50
+ end
51
+
52
+ def get!(path)
53
+ get(path) || raise(Error, "#{self.class.kind} #{path} is not exported")
54
+ end
55
+
56
+ def add(path, object)
57
+ @items[path] = object
58
+ end
59
+
60
+ def clear
61
+ @items.clear
62
+ end
63
+
64
+ def load
65
+ @items = {}
66
+
67
+ @loader.load(["app/external/#{self.class.path}"])
68
+
69
+ @loader.each do |_parent, _name, constant|
70
+ add_constant(constant)
71
+ end
72
+
73
+ @loader.unload
74
+ end
75
+
76
+ private
77
+
78
+ def key_from_name(constant, *namespace)
79
+ segments = constant.name.split('::')
80
+ index = segments.each_index.find { |i| segments[i, namespace.size] == namespace }
81
+ return unless index
82
+
83
+ rest = segments.drop(index + namespace.size)
84
+ rest.map(&:underscore).join('.') unless rest.empty?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Exporters
5
+ class Events < Base
6
+ kind :event
7
+ path 'messaging/subscribers'
8
+
9
+ def key_for(constant)
10
+ key_from_name(constant, 'External', 'Messaging', 'Subscribers')&.then do |key|
11
+ unless constant < ThunderChunk::Operations::Subscriber
12
+ raise RegistryRequirementError, "#{constant.name} must inherit from ThunderChunk::Operations::Subscriber"
13
+ end
14
+
15
+ key.sub('.', '/')
16
+ end
17
+ end
18
+
19
+ def call(key, params:)
20
+ get!(key).call(**params)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderChunk
4
+ class Exporters
5
+ class Operations < Base
6
+ kind :operation
7
+
8
+ def key_for(constant)
9
+ key_from_name(constant, 'External', 'Operations')&.tap do |key|
10
+ unless constant < ThunderChunk::Operations::External
11
+ raise RegistryRequirementError, "#{constant.name} must inherit from ThunderChunk::Operations::External"
12
+ end
13
+ # TODO: Here is the corerct location to this valiadtion?
14
+ # WARN: AI MADE
15
+ unless constant.returns_frame
16
+ raise RegistryRequirementError,
17
+ "#{constant.name} must declare `returns Hash do ... end` (fields, or keys and values) or " \
18
+ '`returns Array do items Hash do ... end end`'
19
+ end
20
+ end
21
+ end
22
+
23
+ def call(path, params:)
24
+ operation = get!(path)
25
+
26
+ operation.call(**params)
27
+ end
28
+ end
29
+ end
30
+ end