light-services 3.0.0 → 3.1.1
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/.rubocop.yml +7 -1
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +11 -11
- data/docs/{readme.md → README.md} +12 -11
- data/docs/{summary.md → SUMMARY.md} +11 -1
- data/docs/arguments.md +23 -0
- data/docs/concepts.md +19 -19
- data/docs/configuration.md +36 -0
- data/docs/errors.md +31 -1
- data/docs/outputs.md +23 -0
- data/docs/quickstart.md +1 -1
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/steps.md +62 -8
- data/docs/testing.md +1 -1
- data/lib/light/services/base.rb +110 -7
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +293 -41
- data/lib/light/services/collection.rb +50 -2
- data/lib/light/services/concerns/execution.rb +3 -0
- data/lib/light/services/config.rb +83 -3
- data/lib/light/services/constants.rb +3 -0
- data/lib/light/services/dsl/arguments_dsl.rb +1 -0
- data/lib/light/services/dsl/outputs_dsl.rb +1 -0
- data/lib/light/services/dsl/validation.rb +30 -0
- data/lib/light/services/exceptions.rb +19 -1
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +74 -2
- data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +33 -5
- data/lib/light/services/settings/step.rb +23 -5
- data/lib/light/services/version.rb +1 -1
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- metadata +17 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Enforces a consistent order for DSL declarations in service classes.
|
|
7
|
+
#
|
|
8
|
+
# The expected order is: `config` → `arg` → `step` → `output`
|
|
9
|
+
#
|
|
10
|
+
# @safety
|
|
11
|
+
# This cop's autocorrection is safe but may change the visual grouping of your code.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# class MyService < ApplicationService
|
|
16
|
+
# step :process
|
|
17
|
+
# arg :name, type: String
|
|
18
|
+
# output :result, type: Hash
|
|
19
|
+
# config raise_on_error: true
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # good
|
|
23
|
+
# class MyService < ApplicationService
|
|
24
|
+
# config raise_on_error: true
|
|
25
|
+
#
|
|
26
|
+
# arg :name, type: String
|
|
27
|
+
#
|
|
28
|
+
# step :process
|
|
29
|
+
#
|
|
30
|
+
# output :result, type: Hash
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class DslOrder < Base
|
|
34
|
+
extend AutoCorrector
|
|
35
|
+
|
|
36
|
+
MSG = "`%<current>s` should come before `%<previous>s`. " \
|
|
37
|
+
"Expected order: config → arg → step → output."
|
|
38
|
+
|
|
39
|
+
DSL_METHODS = [:config, :arg, :step, :output].freeze
|
|
40
|
+
DSL_ORDER = { config: 0, arg: 1, step: 2, output: 3 }.freeze
|
|
41
|
+
|
|
42
|
+
def on_class(node)
|
|
43
|
+
@dsl_calls = []
|
|
44
|
+
@class_node = node
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_send(node)
|
|
48
|
+
return unless dsl_call?(node)
|
|
49
|
+
|
|
50
|
+
@dsl_calls ||= []
|
|
51
|
+
@dsl_calls << { method: node.method_name, node: node }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def after_class(_node)
|
|
55
|
+
return unless @dsl_calls&.any?
|
|
56
|
+
|
|
57
|
+
check_order
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def dsl_call?(node)
|
|
63
|
+
node.send_type? &&
|
|
64
|
+
node.receiver.nil? &&
|
|
65
|
+
DSL_METHODS.include?(node.method_name)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_order
|
|
69
|
+
highest_order_seen = -1
|
|
70
|
+
highest_method_seen = nil
|
|
71
|
+
has_offense = false
|
|
72
|
+
|
|
73
|
+
@dsl_calls.each do |call|
|
|
74
|
+
current_order = DSL_ORDER[call[:method]]
|
|
75
|
+
|
|
76
|
+
if current_order < highest_order_seen
|
|
77
|
+
has_offense = true
|
|
78
|
+
add_offense(
|
|
79
|
+
call[:node],
|
|
80
|
+
message: format(MSG, current: call[:method], previous: highest_method_seen),
|
|
81
|
+
) do |corrector|
|
|
82
|
+
reorder_dsl_declarations(corrector) unless @corrected
|
|
83
|
+
@corrected = true
|
|
84
|
+
end
|
|
85
|
+
elsif current_order > highest_order_seen
|
|
86
|
+
highest_order_seen = current_order
|
|
87
|
+
highest_method_seen = call[:method]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@corrected = false if has_offense
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def reorder_dsl_declarations(corrector) # rubocop:disable Metrics/AbcSize
|
|
95
|
+
# Collect all DSL nodes with their source including leading comments
|
|
96
|
+
dsl_sources = @dsl_calls.map do |call|
|
|
97
|
+
{
|
|
98
|
+
method: call[:method],
|
|
99
|
+
node: call[:node],
|
|
100
|
+
source: source_with_leading_comment(call[:node]),
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Sort by expected order
|
|
105
|
+
sorted_sources = dsl_sources.sort_by { |item| DSL_ORDER[item[:method]] }
|
|
106
|
+
|
|
107
|
+
# Group by type to add blank lines between groups
|
|
108
|
+
grouped_source = build_grouped_source(sorted_sources)
|
|
109
|
+
|
|
110
|
+
# Calculate the range to replace (from first DSL to last DSL)
|
|
111
|
+
first_node = @dsl_calls.min_by { |c| c[:node].loc.expression.begin_pos }[:node]
|
|
112
|
+
last_node = @dsl_calls.max_by { |c| c[:node].loc.expression.end_pos }[:node]
|
|
113
|
+
|
|
114
|
+
# Get the range including leading comments of first node
|
|
115
|
+
start_pos = first_node.loc.expression.begin_pos
|
|
116
|
+
leading_comment = leading_comment_for(first_node)
|
|
117
|
+
start_pos = leading_comment.loc.expression.begin_pos if leading_comment
|
|
118
|
+
|
|
119
|
+
# Find the beginning of the line for proper replacement
|
|
120
|
+
start_pos = beginning_of_line(start_pos)
|
|
121
|
+
end_pos = end_of_line(last_node.loc.expression.end_pos)
|
|
122
|
+
|
|
123
|
+
range = range_between(start_pos, end_pos)
|
|
124
|
+
corrector.replace(range, grouped_source)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def source_with_leading_comment(node)
|
|
128
|
+
comment = leading_comment_for(node)
|
|
129
|
+
indent = " " * node.loc.column
|
|
130
|
+
|
|
131
|
+
if comment
|
|
132
|
+
"#{indent}#{comment.text}\n#{indent}#{node.source}"
|
|
133
|
+
else
|
|
134
|
+
"#{indent}#{node.source}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def leading_comment_for(node)
|
|
139
|
+
processed_source.comments.find do |comment|
|
|
140
|
+
comment.loc.line == node.loc.line - 1
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_grouped_source(sorted_sources)
|
|
145
|
+
result = []
|
|
146
|
+
current_type = nil
|
|
147
|
+
|
|
148
|
+
sorted_sources.each do |item|
|
|
149
|
+
# Add blank line when switching to a new DSL type
|
|
150
|
+
result << "" if current_type && current_type != item[:method]
|
|
151
|
+
current_type = item[:method]
|
|
152
|
+
result << item[:source]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
result.join("\n")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def beginning_of_line(pos)
|
|
159
|
+
source = processed_source.buffer.source
|
|
160
|
+
pos -= 1 while pos > 0 && source[pos - 1] != "\n"
|
|
161
|
+
pos
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def end_of_line(pos)
|
|
165
|
+
source = processed_source.buffer.source
|
|
166
|
+
pos += 1 while pos < source.length && source[pos] != "\n"
|
|
167
|
+
pos
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def range_between(start_pos, end_pos)
|
|
171
|
+
Parser::Source::Range.new(processed_source.buffer, start_pos, end_pos)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Ensures that step methods are defined as private.
|
|
7
|
+
# Step methods are implementation details and should not be part of the public API.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# class MyService < ApplicationService
|
|
12
|
+
# step :process
|
|
13
|
+
# step :notify
|
|
14
|
+
#
|
|
15
|
+
# def process
|
|
16
|
+
# # This should be private
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def notify
|
|
20
|
+
# # This should be private
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# class MyService < ApplicationService
|
|
26
|
+
# step :process
|
|
27
|
+
# step :notify
|
|
28
|
+
#
|
|
29
|
+
# private
|
|
30
|
+
#
|
|
31
|
+
# def process
|
|
32
|
+
# # Now private
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# def notify
|
|
36
|
+
# # Now private
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
class MissingPrivateKeyword < Base
|
|
41
|
+
MSG = "Step method `%<name>s` should be private."
|
|
42
|
+
|
|
43
|
+
def on_class(_node)
|
|
44
|
+
@step_names = []
|
|
45
|
+
@private_section_started = false
|
|
46
|
+
@public_step_methods = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on_send(node)
|
|
50
|
+
if step_call?(node)
|
|
51
|
+
step_name = node.arguments.first&.value
|
|
52
|
+
@step_names ||= []
|
|
53
|
+
@step_names << step_name if step_name
|
|
54
|
+
elsif private_declaration?(node)
|
|
55
|
+
@private_section_started = true
|
|
56
|
+
elsif public_declaration?(node)
|
|
57
|
+
@private_section_started = false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_def(node)
|
|
62
|
+
return unless @step_names&.include?(node.method_name)
|
|
63
|
+
return if @private_section_started
|
|
64
|
+
|
|
65
|
+
@public_step_methods ||= []
|
|
66
|
+
@public_step_methods << node
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def after_class(_node)
|
|
70
|
+
return unless @public_step_methods&.any?
|
|
71
|
+
|
|
72
|
+
@public_step_methods.each do |node|
|
|
73
|
+
add_offense(node, message: format(MSG, name: node.method_name))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def step_call?(node)
|
|
80
|
+
node.send_type? &&
|
|
81
|
+
node.method_name == :step &&
|
|
82
|
+
node.receiver.nil? &&
|
|
83
|
+
node.arguments.first&.sym_type?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def private_declaration?(node)
|
|
87
|
+
node.send_type? &&
|
|
88
|
+
node.method_name == :private &&
|
|
89
|
+
node.receiver.nil? &&
|
|
90
|
+
node.arguments.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def public_declaration?(node)
|
|
94
|
+
node.send_type? &&
|
|
95
|
+
node.method_name == :public &&
|
|
96
|
+
node.receiver.nil? &&
|
|
97
|
+
node.arguments.empty?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Prevents direct instantiation of service classes with `.new`.
|
|
7
|
+
# Services should be called using `.run`, `.run!`, or `.call`.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# UserService.new(name: "John")
|
|
12
|
+
# User::Create.new(params: {})
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# UserService.run(name: "John")
|
|
16
|
+
# UserService.run!(name: "John")
|
|
17
|
+
# UserService.call(name: "John")
|
|
18
|
+
# User::Create.run(params: {})
|
|
19
|
+
#
|
|
20
|
+
# @example ServicePattern: 'Service$' (default)
|
|
21
|
+
# # Matches class names ending with "Service"
|
|
22
|
+
# UserService.new # offense
|
|
23
|
+
# UserCreator.new # no offense (doesn't match pattern)
|
|
24
|
+
#
|
|
25
|
+
# @example ServicePattern: '(Service|Creator)$'
|
|
26
|
+
# # Matches class names ending with "Service" or "Creator"
|
|
27
|
+
# UserService.new # offense
|
|
28
|
+
# UserCreator.new # offense
|
|
29
|
+
#
|
|
30
|
+
class NoDirectInstantiation < Base
|
|
31
|
+
MSG = "Use `.run`, `.run!`, or `.call` instead of `.new` for service classes."
|
|
32
|
+
|
|
33
|
+
RESTRICT_ON_SEND = [:new].freeze
|
|
34
|
+
|
|
35
|
+
def on_send(node)
|
|
36
|
+
return unless node.method_name == :new
|
|
37
|
+
return unless service_class?(node.receiver)
|
|
38
|
+
|
|
39
|
+
add_offense(node)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def service_class?(node)
|
|
45
|
+
return false unless node
|
|
46
|
+
|
|
47
|
+
class_name = extract_class_name(node)
|
|
48
|
+
return false unless class_name
|
|
49
|
+
|
|
50
|
+
pattern = cop_config.fetch("ServicePattern", "Service$")
|
|
51
|
+
class_name.match?(Regexp.new(pattern))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def extract_class_name(node)
|
|
55
|
+
case node.type
|
|
56
|
+
when :const
|
|
57
|
+
node.const_name
|
|
58
|
+
when :send
|
|
59
|
+
# For chained constants like User::Create
|
|
60
|
+
node.source
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Ensures that all `output` declarations in Light::Services include a `type:` option.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# output :result
|
|
11
|
+
# output :data, optional: true
|
|
12
|
+
# output :count, default: 0
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# output :result, type: Hash
|
|
16
|
+
# output :data, type: Hash, optional: true
|
|
17
|
+
# output :count, type: Integer, default: 0
|
|
18
|
+
#
|
|
19
|
+
class OutputTypeRequired < Base
|
|
20
|
+
MSG = "Output `%<name>s` must have a `type:` option."
|
|
21
|
+
|
|
22
|
+
RESTRICT_ON_SEND = [:output].freeze
|
|
23
|
+
|
|
24
|
+
# @!method output_call?(node)
|
|
25
|
+
def_node_matcher :output_call?, <<~PATTERN
|
|
26
|
+
(send nil? :output (sym $_) ...)
|
|
27
|
+
PATTERN
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
output_call?(node) do |name|
|
|
31
|
+
return if has_type_option?(node)
|
|
32
|
+
|
|
33
|
+
add_offense(node, message: format(MSG, name: name))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def has_type_option?(node)
|
|
40
|
+
# output :name (no options)
|
|
41
|
+
return false if node.arguments.size == 1
|
|
42
|
+
|
|
43
|
+
# output :name, type: Foo or output :name, { type: Foo }
|
|
44
|
+
opts_node = node.arguments[1]
|
|
45
|
+
return false unless opts_node&.hash_type?
|
|
46
|
+
|
|
47
|
+
opts_node.pairs.any? { |pair| pair.key.sym_type? && pair.key.value == :type }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Ensures that all `step` declarations have a corresponding method defined in the same class.
|
|
7
|
+
#
|
|
8
|
+
# Note: This cop only checks for methods defined in the same file/class. It cannot detect
|
|
9
|
+
# methods inherited from parent classes. Use the `ExcludedSteps` option or `rubocop:disable`
|
|
10
|
+
# comments for inherited steps.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# class MyService < ApplicationService
|
|
15
|
+
# step :validate
|
|
16
|
+
# step :process
|
|
17
|
+
#
|
|
18
|
+
# private
|
|
19
|
+
#
|
|
20
|
+
# def validate
|
|
21
|
+
# # only validate is defined, process is missing
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
# class MyService < ApplicationService
|
|
27
|
+
# step :validate
|
|
28
|
+
# step :process
|
|
29
|
+
#
|
|
30
|
+
# private
|
|
31
|
+
#
|
|
32
|
+
# def validate
|
|
33
|
+
# # validation logic
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def process
|
|
37
|
+
# # processing logic
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @example ExcludedSteps: ['initialize_entity', 'assign_attributes'] (default: [])
|
|
42
|
+
# # good - these steps are excluded from checking
|
|
43
|
+
# class User::Create < CreateService
|
|
44
|
+
# step :initialize_entity
|
|
45
|
+
# step :assign_attributes
|
|
46
|
+
# step :send_welcome_email
|
|
47
|
+
#
|
|
48
|
+
# private
|
|
49
|
+
#
|
|
50
|
+
# def send_welcome_email
|
|
51
|
+
# # only this method needs to be defined
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
class StepMethodExists < Base
|
|
56
|
+
MSG = "Step `%<name>s` has no corresponding method. " \
|
|
57
|
+
"For inherited steps, disable this line or add to ExcludedSteps."
|
|
58
|
+
|
|
59
|
+
def on_class(_node)
|
|
60
|
+
@step_calls = []
|
|
61
|
+
@defined_methods = []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_send(node)
|
|
65
|
+
return unless step_call?(node)
|
|
66
|
+
|
|
67
|
+
step_name = node.arguments.first&.value
|
|
68
|
+
return unless step_name
|
|
69
|
+
|
|
70
|
+
@step_calls ||= []
|
|
71
|
+
@step_calls << { name: step_name, node: node }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def on_def(node)
|
|
75
|
+
@defined_methods ||= []
|
|
76
|
+
@defined_methods << node.method_name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def after_class(_node)
|
|
80
|
+
return unless @step_calls&.any?
|
|
81
|
+
|
|
82
|
+
@step_calls.each do |step|
|
|
83
|
+
next if @defined_methods&.include?(step[:name])
|
|
84
|
+
next if excluded_step?(step[:name])
|
|
85
|
+
|
|
86
|
+
add_offense(step[:node], message: format(MSG, name: step[:name]))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def step_call?(node)
|
|
93
|
+
node.send_type? &&
|
|
94
|
+
node.method_name == :step &&
|
|
95
|
+
node.receiver.nil? &&
|
|
96
|
+
node.arguments.first&.sym_type?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def excluded_step?(step_name)
|
|
100
|
+
excluded_steps.include?(step_name.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def excluded_steps
|
|
104
|
+
cop_config.fetch("ExcludedSteps", [])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
require_relative "rubocop/cop/light_services/argument_type_required"
|
|
6
|
+
require_relative "rubocop/cop/light_services/condition_method_exists"
|
|
7
|
+
require_relative "rubocop/cop/light_services/deprecated_methods"
|
|
8
|
+
require_relative "rubocop/cop/light_services/dsl_order"
|
|
9
|
+
require_relative "rubocop/cop/light_services/missing_private_keyword"
|
|
10
|
+
require_relative "rubocop/cop/light_services/no_direct_instantiation"
|
|
11
|
+
require_relative "rubocop/cop/light_services/output_type_required"
|
|
12
|
+
require_relative "rubocop/cop/light_services/step_method_exists"
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Unified settings class for arguments and outputs
|
|
4
3
|
module Light
|
|
5
4
|
module Services
|
|
6
5
|
module Settings
|
|
6
|
+
# Stores configuration for a single argument or output field.
|
|
7
|
+
# Created automatically when using the `arg` or `output` DSL methods.
|
|
7
8
|
class Field
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
# @return [Symbol] the field name
|
|
10
|
+
attr_reader :name
|
|
11
|
+
|
|
12
|
+
# @return [Boolean] true if a default value was specified
|
|
13
|
+
attr_reader :default_exists
|
|
14
|
+
|
|
15
|
+
# @return [Object, Proc, nil] the default value or proc
|
|
16
|
+
attr_reader :default
|
|
17
|
+
|
|
18
|
+
# @return [Boolean, nil] true if this is a context argument
|
|
19
|
+
attr_reader :context
|
|
20
|
+
|
|
21
|
+
# @return [Boolean, nil] true if nil values are allowed
|
|
22
|
+
attr_reader :optional
|
|
23
|
+
|
|
24
|
+
# Initialize a new field definition.
|
|
25
|
+
#
|
|
26
|
+
# @param name [Symbol] the field name
|
|
27
|
+
# @param service_class [Class] the service class this field belongs to
|
|
28
|
+
# @param opts [Hash] field options
|
|
29
|
+
# @option opts [Class, Array<Class>] :type type(s) to validate against
|
|
30
|
+
# @option opts [Boolean] :optional whether nil is allowed
|
|
31
|
+
# @option opts [Object, Proc] :default default value or proc
|
|
32
|
+
# @option opts [Boolean] :context whether to pass to child services
|
|
33
|
+
# @option opts [Symbol] :field_type :argument or :output
|
|
10
34
|
def initialize(name, service_class, opts = {})
|
|
11
35
|
@name = name
|
|
12
36
|
@service_class = service_class
|
|
@@ -21,8 +45,12 @@ module Light
|
|
|
21
45
|
define_methods
|
|
22
46
|
end
|
|
23
47
|
|
|
24
|
-
# Validate
|
|
25
|
-
#
|
|
48
|
+
# Validate a value against the field's type definition.
|
|
49
|
+
# Supports both Ruby class types and dry-types.
|
|
50
|
+
#
|
|
51
|
+
# @param value [Object] the value to validate
|
|
52
|
+
# @return [Object] the value (possibly coerced by dry-types)
|
|
53
|
+
# @raise [ArgTypeError] if the value doesn't match the expected type
|
|
26
54
|
def validate_type!(value)
|
|
27
55
|
return value unless @type
|
|
28
56
|
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This class defines settings for step
|
|
4
3
|
module Light
|
|
5
4
|
module Services
|
|
6
5
|
module Settings
|
|
6
|
+
# Stores configuration for a single service step.
|
|
7
|
+
# Created automatically when using the `step` DSL method.
|
|
7
8
|
class Step
|
|
8
|
-
#
|
|
9
|
-
attr_reader :name
|
|
10
|
-
|
|
9
|
+
# @return [Symbol] the step name (method to call)
|
|
10
|
+
attr_reader :name
|
|
11
|
+
|
|
12
|
+
# @return [Boolean, nil] true if step runs even after errors/warnings
|
|
13
|
+
attr_reader :always
|
|
14
|
+
|
|
15
|
+
# Initialize a new step definition.
|
|
16
|
+
#
|
|
17
|
+
# @param name [Symbol] the step name (must match a method)
|
|
18
|
+
# @param service_class [Class] the service class this step belongs to
|
|
19
|
+
# @param opts [Hash] step options
|
|
20
|
+
# @option opts [Symbol, Proc] :if condition to run the step
|
|
21
|
+
# @option opts [Symbol, Proc] :unless condition to skip the step
|
|
22
|
+
# @option opts [Boolean] :always run even after errors/warnings
|
|
23
|
+
# @raise [Error] if both :if and :unless are specified
|
|
11
24
|
def initialize(name, service_class, opts = {})
|
|
12
25
|
@name = name
|
|
13
26
|
@service_class = service_class
|
|
@@ -22,6 +35,11 @@ module Light
|
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
# Execute the step on the given service instance.
|
|
39
|
+
#
|
|
40
|
+
# @param instance [Base] the service instance
|
|
41
|
+
# @return [Boolean] true if the step was executed, false if skipped
|
|
42
|
+
# @raise [Error] if the step method is not defined
|
|
25
43
|
def run(instance) # rubocop:disable Naming/PredicateMethod
|
|
26
44
|
return false unless run?(instance)
|
|
27
45
|
|
|
@@ -60,7 +78,7 @@ module Light
|
|
|
60
78
|
end
|
|
61
79
|
|
|
62
80
|
def run?(instance)
|
|
63
|
-
return false if instance.
|
|
81
|
+
return false if instance.stopped?
|
|
64
82
|
|
|
65
83
|
if @if
|
|
66
84
|
check_condition(@if, instance)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "indexing_enhancement"
|
|
4
|
+
require_relative "definition"
|
|
5
|
+
|
|
6
|
+
# Declare version compatibility without runtime dependency on ruby-lsp
|
|
7
|
+
RubyLsp::Addon.depend_on_ruby_lsp!("~> 0.26")
|
|
8
|
+
|
|
9
|
+
module RubyLsp
|
|
10
|
+
module LightServices
|
|
11
|
+
class Addon < ::RubyLsp::Addon
|
|
12
|
+
def activate(global_state, message_queue)
|
|
13
|
+
@global_state = global_state
|
|
14
|
+
@message_queue = message_queue
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deactivate; end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
"Ruby LSP Light Services"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def version
|
|
24
|
+
Light::Services::VERSION
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Called on every "go to definition" request
|
|
28
|
+
# Provides navigation from step DSL symbols to their method definitions
|
|
29
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
30
|
+
return unless @global_state
|
|
31
|
+
|
|
32
|
+
Definition.new(response_builder, uri, node_context, @global_state.index, dispatcher)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|