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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 45776abcd16980536bbd63b5bb0b5f67c17103b990408ea68673c1ccb3b5fec2
|
|
4
|
+
data.tar.gz: a4965001230ededf0ee43cd9ddcead9be3fc6bad008fd7fac0cc2328be98d5c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1c544048645185dbcaf0fc93fe2364e8361a23e7a0cd9850933dcdc5504dff09d5fa026c51f8a4e18f8788bfd00fed687d5a1c90ffe52c4986b9b5f73d554dd6
|
|
7
|
+
data.tar.gz: 9c38b5e95bed8425e73ebfb4dc4b335808849d693ba605f5498e83795dbc13bda60ce68d11c1d1a362eddb156b1f8403317ba143deb2a890a149b14de15313db
|
data/README.md
ADDED
data/config/default.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# WARN: AI MADE
|
|
2
|
+
# Defaults for every project that loads the thunderoperation cops (require: thunderoperation/rubocop), merged over
|
|
3
|
+
# RuboCop's own defaults; a project's .rubocop.yml still overrides them.
|
|
4
|
+
|
|
5
|
+
# `flow do ... end` and `steps do ... end` hold the whole flow and every step of an operation, so their length is the
|
|
6
|
+
# operation's, not a block's.
|
|
7
|
+
Metrics/BlockLength:
|
|
8
|
+
AllowedMethods:
|
|
9
|
+
- refine # RuboCop's default
|
|
10
|
+
- flow
|
|
11
|
+
- steps
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Inside `Operation.call(...) do ... end` only `success { |result| }` and `failure { |result| }` belong,
|
|
7
|
+
# each with a block, each at most once. Anything else runs against the handlers collector, not the operation.
|
|
8
|
+
class CallHandlers < Base
|
|
9
|
+
include OperationHelpers
|
|
10
|
+
|
|
11
|
+
HANDLERS = %i[success failure].freeze
|
|
12
|
+
UNEXPECTED_MSG = 'Only `success` and `failure` handlers belong in a `call` block.'
|
|
13
|
+
BLOCK_MSG = '`%<handler>s` handler needs a block.'
|
|
14
|
+
DUPLICATE_MSG = '`%<handler>s` handler is declared more than once.'
|
|
15
|
+
|
|
16
|
+
def on_block(node)
|
|
17
|
+
return unless call_with_handlers?(node) && inside_operation?(node)
|
|
18
|
+
|
|
19
|
+
seen = []
|
|
20
|
+
block_statements(node).each do |statement|
|
|
21
|
+
handler = handler_name(statement)
|
|
22
|
+
next add_offense(statement, message: UNEXPECTED_MSG) unless handler
|
|
23
|
+
next add_offense(statement, message: format(BLOCK_MSG, handler:)) unless statement.block_type?
|
|
24
|
+
|
|
25
|
+
add_offense(statement, message: format(DUPLICATE_MSG, handler:)) if seen.include?(handler)
|
|
26
|
+
seen << handler
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def call_with_handlers?(node)
|
|
33
|
+
send = node.send_node
|
|
34
|
+
send.method_name == :call && send.receiver && node.arguments.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inside_operation?(node)
|
|
38
|
+
node.each_ancestor(:class).any? { |klass| operation_class?(klass) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def block_statements(node)
|
|
42
|
+
return [] unless node.body
|
|
43
|
+
|
|
44
|
+
node.body.begin_type? ? node.body.children : [node.body]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handler_name(statement)
|
|
48
|
+
send = statement.block_type? ? statement.send_node : statement
|
|
49
|
+
return unless send.send_type? && send.receiver.nil? && HANDLERS.include?(send.method_name)
|
|
50
|
+
|
|
51
|
+
send.method_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Instance methods of an operation are steps, and steps live in `steps do ... end`. Base classes (no sections) are exempt.
|
|
7
|
+
#
|
|
8
|
+
# # bad
|
|
9
|
+
# def create_product; end
|
|
10
|
+
#
|
|
11
|
+
# # good
|
|
12
|
+
# steps do
|
|
13
|
+
# def create_product; end
|
|
14
|
+
# end
|
|
15
|
+
class DefOutsideSteps < Base
|
|
16
|
+
include OperationHelpers
|
|
17
|
+
|
|
18
|
+
MSG = 'Define `%<method>s` inside `steps do ... end`.'
|
|
19
|
+
|
|
20
|
+
def on_class(node)
|
|
21
|
+
return unless operation_class?(node) && !base_class?(node)
|
|
22
|
+
|
|
23
|
+
body_statements(node).each do |statement|
|
|
24
|
+
next unless statement.def_type?
|
|
25
|
+
|
|
26
|
+
add_offense(statement, message: format(MSG, method: statement.method_name))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# A class with no param, flow or steps is a base class (Subscriber, ApplicationOperation): helpers live there.
|
|
33
|
+
def base_class?(node)
|
|
34
|
+
sections(node).empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# An operation that declares params or steps must declare a `flow`. A class with none of the three is
|
|
7
|
+
# taken as a base class and left alone.
|
|
8
|
+
class FlowRequired < Base
|
|
9
|
+
include OperationHelpers
|
|
10
|
+
|
|
11
|
+
MSG = 'Operation has no `flow do ... end`.'
|
|
12
|
+
|
|
13
|
+
def on_class(node)
|
|
14
|
+
return unless operation_class?(node)
|
|
15
|
+
|
|
16
|
+
names = sections(node).map(&:first)
|
|
17
|
+
return if names.include?(:flow) || !names.intersect?(%i[param steps])
|
|
18
|
+
|
|
19
|
+
add_offense(node.identifier)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module RuboCop
|
|
5
|
+
module Cop
|
|
6
|
+
module ThunderOperation
|
|
7
|
+
# In the editor only: marks `flow do` when the flow calls steps that `steps do ... end` does not define, so its
|
|
8
|
+
# code action creates all of them at once (each step's line, through UnknownStep, creates only its own). Off
|
|
9
|
+
# outside the editor's language server, so `rubocop -A` does not create the steps twice.
|
|
10
|
+
#
|
|
11
|
+
# flow do # Steps missing from steps do: create_order, notify
|
|
12
|
+
# create_order
|
|
13
|
+
# notify
|
|
14
|
+
# end
|
|
15
|
+
class MissingSteps < Base
|
|
16
|
+
include OperationHelpers
|
|
17
|
+
extend AutoCorrector
|
|
18
|
+
|
|
19
|
+
MSG = 'Steps missing from `steps do ... end`: %<steps>s.'
|
|
20
|
+
|
|
21
|
+
def safe_autocorrect? = false
|
|
22
|
+
|
|
23
|
+
def on_class(node)
|
|
24
|
+
return unless RuboCop::LSP.enabled? && operation_class?(node)
|
|
25
|
+
|
|
26
|
+
steps_block = section(node, :steps)
|
|
27
|
+
flow_block = section(node, :flow)
|
|
28
|
+
return unless steps_block && flow_block&.block_type?
|
|
29
|
+
|
|
30
|
+
defined = step_names(steps_block)
|
|
31
|
+
flow_order = flow_step_names(flow_block)
|
|
32
|
+
missing = flow_order.reject { |name| defined.include?(name) }
|
|
33
|
+
return if missing.empty?
|
|
34
|
+
|
|
35
|
+
add_offense(flow_block.send_node, message: format(MSG, steps: missing.join(', '))) do |corrector|
|
|
36
|
+
add_steps(corrector, steps_block, flow_order, missing, missing)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Shared reading of an operation class body: which classes are operations, their sections
|
|
7
|
+
# (`param`, `flow`, `steps`, `returns`), the steps a flow calls and the methods `steps do` defines.
|
|
8
|
+
module OperationHelpers
|
|
9
|
+
# InternalOperation and ExternalOperation: the bases a chunk keeps in app/common.
|
|
10
|
+
OPERATION_PARENTS = %w[
|
|
11
|
+
Operation Operations::Base Operations::Internal Operations::External Operations::Subscriber ThunderOperation::Base
|
|
12
|
+
InternalOperation ExternalOperation
|
|
13
|
+
].freeze
|
|
14
|
+
SECTIONS = %i[param flow steps returns].freeze
|
|
15
|
+
# Flow calls that are steps of the framework, not methods of the operation.
|
|
16
|
+
FLOW_KEYWORDS = %i[success render].freeze
|
|
17
|
+
|
|
18
|
+
def operation_class?(class_node)
|
|
19
|
+
parent = class_node.parent_class
|
|
20
|
+
return false unless parent&.const_type?
|
|
21
|
+
|
|
22
|
+
name = parent.const_name
|
|
23
|
+
OPERATION_PARENTS.any? { |suffix| name == suffix || name.end_with?("::#{suffix}") }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Top-level statements of the class body.
|
|
27
|
+
def body_statements(class_node)
|
|
28
|
+
body = class_node.body
|
|
29
|
+
return [] unless body
|
|
30
|
+
|
|
31
|
+
body.begin_type? ? body.children : [body]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# [[section_name, node], ...] in source order; node is the send or the block wrapping it.
|
|
35
|
+
def sections(class_node)
|
|
36
|
+
body_statements(class_node).filter_map do |statement|
|
|
37
|
+
send = statement.block_type? ? statement.send_node : statement
|
|
38
|
+
next unless send.send_type? && send.receiver.nil? && SECTIONS.include?(send.method_name)
|
|
39
|
+
|
|
40
|
+
[send.method_name, statement]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def section(class_node, name)
|
|
45
|
+
sections(class_node).find { |section_name, _| section_name == name }&.last
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Names of the methods a `flow do ... end` block calls, including inside clauses.
|
|
49
|
+
def flow_step_names(flow_block)
|
|
50
|
+
return [] unless flow_block&.block_type? && flow_block.body
|
|
51
|
+
|
|
52
|
+
statements = flow_block.body.begin_type? ? flow_block.body.children : [flow_block.body]
|
|
53
|
+
statements.flat_map { |statement| flow_statement_names(statement) }.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def flow_statement_names(statement)
|
|
57
|
+
if statement.block_type?
|
|
58
|
+
flow_step_names(statement)
|
|
59
|
+
elsif statement.send_type? && statement.receiver.nil? && !FLOW_KEYWORDS.include?(statement.method_name)
|
|
60
|
+
[statement.method_name]
|
|
61
|
+
else
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The `def` nodes directly inside `steps do ... end`.
|
|
67
|
+
def step_defs(steps_block)
|
|
68
|
+
return [] unless steps_block&.block_type? && steps_block.body
|
|
69
|
+
|
|
70
|
+
statements = steps_block.body.begin_type? ? steps_block.body.children : [steps_block.body]
|
|
71
|
+
statements.select(&:def_type?)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def step_names(steps_block)
|
|
75
|
+
step_defs(steps_block).map(&:method_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# WARN: AI MADE
|
|
79
|
+
# Writes an empty `def name` into `steps do`, in flow order (used by UnknownStep and MissingSteps).
|
|
80
|
+
# After the def of the closest earlier step that steps defines, else right after `steps do`. Corrections of one
|
|
81
|
+
# run (rubocop -A) that land on the same point keep the order they were added in, which is the flow order.
|
|
82
|
+
def add_steps(corrector, steps_block, flow_order, missing, names)
|
|
83
|
+
defs = step_defs(steps_block).to_h { |definition| [definition.method_name, definition] }
|
|
84
|
+
indent = ' ' * (steps_block.source_range.column + 2)
|
|
85
|
+
|
|
86
|
+
names.group_by { |name| anchor_for(name, flow_order, defs) }.each do |anchor, group|
|
|
87
|
+
methods = group.map { |name| "#{indent}def #{name}\n#{indent}end" }
|
|
88
|
+
|
|
89
|
+
text =
|
|
90
|
+
if anchor
|
|
91
|
+
methods.map { |method| "\n#{method}\n" }.join
|
|
92
|
+
elsif steps_block.body
|
|
93
|
+
methods.map { |method| "#{method}\n\n" }.join
|
|
94
|
+
else
|
|
95
|
+
first = missing.find { |other| anchor_for(other, flow_order, defs).nil? } == group.first
|
|
96
|
+
methods.each_with_index.map { |method, i| "#{"\n" unless first && i.zero?}#{method}\n" }.join
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
corrector.insert_after(line_start(anchor ? anchor.last_line + 1 : steps_block.loc.begin.line + 1), text)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# An empty range at column 0 of the line. Insertions go there because RuboCop's language server (1.90) turns
|
|
104
|
+
# a column into a position measuring the line of the offense, not the line of the edit: past the length of
|
|
105
|
+
# the offense line, the position comes back short (inside `do`). Column 0 is the same on any line.
|
|
106
|
+
def line_start(line)
|
|
107
|
+
buffer = processed_source.buffer
|
|
108
|
+
position = buffer.line_range(line).begin_pos
|
|
109
|
+
Parser::Source::Range.new(buffer, position, position)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The def of the closest step the flow calls before this one and steps defines.
|
|
113
|
+
def anchor_for(name, flow_order, defs)
|
|
114
|
+
flow_order.take(flow_order.index(name)).reverse.filter_map { |earlier| defs[earlier] }.first
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Nodes that call `name` with no receiver inside `node`.
|
|
118
|
+
def bare_calls(node, name)
|
|
119
|
+
node.each_descendant(:send).select { |send| send.receiver.nil? && send.method_name == name }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Sections of an operation come in a fixed order: `param`s, `flow`, `steps`, `returns`; each once (params many).
|
|
7
|
+
#
|
|
8
|
+
# # bad
|
|
9
|
+
# flow do ... end
|
|
10
|
+
# param :name
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# param :name
|
|
14
|
+
# flow do ... end
|
|
15
|
+
# steps do ... end
|
|
16
|
+
# returns Models::Product
|
|
17
|
+
class OperationSectionOrder < Base
|
|
18
|
+
include OperationHelpers
|
|
19
|
+
|
|
20
|
+
ORDER_MSG = '`%<section>s` must come before `%<previous>s` (order: param, flow, steps, returns).'
|
|
21
|
+
DUPLICATE_MSG = '`%<section>s` is declared more than once.'
|
|
22
|
+
|
|
23
|
+
def on_class(node)
|
|
24
|
+
return unless operation_class?(node)
|
|
25
|
+
|
|
26
|
+
seen = []
|
|
27
|
+
sections(node).each do |name, statement|
|
|
28
|
+
check_order(name, statement, seen)
|
|
29
|
+
check_duplicate(name, statement, seen)
|
|
30
|
+
seen << name
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def check_order(name, statement, seen)
|
|
37
|
+
later = seen.find { |previous| SECTIONS.index(previous) > SECTIONS.index(name) }
|
|
38
|
+
return unless later
|
|
39
|
+
|
|
40
|
+
add_offense(statement, message: format(ORDER_MSG, section: name, previous: later))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_duplicate(name, statement, seen)
|
|
44
|
+
return if name == :param || !seen.include?(name)
|
|
45
|
+
|
|
46
|
+
add_offense(statement, message: format(DUPLICATE_MSG, section: name))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# A step does not call another step; only the flow sequences them.
|
|
7
|
+
#
|
|
8
|
+
# steps do
|
|
9
|
+
# def create_product
|
|
10
|
+
# charge_order # bad
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
class StepsCannotCallSteps < Base
|
|
14
|
+
include OperationHelpers
|
|
15
|
+
|
|
16
|
+
MSG = 'Step `%<caller>s` must not call step `%<called>s`; only the flow calls steps.'
|
|
17
|
+
|
|
18
|
+
def on_class(node)
|
|
19
|
+
return unless operation_class?(node)
|
|
20
|
+
|
|
21
|
+
steps_block = section(node, :steps)
|
|
22
|
+
return unless steps_block
|
|
23
|
+
|
|
24
|
+
names = step_names(steps_block)
|
|
25
|
+
step_defs(steps_block).each do |definition|
|
|
26
|
+
names.each do |name|
|
|
27
|
+
bare_calls(definition, name).each do |call|
|
|
28
|
+
add_offense(call, message: format(MSG, caller: definition.method_name, called: name))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Methods in `steps do ... end` are defined in the order the flow first calls them.
|
|
7
|
+
#
|
|
8
|
+
# flow do
|
|
9
|
+
# create_product
|
|
10
|
+
# charge_order
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# steps do
|
|
14
|
+
# def charge_order; end # bad: the flow calls create_product first
|
|
15
|
+
# def create_product; end
|
|
16
|
+
# end
|
|
17
|
+
class StepsOrder < Base
|
|
18
|
+
include OperationHelpers
|
|
19
|
+
|
|
20
|
+
MSG = 'Step `%<step>s` is defined before `%<earlier>s`, but the flow calls it after.'
|
|
21
|
+
|
|
22
|
+
def on_class(node)
|
|
23
|
+
return unless operation_class?(node)
|
|
24
|
+
|
|
25
|
+
steps_block = section(node, :steps)
|
|
26
|
+
flow_block = section(node, :flow)
|
|
27
|
+
return unless steps_block && flow_block
|
|
28
|
+
|
|
29
|
+
check_order(flow_step_names(flow_block), step_defs(steps_block))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Walks the defs the flow calls; a def whose flow position is lower than one already seen is out of place.
|
|
35
|
+
def check_order(flow_order, defs)
|
|
36
|
+
called = defs.select { |definition| flow_order.include?(definition.method_name) }
|
|
37
|
+
highest = nil
|
|
38
|
+
|
|
39
|
+
called.each do |definition|
|
|
40
|
+
position = flow_order.index(definition.method_name)
|
|
41
|
+
|
|
42
|
+
if highest && position < flow_order.index(highest.method_name)
|
|
43
|
+
add_offense(definition, message: format(MSG, step: highest.method_name, earlier: definition.method_name))
|
|
44
|
+
else
|
|
45
|
+
highest = definition
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Every method the flow calls is defined inside `steps do ... end`. Checked only when that block exists.
|
|
7
|
+
#
|
|
8
|
+
# flow do
|
|
9
|
+
# change_order # bad: steps defines charge_order
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# The correction adds an empty `def` for each missing step, in the order the flow calls them: after the def of
|
|
13
|
+
# the step the flow calls before it, or first in `steps do` when none is defined. It is offered as a code action
|
|
14
|
+
# (quick fix) in the editor and applied by `rubocop -A`; being unsafe, format on save and `rubocop -a` skip it,
|
|
15
|
+
# so a step being typed in the flow does not grow an empty method by itself.
|
|
16
|
+
class UnknownStep < Base
|
|
17
|
+
include OperationHelpers
|
|
18
|
+
extend AutoCorrector
|
|
19
|
+
|
|
20
|
+
MSG = 'Step `%<step>s` is not defined in `steps do ... end`.'
|
|
21
|
+
|
|
22
|
+
def safe_autocorrect? = false
|
|
23
|
+
|
|
24
|
+
def on_class(node)
|
|
25
|
+
return unless operation_class?(node)
|
|
26
|
+
|
|
27
|
+
steps_block = section(node, :steps)
|
|
28
|
+
flow_block = section(node, :flow)
|
|
29
|
+
return unless steps_block && flow_block
|
|
30
|
+
|
|
31
|
+
defined = step_names(steps_block)
|
|
32
|
+
flow_order = flow_step_names(flow_block)
|
|
33
|
+
missing = flow_order.reject { |name| defined.include?(name) }
|
|
34
|
+
offered = []
|
|
35
|
+
|
|
36
|
+
flow_calls(flow_block).each do |call|
|
|
37
|
+
name = call.method_name
|
|
38
|
+
next if defined.include?(name)
|
|
39
|
+
|
|
40
|
+
add_offense(call, message: format(MSG, step: name)) do |corrector|
|
|
41
|
+
# Each line fixes its own step; a step called twice is added by its first call only.
|
|
42
|
+
next if offered.include?(name)
|
|
43
|
+
|
|
44
|
+
offered << name
|
|
45
|
+
add_steps(corrector, steps_block, flow_order, missing, [name])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def flow_calls(flow_block)
|
|
53
|
+
flow_block.each_descendant(:send).select do |send|
|
|
54
|
+
send.receiver.nil? && !send.block_literal? && !FLOW_KEYWORDS.include?(send.method_name) &&
|
|
55
|
+
flow_step_names(flow_block).include?(send.method_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ThunderOperation
|
|
6
|
+
# Every method defined inside `steps do ... end` is called by the flow.
|
|
7
|
+
class UnusedStep < Base
|
|
8
|
+
include OperationHelpers
|
|
9
|
+
|
|
10
|
+
MSG = 'Step `%<step>s` is not called by the flow.'
|
|
11
|
+
|
|
12
|
+
def on_class(node)
|
|
13
|
+
return unless operation_class?(node)
|
|
14
|
+
|
|
15
|
+
steps_block = section(node, :steps)
|
|
16
|
+
flow_block = section(node, :flow)
|
|
17
|
+
return unless steps_block && flow_block
|
|
18
|
+
|
|
19
|
+
called = flow_step_names(flow_block)
|
|
20
|
+
step_defs(steps_block).each do |definition|
|
|
21
|
+
next if called.include?(definition.method_name)
|
|
22
|
+
|
|
23
|
+
add_offense(definition, message: format(MSG, step: definition.method_name))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
class Adapter
|
|
5
|
+
attr_reader :operation_class
|
|
6
|
+
|
|
7
|
+
def initialize(operation_class)
|
|
8
|
+
@operation_class = operation_class
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute(_params, _operation_instance, _options, &block)
|
|
12
|
+
block.call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_reader :adapted_block
|
|
17
|
+
|
|
18
|
+
def adapted(&block)
|
|
19
|
+
@adapted_block = block
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Adapters
|
|
5
|
+
# WARN: AI MADE
|
|
6
|
+
# Renders the payload through `returns Hash do ... end` (or `Array do items Hash do ... end end`): each field is
|
|
7
|
+
# read from the object returned (a record, a Hash) and coerced to its type; a nested Hash or an association is
|
|
8
|
+
# read the same way, one item at a time for a list. The declaration becomes a ThunderFrame::Frame, built once.
|
|
9
|
+
class Framer < Adapter
|
|
10
|
+
# WARN: AI MADE
|
|
11
|
+
# A map (`keys Date`, `values Hash do ... end`): each value rendered through the values' frame, keys kept.
|
|
12
|
+
MapFrame = Data.define(:values_frame) do
|
|
13
|
+
def render_hash(entity)
|
|
14
|
+
return entity.map { |item| render_hash(item) } if entity.is_a?(Array)
|
|
15
|
+
|
|
16
|
+
entity&.transform_values { |value| values_frame ? values_frame.render_hash(value) : value }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute(_params, _operation_instance, _options, &block)
|
|
21
|
+
response = block.call
|
|
22
|
+
return response unless response.success? && operation_class.returns_frame
|
|
23
|
+
|
|
24
|
+
response.with(payload: operation_class.returns_frame.render_hash(response.payload))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The frame of a Hash parameter; nil for a return that is not a Hash or a list of Hashes.
|
|
28
|
+
def self.frame_for(owner, parameter)
|
|
29
|
+
parameter = parameter.items if parameter.is_a?(Parameters::ArrayParameter)
|
|
30
|
+
return unless parameter.is_a?(Parameters::HashParameter)
|
|
31
|
+
return MapFrame.new(frame_for(owner, parameter.values)) if parameter.map?
|
|
32
|
+
|
|
33
|
+
ThunderFrame::Frame.new(:Result, owner:).tap { |frame| fill(frame, parameter) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.fill(frame, parameter)
|
|
37
|
+
parameter.configuration.each do |name, child|
|
|
38
|
+
options = child.options.slice(:optional, :from, :as, :eager_loads)
|
|
39
|
+
nested = child.is_a?(Parameters::ArrayParameter) ? child.items : child
|
|
40
|
+
|
|
41
|
+
# A Hash with no fields declared, or a map, is a value: rendered as it comes.
|
|
42
|
+
if nested.is_a?(Parameters::HashParameter) && nested.configuration.any?
|
|
43
|
+
frame.association(name, **options) { Framer.fill(self, nested) }
|
|
44
|
+
elsif child.is_a?(Parameters::ArrayParameter)
|
|
45
|
+
raise Exceptions::DeclarationError, "#{frame.label}: #{name} is a list of values; a rendered return " \
|
|
46
|
+
'takes lists of Hashes only'
|
|
47
|
+
else
|
|
48
|
+
frame.field(name, child.type || String, **options)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
adapted do
|
|
54
|
+
def self.renders_returns? = true
|
|
55
|
+
|
|
56
|
+
def self.returns_frame
|
|
57
|
+
return @returns_frame if defined?(@returns_frame)
|
|
58
|
+
|
|
59
|
+
@returns_frame = Framer.frame_for(self, returns)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderOperation
|
|
4
|
+
module Adapters
|
|
5
|
+
class Interruptable < Adapter
|
|
6
|
+
def execute(_params, operation_instance, _options, &block)
|
|
7
|
+
block.call
|
|
8
|
+
rescue Exceptions::Interruption => e
|
|
9
|
+
operation_instance._failure(e)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
adapted do
|
|
13
|
+
def interrupt
|
|
14
|
+
raise Exceptions::Interruption
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|