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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class ExternalImporter
|
|
5
|
+
def initialize(chunk = nil)
|
|
6
|
+
@chunk = chunk
|
|
7
|
+
@importers = chunk.configuration['boot.external_importers'].to_h do |klass|
|
|
8
|
+
[klass.kind, klass.new(chunk)]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def load
|
|
13
|
+
@importers.each_value(&:load)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def hash
|
|
17
|
+
@importers.transform_values(&:items)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(kind)
|
|
21
|
+
@importers[kind.to_sym]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get!(kind, path)
|
|
25
|
+
get(kind)&.get(path) || raise(Error, "#{kind} #{path} is not imported")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def each_constant(*, &)
|
|
29
|
+
hash.each_value do |items|
|
|
30
|
+
items.each_value(*, &)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
@importers.each_value(&:clear)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_kind(importer)
|
|
39
|
+
kind = importer.class.kind
|
|
40
|
+
|
|
41
|
+
raise ArgumentError, 'This importer already has a kind' if @importers.has_value?(importer)
|
|
42
|
+
raise ArgumentError, 'This kind already has an importer' if @importers.has_key?(kind)
|
|
43
|
+
|
|
44
|
+
@importers[kind] = importer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def [](value)
|
|
48
|
+
hash[value]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class ExternalImporters
|
|
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
|
+
if value
|
|
14
|
+
@path = value
|
|
15
|
+
@should_load = true
|
|
16
|
+
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@path || @kind.to_s.pluralize || raise(NotImplementedError, "#{name} must declare `kind` or `path`")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether load reads app/external/<path> at boot. Off by default: an importer that builds each import on get
|
|
24
|
+
# has nothing to load.
|
|
25
|
+
def self.should_load(value = nil)
|
|
26
|
+
return @should_load = value unless value.nil?
|
|
27
|
+
|
|
28
|
+
@should_load || false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :items
|
|
32
|
+
|
|
33
|
+
def initialize(chunk)
|
|
34
|
+
@chunk = chunk
|
|
35
|
+
@items = {}
|
|
36
|
+
@loader = Loader.new(@chunk.name, @chunk.root_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_constant(constant)
|
|
40
|
+
return false unless constant.is_a?(Class)
|
|
41
|
+
|
|
42
|
+
key = key_for(constant)
|
|
43
|
+
return false unless key
|
|
44
|
+
|
|
45
|
+
add(key, constant)
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def key_for(_constant)
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get(_path)
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add(path, object)
|
|
58
|
+
@items[path] = object
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load
|
|
62
|
+
return unless self.class.should_load
|
|
63
|
+
|
|
64
|
+
@items = {}
|
|
65
|
+
|
|
66
|
+
@loader.load(["app/external/#{self.class.path}"])
|
|
67
|
+
|
|
68
|
+
@loader.each do |_parent, _name, constant|
|
|
69
|
+
add_constant(constant)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@loader.unload
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def clear
|
|
76
|
+
@items.clear
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def key_from_name(constant, *namespace)
|
|
82
|
+
segments = constant.name.split('::')
|
|
83
|
+
index = segments.each_index.find { |i| segments[i, namespace.size] == namespace }
|
|
84
|
+
return unless index
|
|
85
|
+
|
|
86
|
+
rest = segments.drop(index + namespace.size)
|
|
87
|
+
rest.map(&:underscore).join('.') unless rest.empty?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class ExternalImporters
|
|
5
|
+
class Events < Base
|
|
6
|
+
class << self
|
|
7
|
+
# WARN: AI MADE
|
|
8
|
+
attr_accessor :resolver
|
|
9
|
+
# WARN: ---
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
kind :event
|
|
13
|
+
path 'messaging/events/'
|
|
14
|
+
|
|
15
|
+
def key_for(constant)
|
|
16
|
+
key_from_name(constant, 'External', 'Messaging', 'Events')&.tap do |key|
|
|
17
|
+
unless constant < ThunderChunk::Event
|
|
18
|
+
raise RegistryRequirementError, "#{constant.name} must inherit from ThunderChunk::Event"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# books.add
|
|
24
|
+
def get(key)
|
|
25
|
+
event_klass = @items[key] || raise(Error, 'this event is not registered')
|
|
26
|
+
|
|
27
|
+
event_klass.new(
|
|
28
|
+
@chunk,
|
|
29
|
+
key,
|
|
30
|
+
self.class.publisher(@chunk)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# WARN: AI MADE
|
|
35
|
+
# One publisher per chunk: each one is built with the config of its chunk.
|
|
36
|
+
def self.publisher(chunk)
|
|
37
|
+
@publishers ||= {}.compare_by_identity
|
|
38
|
+
@publishers[chunk] ||= begin
|
|
39
|
+
# WARN: ---
|
|
40
|
+
broker = chunk.config['messaging.broker.type']
|
|
41
|
+
publisher = chunk.config['messaging.publisher']
|
|
42
|
+
|
|
43
|
+
# WARN: AI MADE
|
|
44
|
+
resolver = Events.resolver || raise(Error, "no publisher for #{broker} #{publisher}: require 'thunderwire'")
|
|
45
|
+
# WARN: ---
|
|
46
|
+
|
|
47
|
+
publisher_klass = resolver.call(broker, publisher)
|
|
48
|
+
|
|
49
|
+
configuration = publisher_klass.configurations.to_h do |key, path|
|
|
50
|
+
[key, chunk.config[path]]
|
|
51
|
+
end.compact
|
|
52
|
+
|
|
53
|
+
publisher_klass.new(**configuration)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class ExternalImporters
|
|
5
|
+
class Operations < Base
|
|
6
|
+
kind :external_operation
|
|
7
|
+
|
|
8
|
+
# library/books.add
|
|
9
|
+
def get(path)
|
|
10
|
+
unless path.match?(%r{\A[a-z]+(_[a-z]+)*/[a-z]+([._][a-z]+)*\z})
|
|
11
|
+
raise Error, "invalid operation path #{path.inspect}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@items[path] ||= begin
|
|
15
|
+
chunk_identifier, key = path.split('/')
|
|
16
|
+
dock = @chunk.sender_dock
|
|
17
|
+
# WARN: AI MADE
|
|
18
|
+
# The sender (and the remote's url in the config) is found on boot, so a missing url fails there; in specs
|
|
19
|
+
# every call is stubbed (ThunderSpec), so it is found on the first call only.
|
|
20
|
+
sender = -> { dock.sender_for(chunk_identifier, :operation) }
|
|
21
|
+
sender.call unless ENV['THUNDER_ENV'] == 'test'
|
|
22
|
+
|
|
23
|
+
ThunderChunk::Operations::Remote.new(sender:, key:)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class Loader
|
|
5
|
+
class Load
|
|
6
|
+
# WARN: AI MADE
|
|
7
|
+
# The classes the chunk's code inherits from (ApplicationModel, InternalOperation): a file there defines one
|
|
8
|
+
# class at the top, without the chunk's modules, and any file of the chunk reaches it by its bare name.
|
|
9
|
+
COMMON = 'app/common'
|
|
10
|
+
|
|
11
|
+
def initialize(restrict_module, chunk_module, root_path)
|
|
12
|
+
@restrict_module = restrict_module
|
|
13
|
+
@root_path = root_path
|
|
14
|
+
@root = Namespace.new(chunk_module, root_path.join('app'), self)
|
|
15
|
+
@loaded = Set.new
|
|
16
|
+
@running = false
|
|
17
|
+
|
|
18
|
+
load = self
|
|
19
|
+
restrict_module.define_singleton_method(:const_missing) { |name| load.resolve_common(name) || super(name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def running? = @running
|
|
23
|
+
|
|
24
|
+
def run(files)
|
|
25
|
+
@running = true
|
|
26
|
+
files.each { |file| load_file(file) }
|
|
27
|
+
ensure
|
|
28
|
+
@running = false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_file(file)
|
|
32
|
+
return false unless @loaded.add?(file)
|
|
33
|
+
|
|
34
|
+
parent = common?(file) ? @restrict_module : @root.namespace_for(file.dirname).mod
|
|
35
|
+
@restrict_module.module_eval(file.read, file.to_s, 1)
|
|
36
|
+
validate_file_constant!(file, parent)
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# WARN: AI MADE
|
|
41
|
+
# The common class with this name, loaded on its first use; nil when app/common has no file for it.
|
|
42
|
+
def resolve_common(name)
|
|
43
|
+
return unless running?
|
|
44
|
+
|
|
45
|
+
file = @root_path.glob("#{COMMON}/**/#{name.to_s.underscore}.rb").min
|
|
46
|
+
file && load_file(file) && @restrict_module.const_get(name, false)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def common?(file) = file.to_s.start_with?("#{@root_path.join(COMMON)}/")
|
|
52
|
+
|
|
53
|
+
def validate_file_constant!(file, parent)
|
|
54
|
+
constant = file.basename('.rb').to_s.camelize
|
|
55
|
+
return if parent.const_defined?(constant, false)
|
|
56
|
+
|
|
57
|
+
expected = parent.equal?(@restrict_module) ? constant : "#{parent.name}::#{constant}"
|
|
58
|
+
raise FileConstantMismatchError, "expected #{file.relative_path_from(@root_path)} to define #{expected}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class Loader
|
|
5
|
+
class Namespace
|
|
6
|
+
attr_reader :mod
|
|
7
|
+
|
|
8
|
+
def initialize(mod, directory, load)
|
|
9
|
+
@mod = mod
|
|
10
|
+
@directory = directory
|
|
11
|
+
@load = load
|
|
12
|
+
@children = {}
|
|
13
|
+
|
|
14
|
+
namespace = self
|
|
15
|
+
mod.define_singleton_method(:const_missing) { |name| namespace.resolve(name) || super(name) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def namespace_for(directory)
|
|
19
|
+
directory.relative_path_from(@directory).each_filename.reduce(self) { |namespace, name| namespace.child(name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def child(name)
|
|
23
|
+
@children[name] ||= begin
|
|
24
|
+
constant = name.camelize
|
|
25
|
+
mod = @mod.const_defined?(constant, false) ? @mod.const_get(constant, false) : @mod.const_set(constant, Module.new)
|
|
26
|
+
Namespace.new(mod, @directory.join(name), @load)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve(name)
|
|
31
|
+
return unless @load.running?
|
|
32
|
+
|
|
33
|
+
path = @directory.join(name.to_s.underscore)
|
|
34
|
+
file = path.sub_ext('.rb')
|
|
35
|
+
|
|
36
|
+
if file.file?
|
|
37
|
+
@load.load_file(file) && @mod.const_get(name, false)
|
|
38
|
+
elsif path.directory?
|
|
39
|
+
child(path.basename.to_s).mod
|
|
40
|
+
else
|
|
41
|
+
# WARN: AI MADE
|
|
42
|
+
@load.resolve_common(name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
LOADING_ORDER = %w[
|
|
5
|
+
app/internal/models
|
|
6
|
+
app/internal/operations
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
9
|
+
class Loader
|
|
10
|
+
attr_reader :chunk_module
|
|
11
|
+
|
|
12
|
+
def initialize(chunk_name, root_path)
|
|
13
|
+
@root_path = root_path
|
|
14
|
+
@chunk_name = chunk_name
|
|
15
|
+
@restrict_module = Module.new.tap { |mod| mod.set_temporary_name('<restrict>') }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load(order = LOADING_ORDER)
|
|
19
|
+
@restrict_module.send(:remove_const, @chunk_name) if @restrict_module.const_defined?(@chunk_name, false)
|
|
20
|
+
@chunk_module = named_module(@chunk_name)
|
|
21
|
+
@restrict_module.const_set(@chunk_name, @chunk_module)
|
|
22
|
+
|
|
23
|
+
Load.new(@restrict_module, @chunk_module, @root_path).run(files(order))
|
|
24
|
+
rescue Exception
|
|
25
|
+
unload
|
|
26
|
+
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
constants_of(@restrict_module).reverse_each(&block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unload
|
|
35
|
+
constants_of(@restrict_module).reverse_each do |parent, name, value|
|
|
36
|
+
yield(parent, name, value) if block_given?
|
|
37
|
+
|
|
38
|
+
parent.send(:remove_const, name)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def files(order)
|
|
45
|
+
order.flat_map { |path| Dir.glob(@root_path.join(path, '**', '*.rb')).sort.map { Pathname(it) } }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def constants_of(parent)
|
|
49
|
+
parent.constants(false).flat_map do |name|
|
|
50
|
+
value = parent.const_get(name, false)
|
|
51
|
+
namespace = value.equal?(@chunk_module) || (value.instance_of?(Module) && value.name == "#{parent.name}::#{name}")
|
|
52
|
+
|
|
53
|
+
[[parent, name, value], *(namespace ? constants_of(value) : [])]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def named_module(name)
|
|
58
|
+
raise Error, "::#{name} already exists; it would collide with the chunk" if Object.const_defined?(name, false)
|
|
59
|
+
|
|
60
|
+
Module.new.tap do |mod|
|
|
61
|
+
Object.const_set(name, mod)
|
|
62
|
+
Object.send(:remove_const, name)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
class Model < ActiveRecord::Base
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
|
|
7
|
+
class_attribute :chunk_key, :chunk_registry, instance_writer: false, instance_predicate: false
|
|
8
|
+
|
|
9
|
+
# WARN: AI MADE
|
|
10
|
+
# A uuid primary key gets a UUIDv7 (ordered by creation time) when the record is created without one, as in
|
|
11
|
+
# tcard_backend's ApplicationRecord. The column default (gen_random_uuid(), a v4) is left for rows inserted in SQL.
|
|
12
|
+
before_create :assign_uuid_v7_id
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def module_parent = Object
|
|
16
|
+
def module_parents = [Object]
|
|
17
|
+
|
|
18
|
+
def chunk_name
|
|
19
|
+
raise Error, "#{name} is not registered yet" unless chunk_key
|
|
20
|
+
|
|
21
|
+
chunk_key.split('.').map(&:camelize).join('::')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def model_name
|
|
25
|
+
@model_name ||= ActiveModel::Name.new(self, nil, chunk_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sti_name = chunk_name
|
|
29
|
+
def polymorphic_name = chunk_name
|
|
30
|
+
|
|
31
|
+
def sti_class_for(type_name)
|
|
32
|
+
model_for(type_name) || raise(ActiveRecord::SubclassNotFound, "unknown model #{type_name} for #{chunk_name}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def polymorphic_class_for(name) = model_for!(name)
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def compute_type(type_name)
|
|
40
|
+
return model_for!(type_name.delete_prefix('::')) if type_name.start_with?('::')
|
|
41
|
+
|
|
42
|
+
scope = chunk_name.split('::')[0...-1]
|
|
43
|
+
candidates = scope.size.downto(0).map { |n| [*scope.first(n), type_name].join('::') }
|
|
44
|
+
candidates.each { |candidate| (klass = model_for(candidate)) and return klass }
|
|
45
|
+
|
|
46
|
+
raise NameError.new("model #{type_name} not found from #{chunk_name} (tried #{candidates.join(', ')})", type_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def model_for(name) = chunk_registry.get(name.underscore.tr('/', '.'))
|
|
50
|
+
def model_for!(name) = model_for(name) || raise(NameError.new("unknown model #{name}", name))
|
|
51
|
+
|
|
52
|
+
def compute_table_name
|
|
53
|
+
base_class? ? chunk_name.underscore.tr('/', '_').pluralize : superclass.table_name
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def assign_uuid_v7_id
|
|
60
|
+
return if id.present?
|
|
61
|
+
return unless self.class.primary_key == 'id'
|
|
62
|
+
return unless self.class.columns_hash['id']&.type == :uuid
|
|
63
|
+
|
|
64
|
+
self.id = SecureRandom.uuid_v7
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
module Operations
|
|
5
|
+
module Adapters
|
|
6
|
+
class UseDependency < ThunderOperation::Adapter
|
|
7
|
+
adapted do
|
|
8
|
+
def self.use(kind, path, **options)
|
|
9
|
+
dependencies << Dependency.new(
|
|
10
|
+
kind: kind.to_sym,
|
|
11
|
+
path: path.to_s,
|
|
12
|
+
config: { on_instance: true },
|
|
13
|
+
options:
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.dependencies
|
|
18
|
+
@dependencies ||= []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.const_missing(name)
|
|
22
|
+
raise NameError.new("#{name} is not available in this operation; #{use_hint(name)}", name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.use_hint(name)
|
|
26
|
+
key = name.to_s.underscore
|
|
27
|
+
return "declare it with `use :<kind>, '#{key}'`" unless respond_to?(:chunk) && chunk
|
|
28
|
+
|
|
29
|
+
matches = chunk.registry.hash.flat_map do |kind, items|
|
|
30
|
+
items.keys.sort.select { |path| path == key || path.start_with?("#{key}.") }.map { |path| "`use :#{kind}, '#{path}'`" }
|
|
31
|
+
end
|
|
32
|
+
return "nothing named #{key} is registered in the chunk" if matches.empty?
|
|
33
|
+
|
|
34
|
+
"declare it with #{matches.join(' or ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method :use_hint
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
module ThunderChunk
|
|
3
|
+
module Operations
|
|
4
|
+
class Base < ThunderOperation::Base
|
|
5
|
+
append_adapter ThunderOperation::Adapters::RichParameters
|
|
6
|
+
append_adapter ThunderOperation::Adapters::Interruptable
|
|
7
|
+
append_adapter Adapters::UseDependency
|
|
8
|
+
# WARN: AI MADE
|
|
9
|
+
append_adapter ThunderOperation::Adapters::Validations
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ThunderChunk
|
|
2
|
+
module Operations
|
|
3
|
+
class Remote
|
|
4
|
+
class RemoteFailure < ThunderChunk::Error
|
|
5
|
+
attr_reader :response
|
|
6
|
+
|
|
7
|
+
def initialize(response)
|
|
8
|
+
@response = response
|
|
9
|
+
super("Remote operation failed: #{response.inspect}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The error of a Response that failed in another chunk: the original exception stays there, so this one carries
|
|
14
|
+
# its message, class name, extras and trail.
|
|
15
|
+
class RemoteResponseFailure < ThunderChunk::Error
|
|
16
|
+
attr_reader :error_class, :extras, :trail
|
|
17
|
+
|
|
18
|
+
def initialize(message, error_class:, extras:, trail:)
|
|
19
|
+
@error_class = error_class
|
|
20
|
+
@extras = extras
|
|
21
|
+
@trail = trail
|
|
22
|
+
super(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# WARN: AI MADE
|
|
27
|
+
# sender: a sender, or a lambda that returns one when the first call needs it.
|
|
28
|
+
def initialize(sender:, key:)
|
|
29
|
+
@sender = sender
|
|
30
|
+
@key = key
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(**params) = sender.call(@key, params)
|
|
34
|
+
|
|
35
|
+
def call!(**params)
|
|
36
|
+
response = call(**params)
|
|
37
|
+
raise RemoteFailure, response if response.failure?
|
|
38
|
+
|
|
39
|
+
response
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def sender
|
|
45
|
+
@sender = @sender.call if @sender.is_a?(Proc)
|
|
46
|
+
@sender
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderChunk
|
|
4
|
+
module Operations
|
|
5
|
+
class Subscriber < Base
|
|
6
|
+
# Every param of a subscriber is optional: a key the event leaves out arrives as nil.
|
|
7
|
+
def self.param(field_name, *args, **options, &)
|
|
8
|
+
super(field_name, *args, { **options, optional: true }, &)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|