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.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/rubocop/cop/thunderchunk/restricted_access.rb +38 -0
- data/lib/thunderchunk/chunk.rb +123 -0
- data/lib/thunderchunk/configuration.rb +127 -0
- data/lib/thunderchunk/database.rb +64 -0
- data/lib/thunderchunk/default_configuration.rb +29 -0
- data/lib/thunderchunk/dependency.rb +6 -0
- data/lib/thunderchunk/dependency_injector.rb +51 -0
- data/lib/thunderchunk/event.rb +40 -0
- data/lib/thunderchunk/exporter.rb +55 -0
- data/lib/thunderchunk/exporters/base.rb +88 -0
- data/lib/thunderchunk/exporters/events.rb +24 -0
- data/lib/thunderchunk/exporters/operations.rb +30 -0
- data/lib/thunderchunk/external_importer.rb +51 -0
- data/lib/thunderchunk/external_importers/base.rb +91 -0
- data/lib/thunderchunk/external_importers/events.rb +58 -0
- data/lib/thunderchunk/external_importers/operations.rb +28 -0
- data/lib/thunderchunk/loader/load.rb +62 -0
- data/lib/thunderchunk/loader/namespace.rb +47 -0
- data/lib/thunderchunk/loader.rb +66 -0
- data/lib/thunderchunk/model.rb +67 -0
- data/lib/thunderchunk/operation.rb +0 -0
- data/lib/thunderchunk/operations/adapters/use_dependecy.rb +42 -0
- data/lib/thunderchunk/operations/base.rb +12 -0
- data/lib/thunderchunk/operations/external.rb +7 -0
- data/lib/thunderchunk/operations/internal.rb +6 -0
- data/lib/thunderchunk/operations/remote.rb +50 -0
- data/lib/thunderchunk/operations/subscriber.rb +12 -0
- data/lib/thunderchunk/outbox.rb +10 -0
- data/lib/thunderchunk/registries/base.rb +76 -0
- data/lib/thunderchunk/registries/models.rb +26 -0
- data/lib/thunderchunk/registries/operations.rb +17 -0
- data/lib/thunderchunk/registry.rb +55 -0
- data/lib/thunderchunk/rubocop.rb +6 -0
- data/lib/thunderchunk/sender_dock.rb +49 -0
- data/lib/thunderchunk/type_names.rb +35 -0
- data/lib/thunderchunk/usage_importer/kinds/model.rb +14 -0
- data/lib/thunderchunk/usage_importer/use.rb +13 -0
- data/lib/thunderchunk/version.rb +5 -0
- data/lib/thunderchunk.rb +61 -0
- 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,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,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
|