thunderoperation 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/config/default.yml +11 -0
- data/lib/rubocop/cop/thunderoperation/call_handlers.rb +56 -0
- data/lib/rubocop/cop/thunderoperation/def_outside_steps.rb +39 -0
- data/lib/rubocop/cop/thunderoperation/flow_required.rb +24 -0
- data/lib/rubocop/cop/thunderoperation/missing_steps.rb +42 -0
- data/lib/rubocop/cop/thunderoperation/operation_helpers.rb +124 -0
- data/lib/rubocop/cop/thunderoperation/operation_section_order.rb +51 -0
- data/lib/rubocop/cop/thunderoperation/steps_cannot_call_steps.rb +36 -0
- data/lib/rubocop/cop/thunderoperation/steps_order.rb +52 -0
- data/lib/rubocop/cop/thunderoperation/unknown_step.rb +61 -0
- data/lib/rubocop/cop/thunderoperation/unused_step.rb +29 -0
- data/lib/thunderoperation/adapter.rb +23 -0
- data/lib/thunderoperation/adapters/framer.rb +64 -0
- data/lib/thunderoperation/adapters/interruptable.rb +19 -0
- data/lib/thunderoperation/adapters/rich_parameters.rb +42 -0
- data/lib/thunderoperation/adapters/validations.rb +49 -0
- data/lib/thunderoperation/base.rb +144 -0
- data/lib/thunderoperation/clauses/base.rb +11 -0
- data/lib/thunderoperation/clauses/transaction.rb +11 -0
- data/lib/thunderoperation/clauses.rb +11 -0
- data/lib/thunderoperation/concerns/has_adapter.rb +47 -0
- data/lib/thunderoperation/config.rb +30 -0
- data/lib/thunderoperation/exceptions/trail.rb +27 -0
- data/lib/thunderoperation/exceptions.rb +32 -0
- data/lib/thunderoperation/flow/builder.rb +31 -0
- data/lib/thunderoperation/flow.rb +57 -0
- data/lib/thunderoperation/handlers.rb +43 -0
- data/lib/thunderoperation/lookup.rb +13 -0
- data/lib/thunderoperation/parameters/array_parameter.rb +48 -0
- data/lib/thunderoperation/parameters/base.rb +92 -0
- data/lib/thunderoperation/parameters/hash_parameter.rb +154 -0
- data/lib/thunderoperation/parameters.rb +11 -0
- data/lib/thunderoperation/response.rb +62 -0
- data/lib/thunderoperation/rubocop.rb +31 -0
- data/lib/thunderoperation/steps/base.rb +44 -0
- data/lib/thunderoperation/steps/clause.rb +32 -0
- data/lib/thunderoperation/steps/method.rb +34 -0
- data/lib/thunderoperation/steps/render.rb +22 -0
- data/lib/thunderoperation/steps/success.rb +29 -0
- data/lib/thunderoperation/steps.rb +33 -0
- data/lib/thunderoperation/steps_definition.rb +75 -0
- data/lib/thunderoperation/version.rb +5 -0
- data/lib/thunderoperation.rb +36 -0
- metadata +108 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Adapters
|
|
5
|
+
class RichParameters < Adapter
|
|
6
|
+
def execute(_params, operation_instance, _options, &block)
|
|
7
|
+
operation_instance._load_params
|
|
8
|
+
|
|
9
|
+
block.call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
adapted do
|
|
13
|
+
def self.param(*, &)
|
|
14
|
+
@param_ivars = nil
|
|
15
|
+
base_parameter.param(*, &)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.base_parameter
|
|
19
|
+
@base_parameter ||= Parameters::HashParameter.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.param_ivars
|
|
23
|
+
@param_ivars ||= base_parameter.output_keys.to_h { |key| [key, :"@#{key}"] }.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _reportable_params
|
|
27
|
+
return unless defined?(@sanitized_params) && @sanitized_params
|
|
28
|
+
|
|
29
|
+
self.class.base_parameter.filter(@sanitized_params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def _load_params
|
|
33
|
+
@sanitized_params = self.class.base_parameter.sanitize(@params || {})
|
|
34
|
+
|
|
35
|
+
self.class.param_ivars.each do |key, ivar|
|
|
36
|
+
instance_variable_set(ivar, @sanitized_params[key]) if @sanitized_params.key?(key)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Adapters
|
|
5
|
+
class Validations < Adapter
|
|
6
|
+
def execute(_params, _operation_instance, _options, &block)
|
|
7
|
+
validate_steps!
|
|
8
|
+
|
|
9
|
+
response = block.call
|
|
10
|
+
validate_return!(response) if check_return?(response)
|
|
11
|
+
|
|
12
|
+
response
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def validate_steps!
|
|
18
|
+
steps = operation_class.steps
|
|
19
|
+
return if steps.equal?(StepsDefinition::NONE)
|
|
20
|
+
|
|
21
|
+
unknown = operation_class.flow.method_names - steps.names
|
|
22
|
+
return if unknown.empty?
|
|
23
|
+
|
|
24
|
+
raise Exceptions::UnknownStep,
|
|
25
|
+
"#{operation_class.name}: flow calls #{unknown.join(', ')}, not defined in `steps do ... end`"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# WARN: AI MADE
|
|
29
|
+
# Only with ThunderOperation.config.validate_returns, and not when an adapter renders the payload instead.
|
|
30
|
+
def check_return?(response)
|
|
31
|
+
response.success? && ThunderOperation.config.validate_returns &&
|
|
32
|
+
operation_class.returns.is_a?(Parameters::Base) && !operation_class.renders_returns?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A missing payload fails unless the returns is optional: `success` without a payload is a mistake too.
|
|
36
|
+
def validate_return!(response)
|
|
37
|
+
returns = operation_class.returns
|
|
38
|
+
if response.payload.nil? && !returns.optional?
|
|
39
|
+
raise Exceptions::ParameterError.new("Invalid value for 'returns': expected #{returns.type}, got NilClass",
|
|
40
|
+
field: 'returns')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
returns.check!(response.payload, path: 'returns')
|
|
44
|
+
rescue Exceptions::ParameterError => e
|
|
45
|
+
raise Exceptions::ReturnTypeError, "#{operation_class.name}: #{e.message} (#{response.payload.inspect})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Base
|
|
5
|
+
include Concerns::HasAdapter
|
|
6
|
+
|
|
7
|
+
class OptionsProxy
|
|
8
|
+
def initialize(operation_class, options)
|
|
9
|
+
@operation_class = operation_class
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(params = nil, **kwargs, &block)
|
|
14
|
+
@operation_class.execute(params || kwargs, @options, &block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call!(params = nil, **kwargs, &block)
|
|
18
|
+
@operation_class.execute(params || kwargs, @options.merge(insecure_mode: true), &block)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
NO_OPTIONS = {}.freeze
|
|
23
|
+
INSECURE_OPTIONS = { insecure_mode: true }.freeze
|
|
24
|
+
|
|
25
|
+
attr_accessor :_current_step
|
|
26
|
+
|
|
27
|
+
def initialize(params, options = {})
|
|
28
|
+
@params = params
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def flow(&block)
|
|
34
|
+
return @flow = Flow.build(&block) if block
|
|
35
|
+
|
|
36
|
+
@flow || (superclass.respond_to?(:flow) ? superclass.flow : Flow::EMPTY)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def steps(&block)
|
|
40
|
+
return @steps = StepsDefinition.build(self, &block) if block
|
|
41
|
+
|
|
42
|
+
@steps || StepsDefinition::NONE
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# WARN: AI MADE
|
|
46
|
+
# What the operation returns, declared as a param: `returns Book::Relation`, `returns Hash do field ... end`,
|
|
47
|
+
# `returns Array do items Hash do ... end end`. A Symbol names a builder an adapter adds.
|
|
48
|
+
def returns(type = nil, options = {}, &block)
|
|
49
|
+
return @returns = returns_builder(type).call(self, &block) if type.is_a?(Symbol)
|
|
50
|
+
return @returns = Parameters::Base.from_args(*[type, options].compact, &block) if type || block
|
|
51
|
+
|
|
52
|
+
@returns || (superclass.respond_to?(:returns) ? superclass.returns : nil)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# True when an adapter renders the payload through `returns` (Framer) instead of only checking it.
|
|
56
|
+
def renders_returns? = false
|
|
57
|
+
|
|
58
|
+
def returns_builders
|
|
59
|
+
@returns_builders ||= superclass.respond_to?(:returns_builders) ? superclass.returns_builders.dup : {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def call(params = nil, **kwargs, &block)
|
|
63
|
+
execute(params || kwargs, NO_OPTIONS, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def call!(params = nil, **kwargs, &block)
|
|
67
|
+
execute(params || kwargs, INSECURE_OPTIONS, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def failure_key_prefix
|
|
71
|
+
name.to_s.underscore.tr('/', '.')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with(**options)
|
|
75
|
+
OptionsProxy.new(self, options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def execute(params, options, &block)
|
|
79
|
+
raise ArgumentError, 'call! does not take a handlers block' if block && options[:insecure_mode]
|
|
80
|
+
|
|
81
|
+
response = run_instance(params, options)
|
|
82
|
+
|
|
83
|
+
block ? Handlers.run(response, &block) : response
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def returns_builder(kind)
|
|
89
|
+
returns_builders.fetch(kind) do
|
|
90
|
+
raise ArgumentError, "unknown returns kind #{kind.inspect}; append the adapter that defines it"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_instance(params, options)
|
|
95
|
+
operation_instance = new(params, options)
|
|
96
|
+
|
|
97
|
+
handle_adapters(params, operation_instance, options) { operation_instance._run }
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
raise Exceptions::Trail.record(e, operation_instance) if options[:insecure_mode]
|
|
100
|
+
|
|
101
|
+
operation_instance._report_error(e)
|
|
102
|
+
operation_instance._failure(e)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def _run
|
|
107
|
+
self.class.flow.run(self) || _success
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def _report_error(error)
|
|
111
|
+
handler = ThunderOperation.config.error_handler
|
|
112
|
+
|
|
113
|
+
return unless handler
|
|
114
|
+
|
|
115
|
+
level = error.is_a?(Exceptions::OperationError) ? :warning : :error
|
|
116
|
+
handler.call(error, level:, operation: self.class, step: _current_step&.name, params: _reportable_params)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def _reportable_params
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def raise_failure(message_key, extras: nil, status: nil)
|
|
124
|
+
raise Exceptions::OperationError.new(message_key, self.class, extras:, status:)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def _success(payload = nil, status: nil)
|
|
128
|
+
Response.new(was_success: true, payload:, status:, operation_name: self.class.name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def _failure(exception, status = :unprocessable_entity, payload: nil)
|
|
132
|
+
Exceptions::Trail.record(exception, self)
|
|
133
|
+
|
|
134
|
+
Response.new(
|
|
135
|
+
was_success: false,
|
|
136
|
+
payload:,
|
|
137
|
+
operation_name: self.class.name,
|
|
138
|
+
error: exception,
|
|
139
|
+
status:,
|
|
140
|
+
failed_step: _current_step&.name
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Concerns
|
|
5
|
+
module HasAdapter
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def append_adapter(adapter_class)
|
|
12
|
+
adapters << adapter_class
|
|
13
|
+
@adapter_rings = nil
|
|
14
|
+
|
|
15
|
+
return unless adapter_class.adapted_block
|
|
16
|
+
|
|
17
|
+
class_exec(&adapter_class.adapted_block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def adapters
|
|
21
|
+
@adapters ||= superclass.respond_to?(:adapters) ? superclass.adapters.dup : []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle_adapters(params, operation_instance, options, &block)
|
|
25
|
+
deep_ring, outer_ring = adapter_rings
|
|
26
|
+
|
|
27
|
+
deep_block = deep_ring.reduce(block) do |previous_block, adapter|
|
|
28
|
+
proc { adapter.deep_execute(params, operation_instance, options, &previous_block) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
outer_ring.reduce(deep_block) do |previous_block, adapter|
|
|
32
|
+
proc { adapter.execute(params, operation_instance, options, &previous_block) }
|
|
33
|
+
end.call
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def adapter_rings
|
|
39
|
+
@adapter_rings ||= begin
|
|
40
|
+
instances = adapters.map { |adapter_class| adapter_class.new(self).freeze }
|
|
41
|
+
[instances.select { |adapter| adapter.respond_to?(:deep_execute) }.freeze, instances.freeze].freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Config
|
|
5
|
+
attr_accessor :transaction
|
|
6
|
+
|
|
7
|
+
attr_accessor :error_handler
|
|
8
|
+
|
|
9
|
+
# WARN: AI MADE
|
|
10
|
+
# Whether a payload is checked against `returns` (Validations); off by default, so a mistake in a declaration
|
|
11
|
+
# does not fail an operation in production.
|
|
12
|
+
attr_accessor :validate_returns
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@transaction = ->(&block) { block.call }
|
|
16
|
+
@error_handler = nil
|
|
17
|
+
@validate_returns = false
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def config
|
|
23
|
+
@config ||= Config.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset_config!
|
|
27
|
+
@config = Config.new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Exceptions
|
|
5
|
+
module Trail
|
|
6
|
+
Entry = Data.define(:operation_class, :step) do
|
|
7
|
+
def to_s
|
|
8
|
+
step ? "#{operation_class.name}##{step}" : operation_class.name.to_s
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.record(error, operation)
|
|
13
|
+
return error if error.nil? || error.frozen?
|
|
14
|
+
|
|
15
|
+
error.extend(self) unless error.is_a?(self)
|
|
16
|
+
entry = Entry.new(operation.class, operation._current_step&.name)
|
|
17
|
+
error.operation_trail << entry unless error.operation_trail.last == entry
|
|
18
|
+
|
|
19
|
+
error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def operation_trail
|
|
23
|
+
@operation_trail ||= []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Exceptions
|
|
5
|
+
class Interruption < StandardError; end
|
|
6
|
+
|
|
7
|
+
class ParameterError < StandardError
|
|
8
|
+
attr_reader :field
|
|
9
|
+
|
|
10
|
+
def initialize(message, field: nil)
|
|
11
|
+
@field = field
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class OperationError < StandardError
|
|
17
|
+
attr_reader :message_key, :extras, :status
|
|
18
|
+
|
|
19
|
+
def initialize(message_key, operation_class, extras: nil, status: nil)
|
|
20
|
+
@message_key = message_key
|
|
21
|
+
@extras = extras
|
|
22
|
+
@status = status
|
|
23
|
+
|
|
24
|
+
super("operation_exception.#{operation_class.failure_key_prefix}.#{message_key}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class DeclarationError < ScriptError; end
|
|
29
|
+
class UnknownStep < DeclarationError; end
|
|
30
|
+
class ReturnTypeError < DeclarationError; end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Flow
|
|
5
|
+
class Builder < BasicObject
|
|
6
|
+
def self.record(&block)
|
|
7
|
+
builder = new
|
|
8
|
+
builder.instance_eval(&block)
|
|
9
|
+
builder.__steps__
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@steps = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def __steps__
|
|
17
|
+
@steps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
21
|
+
@steps << ::ThunderOperation::Steps.build(name, *args, **kwargs, &block)
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(*)
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Flow
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
attr_reader :steps
|
|
8
|
+
|
|
9
|
+
def self.build(&block)
|
|
10
|
+
new(Builder.record(&block))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(steps = [])
|
|
14
|
+
@steps = steps.dup.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
EMPTY = new
|
|
19
|
+
|
|
20
|
+
def each(&)
|
|
21
|
+
steps.each(&)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def names
|
|
25
|
+
steps.map(&:name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_names
|
|
29
|
+
steps.flat_map do |step|
|
|
30
|
+
case step
|
|
31
|
+
when Steps::Method then [step.name]
|
|
32
|
+
when Steps::Clause then step.flow.method_names
|
|
33
|
+
else []
|
|
34
|
+
end
|
|
35
|
+
end.uniq
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def empty?
|
|
39
|
+
steps.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(instance)
|
|
43
|
+
steps.each do |step|
|
|
44
|
+
instance._current_step = step
|
|
45
|
+
result = step.run(instance)
|
|
46
|
+
|
|
47
|
+
return result if result.is_a?(Response)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
steps.map(&:to_s).join(', ')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Handlers < BasicObject
|
|
5
|
+
def self.run(response, &block)
|
|
6
|
+
handlers = new
|
|
7
|
+
handlers.instance_exec(&block)
|
|
8
|
+
handlers.__dispatch__(response, block.binding.receiver)
|
|
9
|
+
response
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@success = nil
|
|
14
|
+
@failure = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def success(&block)
|
|
18
|
+
@success = block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failure(&block)
|
|
22
|
+
@failure = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def __dispatch__(response, receiver)
|
|
26
|
+
if response.success?
|
|
27
|
+
receiver.instance_exec(response, &@success) if @success
|
|
28
|
+
elsif @failure
|
|
29
|
+
receiver.instance_exec(response, &@failure)
|
|
30
|
+
else
|
|
31
|
+
::Kernel.raise response.error
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def method_missing(name, *)
|
|
36
|
+
::Kernel.raise ::ArgumentError, "only `success` and `failure` are allowed in a call block, got `#{name}`"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def respond_to_missing?(*)
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Lookup
|
|
5
|
+
def resolve(name, base:, except: [])
|
|
6
|
+
const_name = name.to_s.camelize
|
|
7
|
+
return if except.include?(const_name) || !const_defined?(const_name, false)
|
|
8
|
+
|
|
9
|
+
klass = const_get(const_name, false)
|
|
10
|
+
klass if klass.is_a?(Class) && klass < base
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Parameters
|
|
5
|
+
class ArrayParameter < Base
|
|
6
|
+
def initialize(_type = nil, options = {}, &block)
|
|
7
|
+
super(Array, options)
|
|
8
|
+
|
|
9
|
+
@items = nil
|
|
10
|
+
|
|
11
|
+
instance_exec(&block) if block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def items(*args, &block)
|
|
15
|
+
return @items if args.empty? && block.nil?
|
|
16
|
+
|
|
17
|
+
@items = self.class.from_args(*args, &block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def filter(values)
|
|
21
|
+
return FILTERED if sensitive?
|
|
22
|
+
return values if @items.nil? || values.nil?
|
|
23
|
+
|
|
24
|
+
values.map { |value| @items.filter(value) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# WARN: AI MADE
|
|
28
|
+
def resolve_types!(&)
|
|
29
|
+
@items&.resolve_types!(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check!(values, path: nil)
|
|
33
|
+
validate!(values, path:)
|
|
34
|
+
return if @items.nil? || values.nil?
|
|
35
|
+
|
|
36
|
+
values.each_with_index { |value, index| @items.check!(value, path: "#{path}[#{index}]") }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sanitize(values, path: nil)
|
|
40
|
+
validate!(values, path:)
|
|
41
|
+
|
|
42
|
+
return values if @items.nil? || values.nil?
|
|
43
|
+
|
|
44
|
+
values.each_with_index.map { |value, index| @items.sanitize(value, path: "#{path}[#{index}]") }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Parameters
|
|
5
|
+
class Base
|
|
6
|
+
FILTERED = '[FILTERED]'
|
|
7
|
+
|
|
8
|
+
attr_reader :type, :options
|
|
9
|
+
|
|
10
|
+
def self.from_args(*args, &block)
|
|
11
|
+
type, options = parse_args(args)
|
|
12
|
+
|
|
13
|
+
raise Exceptions::DeclarationError, "parameter type is required, got nil" if type.nil? && !block
|
|
14
|
+
|
|
15
|
+
klass = (Parameters.for_type(type) if type.is_a?(Class)) || (block ? HashParameter : Base)
|
|
16
|
+
|
|
17
|
+
klass.new(type, options, &block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse_args(args)
|
|
21
|
+
# A String names a class the engine does not know yet ('Models::Supplier'); whoever loads the operation
|
|
22
|
+
# replaces it through resolve_types!.
|
|
23
|
+
case args
|
|
24
|
+
in [Class | String => type, Hash => options] then [type, options]
|
|
25
|
+
in [Hash => options] then [nil, options]
|
|
26
|
+
in [Class | String => type] then [type, {}]
|
|
27
|
+
else [nil, {}]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
private_class_method :parse_args
|
|
31
|
+
|
|
32
|
+
def initialize(type = nil, options = {})
|
|
33
|
+
@type = type
|
|
34
|
+
@options = options
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def optional?
|
|
38
|
+
@options[:optional] == true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def required?
|
|
42
|
+
!optional?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sensitive?
|
|
46
|
+
@options[:sensitive] == true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def filter(value)
|
|
50
|
+
sensitive? ? FILTERED : value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sanitize(value, path: nil)
|
|
54
|
+
validate!(value, path:)
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def valid?(value)
|
|
60
|
+
if unresolved_type?
|
|
61
|
+
raise Exceptions::DeclarationError, "type '#{@type}' was never resolved to a class"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
(@type.nil? || value.is_a?(@type)) ||
|
|
65
|
+
(options[:null] != false && value.nil?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# WARN: AI MADE
|
|
69
|
+
# Replaces a type given by name with the class the block returns for it; nested parameters do the same.
|
|
70
|
+
def resolve_types!
|
|
71
|
+
@type = yield(@type) if @type.is_a?(String)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unresolved_type? = @type.is_a?(String)
|
|
75
|
+
|
|
76
|
+
# The value has exactly the declared shape (returns and event payloads): the type, and for a Hash every
|
|
77
|
+
# declared key and no other. Raises ParameterError with the path of the first difference.
|
|
78
|
+
def check!(value, path: nil)
|
|
79
|
+
validate!(value, path:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate!(value, path: nil)
|
|
83
|
+
return if valid?(value)
|
|
84
|
+
|
|
85
|
+
raise Exceptions::ParameterError.new(
|
|
86
|
+
"Invalid value for '#{path}': expected #{@type}, got #{value.class}",
|
|
87
|
+
field: path
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|