dexkit 0.1.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/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/guides/llm/OPERATION.md +553 -0
- data/lib/dex/error.rb +17 -0
- data/lib/dex/match.rb +13 -0
- data/lib/dex/operation/async_proxy.rb +115 -0
- data/lib/dex/operation/async_wrapper.rb +35 -0
- data/lib/dex/operation/callback_wrapper.rb +113 -0
- data/lib/dex/operation/jobs.rb +64 -0
- data/lib/dex/operation/lock_wrapper.rb +77 -0
- data/lib/dex/operation/outcome.rb +70 -0
- data/lib/dex/operation/pipeline.rb +60 -0
- data/lib/dex/operation/props_setup.rb +50 -0
- data/lib/dex/operation/record_backend.rb +64 -0
- data/lib/dex/operation/record_wrapper.rb +135 -0
- data/lib/dex/operation/rescue_wrapper.rb +63 -0
- data/lib/dex/operation/result_wrapper.rb +92 -0
- data/lib/dex/operation/safe_wrapper.rb +9 -0
- data/lib/dex/operation/settings.rb +26 -0
- data/lib/dex/operation/transaction_adapter.rb +54 -0
- data/lib/dex/operation/transaction_wrapper.rb +87 -0
- data/lib/dex/operation.rb +192 -0
- data/lib/dex/ref_type.rb +34 -0
- data/lib/dex/test_helpers/assertions.rb +310 -0
- data/lib/dex/test_helpers/execution.rb +28 -0
- data/lib/dex/test_helpers/stubbing.rb +59 -0
- data/lib/dex/test_helpers.rb +146 -0
- data/lib/dex/test_log.rb +55 -0
- data/lib/dex/version.rb +5 -0
- data/lib/dexkit.rb +57 -0
- metadata +160 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module RecordWrapper
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def _record_wrap
|
|
10
|
+
halted = nil
|
|
11
|
+
result = catch(:_dex_halt) { yield }
|
|
12
|
+
|
|
13
|
+
if result.is_a?(Operation::Halt)
|
|
14
|
+
halted = result
|
|
15
|
+
result = halted.success? ? halted.value : nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if halted.nil? || halted.success?
|
|
19
|
+
if _record_has_pending_record?
|
|
20
|
+
_record_update_done!(result)
|
|
21
|
+
elsif _record_enabled?
|
|
22
|
+
_record_save!(result)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
throw(:_dex_halt, halted) if halted
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
RECORD_KNOWN_OPTIONS = %i[params response].freeze
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
def record(enabled = nil, **options)
|
|
34
|
+
unknown = options.keys - RecordWrapper::RECORD_KNOWN_OPTIONS
|
|
35
|
+
if unknown.any?
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"unknown record option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
38
|
+
"Known: #{RecordWrapper::RECORD_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if enabled == false
|
|
42
|
+
set :record, enabled: false
|
|
43
|
+
elsif enabled == true || enabled.nil?
|
|
44
|
+
merged = { enabled: true, params: true, response: true }.merge(options)
|
|
45
|
+
set :record, **merged
|
|
46
|
+
else
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"record expects true, false, or nil, got: #{enabled.inspect}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def _record_enabled?
|
|
56
|
+
return false unless Dex.record_backend
|
|
57
|
+
return false unless self.class.name
|
|
58
|
+
|
|
59
|
+
record_settings = self.class.settings_for(:record)
|
|
60
|
+
record_settings.fetch(:enabled, true)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _record_has_pending_record?
|
|
64
|
+
defined?(@_dex_record_id) && @_dex_record_id
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _record_save!(result)
|
|
68
|
+
Dex.record_backend.create_record(_record_attributes(result))
|
|
69
|
+
rescue => e
|
|
70
|
+
_record_handle_error(e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def _record_update_done!(result)
|
|
74
|
+
attrs = { status: "done", performed_at: _record_current_time }
|
|
75
|
+
attrs[:response] = _record_response(result) if _record_response?
|
|
76
|
+
Dex.record_backend.update_record(@_dex_record_id, attrs)
|
|
77
|
+
rescue => e
|
|
78
|
+
_record_handle_error(e)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def _record_attributes(result)
|
|
82
|
+
attrs = { name: self.class.name, performed_at: _record_current_time, status: "done" }
|
|
83
|
+
attrs[:params] = _record_params? ? _record_params : nil
|
|
84
|
+
attrs[:response] = _record_response? ? _record_response(result) : nil
|
|
85
|
+
attrs
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def _record_params
|
|
89
|
+
_props_as_json
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def _record_params?
|
|
93
|
+
self.class.settings_for(:record).fetch(:params, true)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def _record_response?
|
|
97
|
+
self.class.settings_for(:record).fetch(:response, true)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def _record_response(result)
|
|
101
|
+
success_type = self.class.respond_to?(:_success_type) && self.class._success_type
|
|
102
|
+
|
|
103
|
+
if success_type
|
|
104
|
+
_record_serialize_typed_result(result, success_type)
|
|
105
|
+
else
|
|
106
|
+
case result
|
|
107
|
+
when nil then nil
|
|
108
|
+
when Hash then result
|
|
109
|
+
else { value: result }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def _record_serialize_typed_result(result, type)
|
|
115
|
+
return nil if result.nil?
|
|
116
|
+
|
|
117
|
+
ref_type = self.class.send(:_dex_find_ref_type, type)
|
|
118
|
+
if ref_type && result.respond_to?(:id)
|
|
119
|
+
result.id
|
|
120
|
+
else
|
|
121
|
+
result.respond_to?(:as_json) ? result.as_json : result
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def _record_current_time
|
|
126
|
+
Time.respond_to?(:current) ? Time.current : Time.now
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def _record_handle_error(error)
|
|
130
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
131
|
+
Rails.logger.warn "[Dex] Failed to record operation: #{error.message}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module RescueWrapper
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def rescue_from(*exception_classes, as:, message: nil)
|
|
11
|
+
raise ArgumentError, "rescue_from requires at least one exception class" if exception_classes.empty?
|
|
12
|
+
|
|
13
|
+
invalid = exception_classes.reject { |k| k.is_a?(Module) && k <= Exception }
|
|
14
|
+
if invalid.any?
|
|
15
|
+
raise ArgumentError,
|
|
16
|
+
"rescue_from expects Exception subclasses, got: #{invalid.map(&:inspect).join(", ")}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, "rescue_from :as must be a Symbol, got: #{as.inspect}" unless as.is_a?(Symbol)
|
|
20
|
+
|
|
21
|
+
exception_classes.each do |klass|
|
|
22
|
+
_rescue_own << { exception_class: klass, code: as, message: message }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _rescue_handlers
|
|
27
|
+
parent = superclass.respond_to?(:_rescue_handlers) ? superclass._rescue_handlers : []
|
|
28
|
+
parent + _rescue_own
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def _rescue_own
|
|
34
|
+
@_rescue_handlers ||= []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _rescue_wrap
|
|
39
|
+
yield
|
|
40
|
+
rescue Dex::Error
|
|
41
|
+
raise
|
|
42
|
+
rescue => e
|
|
43
|
+
handler = _rescue_find_handler(e)
|
|
44
|
+
raise unless handler
|
|
45
|
+
|
|
46
|
+
_rescue_convert!(e, handler)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def _rescue_find_handler(exception)
|
|
52
|
+
self.class._rescue_handlers.reverse_each do |handler|
|
|
53
|
+
return handler if exception.is_a?(handler[:exception_class])
|
|
54
|
+
end
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def _rescue_convert!(exception, handler)
|
|
59
|
+
msg = handler[:message] || exception.message
|
|
60
|
+
raise Dex::Error.new(handler[:code], msg, details: { original: exception })
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module ResultWrapper
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def success(type)
|
|
7
|
+
@_success_type = type
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def error(*codes)
|
|
11
|
+
invalid = codes.reject { |c| c.is_a?(Symbol) }
|
|
12
|
+
if invalid.any?
|
|
13
|
+
raise ArgumentError, "error codes must be Symbols, got: #{invalid.map(&:inspect).join(", ")}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@_declared_errors ||= []
|
|
17
|
+
@_declared_errors.concat(codes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def _success_type
|
|
21
|
+
@_success_type || (superclass.respond_to?(:_success_type) ? superclass._success_type : nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def _declared_errors
|
|
25
|
+
parent = superclass.respond_to?(:_declared_errors) ? superclass._declared_errors : []
|
|
26
|
+
own = @_declared_errors || []
|
|
27
|
+
(parent + own).uniq
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def _has_declared_errors?
|
|
31
|
+
_declared_errors.any?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.included(base)
|
|
36
|
+
base.extend(ClassMethods)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def _result_wrap
|
|
40
|
+
halted = catch(:_dex_halt) { yield }
|
|
41
|
+
if halted.is_a?(Operation::Halt)
|
|
42
|
+
if halted.success?
|
|
43
|
+
_result_validate_success_type!(halted.value)
|
|
44
|
+
halted.value
|
|
45
|
+
else
|
|
46
|
+
raise Dex::Error.new(halted.error_code, halted.error_message, details: halted.error_details)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
_result_validate_success_type!(halted)
|
|
50
|
+
halted
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def error!(code, message = nil, details: nil)
|
|
55
|
+
if self.class._has_declared_errors? && !self.class._declared_errors.include?(code)
|
|
56
|
+
raise ArgumentError, "Undeclared error code: #{code.inspect}. Declared: #{self.class._declared_errors.inspect}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
throw(:_dex_halt, Operation::Halt.new(type: :error, error_code: code, error_message: message,
|
|
60
|
+
error_details: details))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def success!(value = nil, **attrs)
|
|
64
|
+
throw(:_dex_halt, Operation::Halt.new(type: :success, value: attrs.empty? ? value : attrs))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def assert!(*args, &block)
|
|
68
|
+
if block
|
|
69
|
+
code = args[0]
|
|
70
|
+
value = yield
|
|
71
|
+
else
|
|
72
|
+
value, code = args
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
error!(code) unless value
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def _result_validate_success_type!(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
|
|
84
|
+
success_type = self.class._success_type
|
|
85
|
+
return unless success_type
|
|
86
|
+
return if success_type === value
|
|
87
|
+
|
|
88
|
+
raise ArgumentError,
|
|
89
|
+
"#{self.class.name || "Operation"} declared `success #{success_type.inspect}` but returned #{value.class}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module Settings
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def set(key, **options)
|
|
7
|
+
@_settings ||= {}
|
|
8
|
+
@_settings[key] = (@_settings[key] || {}).merge(options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def settings_for(key)
|
|
12
|
+
parent_settings = if superclass.respond_to?(:settings_for)
|
|
13
|
+
superclass.settings_for(key) || {}
|
|
14
|
+
else
|
|
15
|
+
{}
|
|
16
|
+
end
|
|
17
|
+
own_settings = @_settings&.dig(key) || {}
|
|
18
|
+
parent_settings.merge(own_settings)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.included(base)
|
|
23
|
+
base.extend(ClassMethods)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Operation
|
|
5
|
+
module TransactionAdapter
|
|
6
|
+
def self.for(adapter_name)
|
|
7
|
+
case adapter_name&.to_sym
|
|
8
|
+
when :active_record
|
|
9
|
+
ActiveRecordAdapter
|
|
10
|
+
when :mongoid
|
|
11
|
+
MongoidAdapter
|
|
12
|
+
when nil
|
|
13
|
+
detect
|
|
14
|
+
else
|
|
15
|
+
raise ArgumentError, "Unknown transaction adapter: #{adapter_name}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.detect
|
|
20
|
+
if defined?(ActiveRecord::Base)
|
|
21
|
+
ActiveRecordAdapter
|
|
22
|
+
elsif defined?(Mongoid)
|
|
23
|
+
MongoidAdapter
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module ActiveRecordAdapter
|
|
28
|
+
def self.wrap(&block)
|
|
29
|
+
unless defined?(ActiveRecord::Base)
|
|
30
|
+
raise LoadError, "ActiveRecord is required for transactions"
|
|
31
|
+
end
|
|
32
|
+
ActiveRecord::Base.transaction(&block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.rollback_exception_class
|
|
36
|
+
ActiveRecord::Rollback
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module MongoidAdapter
|
|
41
|
+
def self.wrap(&block)
|
|
42
|
+
unless defined?(Mongoid)
|
|
43
|
+
raise LoadError, "Mongoid is required for transactions"
|
|
44
|
+
end
|
|
45
|
+
Mongoid.transaction(&block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.rollback_exception_class
|
|
49
|
+
Mongoid::Errors::Rollback
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module TransactionWrapper
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def _transaction_wrap
|
|
10
|
+
return yield unless _transaction_enabled?
|
|
11
|
+
|
|
12
|
+
halted = nil
|
|
13
|
+
result = _transaction_execute do
|
|
14
|
+
halted_value = catch(:_dex_halt) { yield }
|
|
15
|
+
if halted_value.is_a?(Operation::Halt)
|
|
16
|
+
halted = halted_value
|
|
17
|
+
raise _transaction_adapter.rollback_exception_class if halted.error?
|
|
18
|
+
halted.value
|
|
19
|
+
else
|
|
20
|
+
halted_value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
throw(:_dex_halt, halted) if halted
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
TRANSACTION_KNOWN_ADAPTERS = %i[active_record mongoid].freeze
|
|
29
|
+
TRANSACTION_KNOWN_OPTIONS = %i[adapter].freeze
|
|
30
|
+
|
|
31
|
+
module ClassMethods
|
|
32
|
+
def transaction(enabled_or_options = nil, **options)
|
|
33
|
+
unknown = options.keys - TransactionWrapper::TRANSACTION_KNOWN_OPTIONS
|
|
34
|
+
if unknown.any?
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"unknown transaction option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
37
|
+
"Known: #{TransactionWrapper::TRANSACTION_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
case enabled_or_options
|
|
41
|
+
when false
|
|
42
|
+
set :transaction, enabled: false
|
|
43
|
+
when true, nil
|
|
44
|
+
_transaction_validate_adapter!(options[:adapter]) if options.key?(:adapter)
|
|
45
|
+
set :transaction, enabled: true, **options
|
|
46
|
+
when Symbol
|
|
47
|
+
_transaction_validate_adapter!(enabled_or_options)
|
|
48
|
+
set :transaction, enabled: true, adapter: enabled_or_options, **options
|
|
49
|
+
else
|
|
50
|
+
raise ArgumentError,
|
|
51
|
+
"transaction expects true, false, nil, or a Symbol adapter, got: #{enabled_or_options.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def _transaction_validate_adapter!(adapter)
|
|
58
|
+
return if adapter.nil?
|
|
59
|
+
|
|
60
|
+
unless TransactionWrapper::TRANSACTION_KNOWN_ADAPTERS.include?(adapter.to_sym)
|
|
61
|
+
raise ArgumentError,
|
|
62
|
+
"unknown transaction adapter: #{adapter.inspect}. " \
|
|
63
|
+
"Known: #{TransactionWrapper::TRANSACTION_KNOWN_ADAPTERS.map(&:inspect).join(", ")}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def _transaction_enabled?
|
|
71
|
+
settings = self.class.settings_for(:transaction)
|
|
72
|
+
return false unless settings.fetch(:enabled, true)
|
|
73
|
+
|
|
74
|
+
!_transaction_adapter.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def _transaction_adapter
|
|
78
|
+
settings = self.class.settings_for(:transaction)
|
|
79
|
+
adapter_name = settings.fetch(:adapter, Dex.transaction_adapter)
|
|
80
|
+
Operation::TransactionAdapter.for(adapter_name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def _transaction_execute(&block)
|
|
84
|
+
_transaction_adapter.wrap(&block)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Wrapper modules (loaded before class body so `include`/`use` can find them)
|
|
4
|
+
require_relative "operation/settings"
|
|
5
|
+
require_relative "operation/props_setup"
|
|
6
|
+
require_relative "operation/result_wrapper"
|
|
7
|
+
require_relative "operation/record_wrapper"
|
|
8
|
+
require_relative "operation/transaction_wrapper"
|
|
9
|
+
require_relative "operation/lock_wrapper"
|
|
10
|
+
require_relative "operation/async_wrapper"
|
|
11
|
+
require_relative "operation/safe_wrapper"
|
|
12
|
+
require_relative "operation/rescue_wrapper"
|
|
13
|
+
require_relative "operation/callback_wrapper"
|
|
14
|
+
|
|
15
|
+
# Pipeline (referenced inside class body)
|
|
16
|
+
require_relative "operation/pipeline"
|
|
17
|
+
|
|
18
|
+
module Dex
|
|
19
|
+
class Operation
|
|
20
|
+
Halt = Struct.new(:type, :value, :error_code, :error_message, :error_details, keyword_init: true) do
|
|
21
|
+
def success? = type == :success
|
|
22
|
+
def error? = type == :error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self._serialized_coercions
|
|
26
|
+
@_serialized_coercions ||= {
|
|
27
|
+
Time => ->(v) { v.is_a?(String) ? Time.parse(v) : v },
|
|
28
|
+
Symbol => ->(v) { v.is_a?(String) ? v.to_sym : v }
|
|
29
|
+
}.tap do |h|
|
|
30
|
+
h[Date] = ->(v) { v.is_a?(String) ? Date.parse(v) : v } if defined?(Date)
|
|
31
|
+
h[DateTime] = ->(v) { v.is_a?(String) ? DateTime.parse(v) : v } if defined?(DateTime)
|
|
32
|
+
h[BigDecimal] = ->(v) { v.is_a?(String) ? BigDecimal(v) : v } if defined?(BigDecimal)
|
|
33
|
+
end.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Contract = Data.define(:params, :success, :errors)
|
|
37
|
+
|
|
38
|
+
def self.contract
|
|
39
|
+
Contract.new(
|
|
40
|
+
params: _contract_params,
|
|
41
|
+
success: _success_type,
|
|
42
|
+
errors: _declared_errors
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self._contract_params
|
|
47
|
+
return {} unless respond_to?(:literal_properties)
|
|
48
|
+
|
|
49
|
+
literal_properties.each_with_object({}) do |prop, hash|
|
|
50
|
+
hash[prop.name] = prop.type
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private_class_method :_contract_params
|
|
55
|
+
|
|
56
|
+
def self._dex_find_ref_type(type)
|
|
57
|
+
return type if type.is_a?(Dex::RefType)
|
|
58
|
+
|
|
59
|
+
if type.respond_to?(:type)
|
|
60
|
+
inner = type.type
|
|
61
|
+
return inner if inner.is_a?(Dex::RefType)
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self._dex_coerce_serialized_hash(hash)
|
|
67
|
+
return hash.transform_keys(&:to_sym) unless respond_to?(:literal_properties)
|
|
68
|
+
|
|
69
|
+
result = {}
|
|
70
|
+
literal_properties.each do |prop|
|
|
71
|
+
name = prop.name
|
|
72
|
+
raw = hash.key?(name) ? hash[name] : hash[name.to_s]
|
|
73
|
+
result[name] = _dex_coerce_value(prop.type, raw)
|
|
74
|
+
end
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self._dex_resolve_base_class(type)
|
|
79
|
+
return type.model_class if type.is_a?(Dex::RefType)
|
|
80
|
+
return _dex_resolve_base_class(type.type) if type.respond_to?(:type)
|
|
81
|
+
|
|
82
|
+
type if type.is_a?(Class)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self._dex_coerce_value(type, value)
|
|
86
|
+
return value unless value
|
|
87
|
+
return value if _dex_find_ref_type(type)
|
|
88
|
+
|
|
89
|
+
if type.respond_to?(:type) && type.is_a?(Literal::Types::ArrayType)
|
|
90
|
+
return value.map { |v| _dex_coerce_value(type.type, v) } if value.is_a?(Array)
|
|
91
|
+
return value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if type.respond_to?(:type) && type.is_a?(Literal::Types::NilableType)
|
|
95
|
+
return _dex_coerce_value(type.type, value)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
base = _dex_resolve_base_class(type)
|
|
99
|
+
coercion = _serialized_coercions[base]
|
|
100
|
+
coercion ? coercion.call(value) : value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private_class_method :_dex_resolve_base_class, :_dex_coerce_value, :_dex_find_ref_type, :_dex_coerce_serialized_hash
|
|
104
|
+
|
|
105
|
+
def self.inherited(subclass)
|
|
106
|
+
subclass.instance_variable_set(:@_pipeline, pipeline.dup)
|
|
107
|
+
super
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.pipeline
|
|
111
|
+
@_pipeline ||= Pipeline.new
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.use(mod, as: nil, wrap: nil, before: nil, after: nil, at: nil)
|
|
115
|
+
step_name = as || _derive_step_name(mod)
|
|
116
|
+
wrap_method = wrap || :"_#{step_name}_wrap"
|
|
117
|
+
pipeline.add(step_name, method: wrap_method, before: before, after: after, at: at)
|
|
118
|
+
include mod
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self._derive_step_name(mod)
|
|
122
|
+
base = mod.name&.split("::")&.last
|
|
123
|
+
raise ArgumentError, "anonymous modules require explicit as: parameter" unless base
|
|
124
|
+
|
|
125
|
+
base.sub(/Wrapper\z/, "")
|
|
126
|
+
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
|
127
|
+
.downcase
|
|
128
|
+
.to_sym
|
|
129
|
+
end
|
|
130
|
+
private_class_method :_derive_step_name
|
|
131
|
+
|
|
132
|
+
def perform(*, **)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def call
|
|
136
|
+
self.class.pipeline.execute(self) { perform }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.method_added(method_name)
|
|
140
|
+
super
|
|
141
|
+
return unless method_name == :perform
|
|
142
|
+
|
|
143
|
+
private :perform
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private :perform
|
|
147
|
+
|
|
148
|
+
def self.call(**kwargs)
|
|
149
|
+
new(**kwargs).call
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Serialization helpers
|
|
153
|
+
|
|
154
|
+
def _props_as_json
|
|
155
|
+
return {} unless self.class.respond_to?(:literal_properties)
|
|
156
|
+
|
|
157
|
+
result = {}
|
|
158
|
+
self.class.literal_properties.each do |prop|
|
|
159
|
+
value = public_send(prop.name)
|
|
160
|
+
ref = self.class.send(:_dex_find_ref_type, prop.type)
|
|
161
|
+
result[prop.name.to_s] = if ref && value
|
|
162
|
+
value.id
|
|
163
|
+
else
|
|
164
|
+
value.respond_to?(:as_json) ? value.as_json : value
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
result
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
include Settings
|
|
171
|
+
include AsyncWrapper
|
|
172
|
+
include SafeWrapper
|
|
173
|
+
include PropsSetup
|
|
174
|
+
|
|
175
|
+
use ResultWrapper
|
|
176
|
+
use LockWrapper
|
|
177
|
+
use TransactionWrapper
|
|
178
|
+
use RecordWrapper
|
|
179
|
+
use RescueWrapper
|
|
180
|
+
use CallbackWrapper
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Nested classes (reopen Operation after it's defined)
|
|
185
|
+
require_relative "operation/outcome"
|
|
186
|
+
require_relative "operation/async_proxy"
|
|
187
|
+
require_relative "operation/record_backend"
|
|
188
|
+
require_relative "operation/transaction_adapter"
|
|
189
|
+
require_relative "operation/jobs"
|
|
190
|
+
|
|
191
|
+
# Top-level aliases (depend on Operation::Ok/Err)
|
|
192
|
+
require_relative "match"
|
data/lib/dex/ref_type.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class RefType
|
|
5
|
+
include Literal::Type
|
|
6
|
+
|
|
7
|
+
attr_reader :model_class, :lock
|
|
8
|
+
|
|
9
|
+
def initialize(model_class, lock: false)
|
|
10
|
+
@model_class = model_class
|
|
11
|
+
@lock = lock
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ===(value)
|
|
15
|
+
value.is_a?(@model_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def >=(other, context: nil)
|
|
19
|
+
other.is_a?(RefType) && other.model_class <= @model_class
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def coerce(value)
|
|
23
|
+
return value if value.is_a?(@model_class)
|
|
24
|
+
return value if value.nil?
|
|
25
|
+
|
|
26
|
+
scope = @lock ? @model_class.lock : @model_class
|
|
27
|
+
scope.find(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inspect
|
|
31
|
+
@lock ? "_Ref(#{@model_class}, lock: true)" : "_Ref(#{@model_class})"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|