dexkit 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +116 -3
- data/guides/llm/EVENT.md +300 -0
- data/guides/llm/FORM.md +520 -0
- data/guides/llm/OPERATION.md +1 -1
- data/lib/dex/concern.rb +10 -0
- data/lib/dex/event/bus.rb +98 -0
- data/lib/dex/event/execution_state.rb +17 -0
- data/lib/dex/event/handler.rb +77 -0
- data/lib/dex/event/metadata.rb +54 -0
- data/lib/dex/event/processor.rb +61 -0
- data/lib/dex/event/suppression.rb +49 -0
- data/lib/dex/event/trace.rb +56 -0
- data/lib/dex/event.rb +87 -0
- data/lib/dex/event_test_helpers/assertions.rb +70 -0
- data/lib/dex/event_test_helpers.rb +88 -0
- data/lib/dex/form/nesting.rb +189 -0
- data/lib/dex/form/uniqueness_validator.rb +86 -0
- data/lib/dex/form.rb +142 -0
- data/lib/dex/operation/async_proxy.rb +30 -36
- data/lib/dex/operation/async_wrapper.rb +3 -19
- data/lib/dex/operation/callback_wrapper.rb +11 -15
- data/lib/dex/operation/jobs.rb +8 -14
- data/lib/dex/operation/lock_wrapper.rb +2 -11
- data/lib/dex/operation/pipeline.rb +5 -5
- data/lib/dex/operation/record_wrapper.rb +10 -38
- data/lib/dex/operation/rescue_wrapper.rb +1 -3
- data/lib/dex/operation/result_wrapper.rb +7 -14
- data/lib/dex/operation/settings.rb +10 -3
- data/lib/dex/operation/transaction_wrapper.rb +7 -20
- data/lib/dex/operation.rb +57 -105
- data/lib/dex/{operation/props_setup.rb → props_setup.rb} +12 -15
- data/lib/dex/test_helpers.rb +3 -1
- data/lib/dex/type_coercion.rb +96 -0
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +15 -1
- metadata +49 -4
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Form
|
|
5
|
+
module Nesting
|
|
6
|
+
extend Dex::Concern
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def _nested_ones
|
|
10
|
+
@_nested_ones ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def _nested_manys
|
|
14
|
+
@_nested_manys ||= {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def nested_one(name, class_name: nil, &block)
|
|
18
|
+
raise ArgumentError, "nested_one requires a block" unless block
|
|
19
|
+
|
|
20
|
+
name = name.to_sym
|
|
21
|
+
nested_class = _build_nested_class(name, class_name, &block)
|
|
22
|
+
_nested_ones[name] = nested_class
|
|
23
|
+
|
|
24
|
+
attr_reader name
|
|
25
|
+
|
|
26
|
+
define_method(:"#{name}=") do |value|
|
|
27
|
+
coerced = _coerce_nested_one(name, value)
|
|
28
|
+
instance_variable_set(:"@#{name}", coerced)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
define_method(:"build_#{name}") do |attrs = {}|
|
|
32
|
+
instance = self.class._nested_ones[name].new(attrs)
|
|
33
|
+
send(:"#{name}=", instance)
|
|
34
|
+
instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_method(:"#{name}_attributes=") do |attrs|
|
|
38
|
+
send(:"#{name}=", attrs)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def nested_many(name, class_name: nil, &block)
|
|
43
|
+
raise ArgumentError, "nested_many requires a block" unless block
|
|
44
|
+
|
|
45
|
+
name = name.to_sym
|
|
46
|
+
nested_class = _build_nested_class(name, class_name, &block)
|
|
47
|
+
_nested_manys[name] = nested_class
|
|
48
|
+
|
|
49
|
+
attr_reader name
|
|
50
|
+
|
|
51
|
+
define_method(:"#{name}=") do |value|
|
|
52
|
+
coerced = _coerce_nested_many(name, value)
|
|
53
|
+
instance_variable_set(:"@#{name}", coerced)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
define_method(:"build_#{name.to_s.singularize}") do |attrs = {}|
|
|
57
|
+
instance = self.class._nested_manys[name].new(attrs)
|
|
58
|
+
items = send(name) || []
|
|
59
|
+
items << instance
|
|
60
|
+
instance_variable_set(:"@#{name}", items)
|
|
61
|
+
instance
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
define_method(:"#{name}_attributes=") do |attrs|
|
|
65
|
+
send(:"#{name}=", attrs)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def _build_nested_class(name, class_name, &block)
|
|
72
|
+
klass = Class.new(Dex::Form, &block)
|
|
73
|
+
const_name = class_name || name.to_s.singularize.camelize
|
|
74
|
+
const_set(const_name, klass)
|
|
75
|
+
klass
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def _coerce_nested_one(name, value)
|
|
82
|
+
klass = self.class._nested_ones[name]
|
|
83
|
+
value = _unwrap_hash_like(value)
|
|
84
|
+
case value
|
|
85
|
+
when Hash
|
|
86
|
+
return nil if _marked_for_destroy?(value)
|
|
87
|
+
klass.new(value.except("_destroy", :_destroy))
|
|
88
|
+
when klass then value
|
|
89
|
+
when nil then value
|
|
90
|
+
else raise ArgumentError, "#{name} must be a Hash or #{klass}, got #{value.class}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def _coerce_nested_many(name, value)
|
|
95
|
+
klass = self.class._nested_manys[name]
|
|
96
|
+
value = _unwrap_hash_like(value)
|
|
97
|
+
items = case value
|
|
98
|
+
when Array then value
|
|
99
|
+
when Hash then _normalize_nested_hash(value)
|
|
100
|
+
else raise ArgumentError, "#{name} must be an Array or Hash, got #{value.class}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
items.filter_map do |item|
|
|
104
|
+
item = _unwrap_hash_like(item)
|
|
105
|
+
case item
|
|
106
|
+
when Hash
|
|
107
|
+
next nil if _marked_for_destroy?(item)
|
|
108
|
+
klass.new(item.except("_destroy", :_destroy))
|
|
109
|
+
when klass then item
|
|
110
|
+
else raise ArgumentError, "each #{name} item must be a Hash or #{klass}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def _unwrap_hash_like(value)
|
|
116
|
+
return value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
|
|
117
|
+
return value if value.is_a?(Hash) || value.is_a?(Array) || value.is_a?(Dex::Form) || value.nil?
|
|
118
|
+
|
|
119
|
+
value.respond_to?(:to_h) ? value.to_h : value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def _normalize_nested_hash(hash)
|
|
123
|
+
hash.sort_by { |k, _| k.to_s.to_i }.map(&:last)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def _marked_for_destroy?(attrs)
|
|
127
|
+
destroy_val = attrs["_destroy"] || attrs[:_destroy]
|
|
128
|
+
ActiveModel::Type::Boolean.new.cast(destroy_val)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def _initialize_nested_defaults(provided_keys)
|
|
132
|
+
self.class._nested_ones.each_key do |name|
|
|
133
|
+
key = name.to_s
|
|
134
|
+
next if provided_keys.include?(key) || provided_keys.include?("#{key}_attributes")
|
|
135
|
+
|
|
136
|
+
send(:"#{name}=", {})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
self.class._nested_manys.each_key do |name|
|
|
140
|
+
next if instance_variable_get(:"@#{name}")
|
|
141
|
+
|
|
142
|
+
instance_variable_set(:"@#{name}", [])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def _validate_nested(context)
|
|
147
|
+
valid = true
|
|
148
|
+
|
|
149
|
+
self.class._nested_ones.each_key do |name|
|
|
150
|
+
nested = send(name)
|
|
151
|
+
next unless nested
|
|
152
|
+
|
|
153
|
+
unless nested.valid?(context)
|
|
154
|
+
nested.errors.each do |error|
|
|
155
|
+
errors.add(:"#{name}.#{error.attribute}", error.message)
|
|
156
|
+
end
|
|
157
|
+
valid = false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
self.class._nested_manys.each_key do |name|
|
|
162
|
+
items = send(name) || []
|
|
163
|
+
items.each_with_index do |item, index|
|
|
164
|
+
next if item.valid?(context)
|
|
165
|
+
|
|
166
|
+
item.errors.each do |error|
|
|
167
|
+
errors.add(:"#{name}[#{index}].#{error.attribute}", error.message)
|
|
168
|
+
end
|
|
169
|
+
valid = false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
valid
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def _nested_to_h(result)
|
|
177
|
+
self.class._nested_ones.each_key do |name|
|
|
178
|
+
nested = send(name)
|
|
179
|
+
result[name] = nested&.to_h
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
self.class._nested_manys.each_key do |name|
|
|
183
|
+
items = send(name) || []
|
|
184
|
+
result[name] = items.map(&:to_h)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Form
|
|
5
|
+
class UniquenessValidator < ActiveModel::EachValidator
|
|
6
|
+
def check_validity!
|
|
7
|
+
if options.key?(:model) && !options[:model].is_a?(Class)
|
|
8
|
+
raise ArgumentError, "uniqueness :model must be a Class, got #{options[:model].inspect}"
|
|
9
|
+
end
|
|
10
|
+
if options.key?(:conditions) && !options[:conditions].respond_to?(:call)
|
|
11
|
+
raise ArgumentError, "uniqueness :conditions must be callable"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate_each(form, attribute, value)
|
|
16
|
+
return if value.blank?
|
|
17
|
+
|
|
18
|
+
model_class = _resolve_model_class(form)
|
|
19
|
+
return unless model_class
|
|
20
|
+
|
|
21
|
+
column = options[:attribute] || attribute
|
|
22
|
+
query = _build_query(model_class, column, value)
|
|
23
|
+
query = _apply_scope(query, form)
|
|
24
|
+
query = _apply_conditions(query, form)
|
|
25
|
+
query = _exclude_current_record(query, form)
|
|
26
|
+
|
|
27
|
+
form.errors.add(attribute, options[:message] || :taken) if query.exists?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def _resolve_model_class(form)
|
|
33
|
+
return options[:model] if options[:model]
|
|
34
|
+
return form.class._model_class if form.class.respond_to?(:_model_class) && form.class._model_class
|
|
35
|
+
|
|
36
|
+
_infer_model_class(form)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def _infer_model_class(form)
|
|
40
|
+
class_name = form.class.name
|
|
41
|
+
return unless class_name
|
|
42
|
+
|
|
43
|
+
model_name = class_name.sub(/Form\z/, "")
|
|
44
|
+
return if model_name == class_name
|
|
45
|
+
|
|
46
|
+
klass = Object.const_get(model_name)
|
|
47
|
+
klass.respond_to?(:where) ? klass : nil
|
|
48
|
+
rescue NameError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def _build_query(model_class, column, value)
|
|
53
|
+
if options[:case_sensitive] == false && value.is_a?(String) && model_class.respond_to?(:arel_table)
|
|
54
|
+
model_class.where(model_class.arel_table[column].lower.eq(value.downcase))
|
|
55
|
+
else
|
|
56
|
+
model_class.where(column => value)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def _apply_scope(query, form)
|
|
61
|
+
Array(options[:scope]).each do |scope_attr|
|
|
62
|
+
query = query.where(scope_attr => form.public_send(scope_attr))
|
|
63
|
+
end
|
|
64
|
+
query
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _apply_conditions(query, form)
|
|
68
|
+
return query unless options[:conditions]
|
|
69
|
+
|
|
70
|
+
callable = options[:conditions]
|
|
71
|
+
if callable.arity.zero?
|
|
72
|
+
query.instance_exec(&callable)
|
|
73
|
+
else
|
|
74
|
+
query.instance_exec(form, &callable)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def _exclude_current_record(query, form)
|
|
79
|
+
return query unless form.record&.persisted?
|
|
80
|
+
|
|
81
|
+
pk = form.record.class.primary_key
|
|
82
|
+
query.where.not(pk => form.record.public_send(pk))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/dex/form.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
require_relative "form/nesting"
|
|
6
|
+
|
|
7
|
+
module Dex
|
|
8
|
+
class Form
|
|
9
|
+
include ActiveModel::Model
|
|
10
|
+
include ActiveModel::Attributes
|
|
11
|
+
include ActiveModel::Validations::Callbacks
|
|
12
|
+
|
|
13
|
+
if defined?(ActiveModel::Attributes::Normalization)
|
|
14
|
+
include ActiveModel::Attributes::Normalization
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
include Nesting
|
|
18
|
+
include Match
|
|
19
|
+
|
|
20
|
+
class ValidationError < StandardError
|
|
21
|
+
attr_reader :form
|
|
22
|
+
|
|
23
|
+
def initialize(form)
|
|
24
|
+
@form = form
|
|
25
|
+
super("Validation failed: #{form.errors.full_messages.join(", ")}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def model(klass = nil)
|
|
31
|
+
if klass
|
|
32
|
+
raise ArgumentError, "model must be a Class, got #{klass.inspect}" unless klass.is_a?(Class)
|
|
33
|
+
@_model_class = klass
|
|
34
|
+
end
|
|
35
|
+
_model_class
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _model_class
|
|
39
|
+
return @_model_class if defined?(@_model_class)
|
|
40
|
+
superclass._model_class if superclass.respond_to?(:_model_class)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inherited(subclass)
|
|
44
|
+
super
|
|
45
|
+
subclass.instance_variable_set(:@_nested_ones, _nested_ones.dup)
|
|
46
|
+
subclass.instance_variable_set(:@_nested_manys, _nested_manys.dup)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
silence_redefinition_of_method :model_name
|
|
51
|
+
def self.model_name
|
|
52
|
+
if _model_class
|
|
53
|
+
_model_class.model_name
|
|
54
|
+
elsif name && !name.start_with?("#")
|
|
55
|
+
super
|
|
56
|
+
else
|
|
57
|
+
@_model_name ||= ActiveModel::Name.new(self, nil, name&.split("::")&.last || "Form")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
attr_reader :record
|
|
62
|
+
|
|
63
|
+
def initialize(attributes = {})
|
|
64
|
+
# Accept ActionController::Parameters without requiring .permit — the form's
|
|
65
|
+
# attribute declarations are the whitelist. Only declared attributes and nested
|
|
66
|
+
# setters are assignable; everything else is silently dropped.
|
|
67
|
+
attributes = attributes.to_unsafe_h if attributes.respond_to?(:to_unsafe_h)
|
|
68
|
+
attrs = (attributes || {}).transform_keys(&:to_s)
|
|
69
|
+
record = attrs.delete("record")
|
|
70
|
+
@record = record if record.nil? || record.respond_to?(:persisted?)
|
|
71
|
+
provided_keys = attrs.keys
|
|
72
|
+
nested_attrs = _extract_nested_attributes(attrs)
|
|
73
|
+
super(attrs.slice(*self.class.attribute_names))
|
|
74
|
+
_apply_nested_attributes(nested_attrs)
|
|
75
|
+
_initialize_nested_defaults(provided_keys)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_record(record)
|
|
79
|
+
raise ArgumentError, "record must respond to #persisted?, got #{record.inspect}" unless record.respond_to?(:persisted?)
|
|
80
|
+
|
|
81
|
+
@record = record
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def persisted?
|
|
86
|
+
record&.persisted? || false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_key
|
|
90
|
+
record&.to_key
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_param
|
|
94
|
+
record&.to_param
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def valid?(context = nil)
|
|
98
|
+
super_result = super
|
|
99
|
+
nested_result = _validate_nested(context)
|
|
100
|
+
super_result && nested_result
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_h
|
|
104
|
+
result = {}
|
|
105
|
+
self.class.attribute_names.each do |name|
|
|
106
|
+
result[name.to_sym] = public_send(name)
|
|
107
|
+
end
|
|
108
|
+
_nested_to_h(result)
|
|
109
|
+
result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
alias_method :to_hash, :to_h
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def _extract_nested_attributes(attrs)
|
|
117
|
+
nested_keys = self.class._nested_ones.keys.map(&:to_s) +
|
|
118
|
+
self.class._nested_manys.keys.map(&:to_s)
|
|
119
|
+
|
|
120
|
+
extracted = {}
|
|
121
|
+
nested_keys.each do |key|
|
|
122
|
+
attr_key = "#{key}_attributes"
|
|
123
|
+
if attrs.key?(attr_key)
|
|
124
|
+
extracted[attr_key] = attrs.delete(attr_key)
|
|
125
|
+
attrs.delete(key)
|
|
126
|
+
elsif attrs.key?(key)
|
|
127
|
+
extracted[key] = attrs.delete(key)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
extracted
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def _apply_nested_attributes(nested_attrs)
|
|
134
|
+
nested_attrs.each do |key, value|
|
|
135
|
+
next if value.nil?
|
|
136
|
+
send(:"#{key}=", value)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
require_relative "form/uniqueness_validator"
|
|
@@ -9,41 +9,41 @@ module Dex
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
|
-
|
|
13
|
-
if
|
|
14
|
-
|
|
12
|
+
ensure_active_job_loaded!
|
|
13
|
+
if use_record_strategy?
|
|
14
|
+
enqueue_record_job
|
|
15
15
|
else
|
|
16
|
-
|
|
16
|
+
enqueue_direct_job
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
job =
|
|
24
|
-
job.perform_later(class_name:
|
|
22
|
+
def enqueue_direct_job
|
|
23
|
+
job = apply_options(Operation::DirectJob)
|
|
24
|
+
job.perform_later(class_name: operation_class_name, params: serialized_params)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def enqueue_record_job
|
|
28
28
|
record = Dex.record_backend.create_record(
|
|
29
|
-
name:
|
|
30
|
-
params:
|
|
29
|
+
name: operation_class_name,
|
|
30
|
+
params: serialized_params,
|
|
31
31
|
status: "pending"
|
|
32
32
|
)
|
|
33
33
|
begin
|
|
34
|
-
job =
|
|
35
|
-
job.perform_later(class_name:
|
|
34
|
+
job = apply_options(Operation::RecordJob)
|
|
35
|
+
job.perform_later(class_name: operation_class_name, record_id: record.id)
|
|
36
36
|
rescue => e
|
|
37
37
|
begin
|
|
38
38
|
record.destroy
|
|
39
39
|
rescue => destroy_error
|
|
40
|
-
|
|
40
|
+
Dex.warn("Failed to clean up pending record #{record.id}: #{destroy_error.message}")
|
|
41
41
|
end
|
|
42
42
|
raise e
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def use_record_strategy?
|
|
47
47
|
return false unless Dex.record_backend
|
|
48
48
|
return false unless @operation.class.name
|
|
49
49
|
|
|
@@ -54,54 +54,48 @@ module Dex
|
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def apply_options(job_class)
|
|
58
58
|
options = {}
|
|
59
|
-
options[:queue] =
|
|
60
|
-
options[:wait_until] =
|
|
61
|
-
options[:wait] =
|
|
59
|
+
options[:queue] = queue if queue
|
|
60
|
+
options[:wait_until] = scheduled_at if scheduled_at
|
|
61
|
+
options[:wait] = scheduled_in if scheduled_in
|
|
62
62
|
options.empty? ? job_class : job_class.set(**options)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def ensure_active_job_loaded!
|
|
66
66
|
return if defined?(ActiveJob::Base)
|
|
67
67
|
|
|
68
68
|
raise LoadError, "ActiveJob is required for async operations. Add 'activejob' to your Gemfile."
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def merged_options
|
|
72
72
|
@operation.class.settings_for(:async).merge(@runtime_options)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
def
|
|
77
|
-
def
|
|
78
|
-
def
|
|
75
|
+
def queue = merged_options[:queue]
|
|
76
|
+
def scheduled_at = merged_options[:at]
|
|
77
|
+
def scheduled_in = merged_options[:in]
|
|
78
|
+
def operation_class_name = @operation.class.name
|
|
79
79
|
|
|
80
|
-
def
|
|
81
|
-
@
|
|
80
|
+
def serialized_params
|
|
81
|
+
@serialized_params ||= begin
|
|
82
82
|
hash = @operation._props_as_json
|
|
83
|
-
|
|
83
|
+
validate_serializable!(hash)
|
|
84
84
|
hash
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def
|
|
89
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
90
|
-
Rails.logger.warn "[Dex] #{message}"
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def _async_validate_serializable!(hash, path: "")
|
|
88
|
+
def validate_serializable!(hash, path: "")
|
|
95
89
|
hash.each do |key, value|
|
|
96
90
|
current = path.empty? ? key.to_s : "#{path}.#{key}"
|
|
97
91
|
case value
|
|
98
92
|
when String, Integer, Float, NilClass, TrueClass, FalseClass
|
|
99
93
|
next
|
|
100
94
|
when Hash
|
|
101
|
-
|
|
95
|
+
validate_serializable!(value, path: current)
|
|
102
96
|
when Array
|
|
103
97
|
value.each_with_index do |v, i|
|
|
104
|
-
|
|
98
|
+
validate_serializable!({ i => v }, path: current)
|
|
105
99
|
end
|
|
106
100
|
else
|
|
107
101
|
raise ArgumentError,
|
|
@@ -2,33 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Dex
|
|
4
4
|
module AsyncWrapper
|
|
5
|
-
ASYNC_KNOWN_OPTIONS = %i[queue in at].freeze
|
|
6
|
-
|
|
7
5
|
module ClassMethods
|
|
8
6
|
def async(**options)
|
|
9
|
-
|
|
10
|
-
if unknown.any?
|
|
11
|
-
raise ArgumentError,
|
|
12
|
-
"unknown async option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
13
|
-
"Known: #{AsyncWrapper::ASYNC_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
14
|
-
end
|
|
15
|
-
|
|
7
|
+
validate_options!(options, %i[queue in at], :async)
|
|
16
8
|
set(:async, **options)
|
|
17
9
|
end
|
|
18
10
|
end
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
base.extend(ClassMethods)
|
|
22
|
-
end
|
|
12
|
+
extend Dex::Concern
|
|
23
13
|
|
|
24
14
|
def async(**options)
|
|
25
|
-
|
|
26
|
-
if unknown.any?
|
|
27
|
-
raise ArgumentError,
|
|
28
|
-
"unknown async option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
29
|
-
"Known: #{AsyncWrapper::ASYNC_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
30
|
-
end
|
|
31
|
-
|
|
15
|
+
self.class.validate_options!(options, %i[queue in at], :async)
|
|
32
16
|
Operation::AsyncProxy.new(self, **options)
|
|
33
17
|
end
|
|
34
18
|
end
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Dex
|
|
4
4
|
module CallbackWrapper
|
|
5
|
-
|
|
6
|
-
base.extend(ClassMethods)
|
|
7
|
-
end
|
|
5
|
+
extend Dex::Concern
|
|
8
6
|
|
|
9
7
|
module ClassMethods
|
|
10
8
|
def before(callable = nil, &block)
|
|
@@ -57,24 +55,22 @@ module Dex
|
|
|
57
55
|
def _callback_wrap
|
|
58
56
|
return yield unless self.class._callback_any?
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
success_halt = nil
|
|
61
59
|
result = _callback_run_around(self.class._callback_list(:around)) do
|
|
62
60
|
_callback_run_before
|
|
63
|
-
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw(:_dex_halt, caught)
|
|
71
|
-
end
|
|
61
|
+
interceptor = Operation::HaltInterceptor.new { yield }
|
|
62
|
+
if interceptor.error?
|
|
63
|
+
interceptor.rethrow!
|
|
64
|
+
elsif interceptor.halted?
|
|
65
|
+
success_halt = interceptor
|
|
66
|
+
_callback_run_after
|
|
67
|
+
interceptor.result
|
|
72
68
|
else
|
|
73
69
|
_callback_run_after
|
|
74
|
-
|
|
70
|
+
interceptor.result
|
|
75
71
|
end
|
|
76
72
|
end
|
|
77
|
-
|
|
73
|
+
success_halt&.rethrow!
|
|
78
74
|
result
|
|
79
75
|
end
|
|
80
76
|
|
data/lib/dex/operation/jobs.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Dex
|
|
|
11
11
|
const_set(:DirectJob, Class.new(ActiveJob::Base) do
|
|
12
12
|
def perform(class_name:, params:)
|
|
13
13
|
klass = class_name.constantize
|
|
14
|
-
klass.new(**klass.send(:
|
|
14
|
+
klass.new(**klass.send(:_coerce_serialized_hash, params)).call
|
|
15
15
|
end
|
|
16
16
|
end)
|
|
17
17
|
when :RecordJob
|
|
@@ -19,39 +19,33 @@ module Dex
|
|
|
19
19
|
def perform(class_name:, record_id:)
|
|
20
20
|
klass = class_name.constantize
|
|
21
21
|
record = Dex.record_backend.find_record(record_id)
|
|
22
|
-
params = klass.send(:
|
|
22
|
+
params = klass.send(:_coerce_serialized_hash, record.params || {})
|
|
23
23
|
|
|
24
24
|
op = klass.new(**params)
|
|
25
25
|
op.instance_variable_set(:@_dex_record_id, record_id)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
update_status(record_id, status: "running")
|
|
28
28
|
op.call
|
|
29
29
|
rescue => e
|
|
30
|
-
|
|
30
|
+
handle_failure(record_id, e)
|
|
31
31
|
raise
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def update_status(record_id, **attributes)
|
|
37
37
|
Dex.record_backend.update_record(record_id, attributes)
|
|
38
38
|
rescue => e
|
|
39
|
-
|
|
39
|
+
Dex.warn("Failed to update record status: #{e.message}")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def
|
|
42
|
+
def handle_failure(record_id, exception)
|
|
43
43
|
error_value = if exception.is_a?(Dex::Error)
|
|
44
44
|
exception.code.to_s
|
|
45
45
|
else
|
|
46
46
|
exception.class.name
|
|
47
47
|
end
|
|
48
|
-
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def _dex_log_warning(message)
|
|
52
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
53
|
-
Rails.logger.warn "[Dex] #{message}"
|
|
54
|
-
end
|
|
48
|
+
update_status(record_id, status: "failed", error: error_value)
|
|
55
49
|
end
|
|
56
50
|
end)
|
|
57
51
|
when :Job
|