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
|
@@ -126,6 +126,36 @@ module Light
|
|
|
126
126
|
# Check inherited steps
|
|
127
127
|
(service_class.superclass.respond_to?(:steps) && service_class.superclass.steps.key?(name_sym))
|
|
128
128
|
end
|
|
129
|
+
|
|
130
|
+
# Validate that the type option is provided when require_type is enabled
|
|
131
|
+
#
|
|
132
|
+
# @param name [Symbol] the field name
|
|
133
|
+
# @param field_type [Symbol] the type of field (:argument, :output)
|
|
134
|
+
# @param service_class [Class] the service class for error messages
|
|
135
|
+
# @param opts [Hash] the options hash to check for type
|
|
136
|
+
def self.validate_type_required!(name, field_type, service_class, opts)
|
|
137
|
+
return if opts.key?(:type)
|
|
138
|
+
return unless require_type_enabled?(service_class)
|
|
139
|
+
|
|
140
|
+
raise Light::Services::MissingTypeError,
|
|
141
|
+
"#{field_type.to_s.capitalize} `#{name}` in #{service_class} must have a type specified " \
|
|
142
|
+
"(require_type is enabled)"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Check if require_type is enabled for the service class
|
|
146
|
+
def self.require_type_enabled?(service_class)
|
|
147
|
+
# Check class-level config in the inheritance chain, then fall back to global config
|
|
148
|
+
klass = service_class
|
|
149
|
+
while klass.respond_to?(:class_config)
|
|
150
|
+
class_config = klass.class_config
|
|
151
|
+
|
|
152
|
+
return class_config[:require_type] if class_config&.key?(:require_type)
|
|
153
|
+
|
|
154
|
+
klass = klass.superclass
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Light::Services.config.require_type
|
|
158
|
+
end
|
|
129
159
|
end
|
|
130
160
|
end
|
|
131
161
|
end
|
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Light
|
|
4
4
|
module Services
|
|
5
|
+
# Base exception class for all Light::Services errors.
|
|
5
6
|
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when an argument or output value doesn't match the expected type.
|
|
6
9
|
class ArgTypeError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when using a reserved name for an argument, output, or step.
|
|
7
12
|
class ReservedNameError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when a name is invalid (e.g., not a Symbol).
|
|
8
15
|
class InvalidNameError < Error; end
|
|
16
|
+
|
|
17
|
+
# Raised when a service has no steps defined and no run method.
|
|
9
18
|
class NoStepsError < Error; end
|
|
10
19
|
|
|
11
|
-
#
|
|
20
|
+
# Raised when type is required but not specified for an argument or output.
|
|
21
|
+
class MissingTypeError < Error; end
|
|
22
|
+
|
|
23
|
+
# Control flow exception for stop_immediately!
|
|
24
|
+
# Not an error - used to halt execution gracefully.
|
|
25
|
+
class StopExecution < StandardError; end
|
|
26
|
+
|
|
27
|
+
# @deprecated Use {Error} instead
|
|
12
28
|
NoStepError = Error
|
|
29
|
+
|
|
30
|
+
# @deprecated Use {Error} instead
|
|
13
31
|
TwoConditions = Error
|
|
14
32
|
end
|
|
15
33
|
end
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This class stores errors and warnings
|
|
4
3
|
module Light
|
|
5
4
|
module Services
|
|
5
|
+
# Represents a single error or warning message.
|
|
6
|
+
#
|
|
7
|
+
# @example Creating a message
|
|
8
|
+
# message = Message.new(:name, "can't be blank", break: true)
|
|
9
|
+
# message.key # => :name
|
|
10
|
+
# message.text # => "can't be blank"
|
|
11
|
+
# message.break? # => true
|
|
6
12
|
class Message
|
|
7
|
-
#
|
|
8
|
-
attr_reader :key
|
|
13
|
+
# @return [Symbol] the key/field this message belongs to
|
|
14
|
+
attr_reader :key
|
|
9
15
|
|
|
16
|
+
# @return [String] the message text
|
|
17
|
+
attr_reader :text
|
|
18
|
+
|
|
19
|
+
# Create a new message.
|
|
20
|
+
#
|
|
21
|
+
# @param key [Symbol] the key/field this message belongs to
|
|
22
|
+
# @param text [String] the message text
|
|
23
|
+
# @param opts [Hash] additional options
|
|
24
|
+
# @option opts [Boolean] :break whether to stop step execution
|
|
25
|
+
# @option opts [Boolean] :rollback whether to rollback the transaction
|
|
10
26
|
def initialize(key, text, opts = {})
|
|
11
27
|
@key = key
|
|
12
28
|
@text = text
|
|
13
29
|
@opts = opts
|
|
14
30
|
end
|
|
15
31
|
|
|
32
|
+
# Check if this message should stop step execution.
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean] true if break option was set
|
|
16
35
|
def break?
|
|
17
36
|
@opts[:break]
|
|
18
37
|
end
|
|
19
38
|
|
|
39
|
+
# Check if this message should trigger a transaction rollback.
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean] true if rollback option was set
|
|
20
42
|
def rollback?
|
|
21
43
|
@opts[:rollback]
|
|
22
44
|
end
|
|
23
45
|
|
|
46
|
+
# Return the message text.
|
|
47
|
+
#
|
|
48
|
+
# @return [String] the message text
|
|
24
49
|
def to_s
|
|
25
50
|
text
|
|
26
51
|
end
|
|
@@ -1,25 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This class stores errors and warnings
|
|
4
3
|
module Light
|
|
5
4
|
module Services
|
|
5
|
+
# Collection of error or warning messages, organized by key.
|
|
6
|
+
#
|
|
7
|
+
# @example Adding and accessing errors
|
|
8
|
+
# errors.add(:name, "can't be blank")
|
|
9
|
+
# errors.add(:email, "is invalid")
|
|
10
|
+
# errors[:name] # => [#<Message key: :name, text: "can't be blank">]
|
|
11
|
+
# errors.to_h # => { name: ["can't be blank"], email: ["is invalid"] }
|
|
6
12
|
class Messages
|
|
7
13
|
extend Forwardable
|
|
8
14
|
|
|
15
|
+
# @!method [](key)
|
|
16
|
+
# Get messages for a specific key.
|
|
17
|
+
# @param key [Symbol] the key to look up
|
|
18
|
+
# @return [Array<Message>, nil] array of messages or nil
|
|
19
|
+
|
|
20
|
+
# @!method any?
|
|
21
|
+
# Check if there are any messages.
|
|
22
|
+
# @return [Boolean] true if messages exist
|
|
23
|
+
|
|
24
|
+
# @!method empty?
|
|
25
|
+
# Check if the collection is empty.
|
|
26
|
+
# @return [Boolean] true if no messages
|
|
27
|
+
|
|
28
|
+
# @!method size
|
|
29
|
+
# Get number of keys with messages.
|
|
30
|
+
# @return [Integer] number of keys
|
|
31
|
+
|
|
32
|
+
# @!method keys
|
|
33
|
+
# Get all keys with messages.
|
|
34
|
+
# @return [Array<Symbol>] array of keys
|
|
35
|
+
|
|
36
|
+
# @!method key?(key)
|
|
37
|
+
# Check if a key has messages.
|
|
38
|
+
# @param key [Symbol] the key to check
|
|
39
|
+
# @return [Boolean] true if key has messages
|
|
9
40
|
def_delegators :@messages, :[], :any?, :empty?, :size, :keys, :values, :each, :each_with_index, :key?
|
|
10
41
|
alias has_key? key?
|
|
11
42
|
|
|
43
|
+
# Initialize a new messages collection.
|
|
44
|
+
#
|
|
45
|
+
# @param config [Hash] configuration options
|
|
46
|
+
# @option config [Boolean] :break_on_add stop execution when message added
|
|
47
|
+
# @option config [Boolean] :raise_on_add raise exception when message added
|
|
48
|
+
# @option config [Boolean] :rollback_on_add rollback transaction when message added
|
|
12
49
|
def initialize(config)
|
|
13
50
|
@break = false
|
|
14
51
|
@config = config
|
|
15
52
|
@messages = {}
|
|
16
53
|
end
|
|
17
54
|
|
|
18
|
-
#
|
|
55
|
+
# Get total count of all messages across all keys.
|
|
56
|
+
#
|
|
57
|
+
# @return [Integer] total number of messages
|
|
19
58
|
def count
|
|
20
59
|
@messages.values.sum(&:size)
|
|
21
60
|
end
|
|
22
61
|
|
|
62
|
+
# Add a message to the collection.
|
|
63
|
+
#
|
|
64
|
+
# @param key [Symbol] the key/field for this message
|
|
65
|
+
# @param texts [String, Array<String>, Message] the message text(s) to add
|
|
66
|
+
# @param opts [Hash] additional options
|
|
67
|
+
# @option opts [Boolean] :break override break behavior for this message
|
|
68
|
+
# @option opts [Boolean] :rollback override rollback behavior for this message
|
|
69
|
+
# @return [void]
|
|
70
|
+
# @raise [Error] if text is nil or empty
|
|
71
|
+
#
|
|
72
|
+
# @example Add a single error
|
|
73
|
+
# errors.add(:name, "can't be blank")
|
|
74
|
+
#
|
|
75
|
+
# @example Add multiple errors
|
|
76
|
+
# errors.add(:email, ["is invalid", "is already taken"])
|
|
23
77
|
def add(key, texts, opts = {})
|
|
24
78
|
raise Light::Services::Error, "Error must be a non-empty string" unless texts
|
|
25
79
|
|
|
@@ -39,10 +93,25 @@ module Light
|
|
|
39
93
|
rollback!(opts.key?(:rollback) ? opts[:rollback] : message.rollback?) if !opts.key?(:last) || opts[:last]
|
|
40
94
|
end
|
|
41
95
|
|
|
96
|
+
# Check if step execution should stop.
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] true if a message triggered a break
|
|
42
99
|
def break?
|
|
43
100
|
@break
|
|
44
101
|
end
|
|
45
102
|
|
|
103
|
+
# Copy messages from another source.
|
|
104
|
+
#
|
|
105
|
+
# @param entity [ActiveRecord::Base, Base, Hash, #each] source to copy from
|
|
106
|
+
# @param opts [Hash] options to pass to each added message
|
|
107
|
+
# @return [void]
|
|
108
|
+
# @raise [Error] if entity type is not supported
|
|
109
|
+
#
|
|
110
|
+
# @example Copy from ActiveRecord model
|
|
111
|
+
# errors.copy_from(user) # copies user.errors
|
|
112
|
+
#
|
|
113
|
+
# @example Copy from another service
|
|
114
|
+
# errors.copy_from(child_service)
|
|
46
115
|
def copy_from(entity, opts = {})
|
|
47
116
|
if defined?(ActiveRecord::Base) && entity.is_a?(ActiveRecord::Base)
|
|
48
117
|
copy_from(entity.errors.messages, opts)
|
|
@@ -60,6 +129,9 @@ module Light
|
|
|
60
129
|
end
|
|
61
130
|
alias from_record copy_from
|
|
62
131
|
|
|
132
|
+
# Convert messages to a hash with string values.
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash{Symbol => Array<String>}] messages as hash
|
|
63
135
|
def to_h
|
|
64
136
|
@messages.to_h.transform_values { |value| value.map(&:to_s) }
|
|
65
137
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Ensures that all `arg` declarations in Light::Services include a `type:` option.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# arg :user_id
|
|
11
|
+
# arg :params, default: {}
|
|
12
|
+
# arg :name, optional: true
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# arg :user_id, type: Integer
|
|
16
|
+
# arg :params, type: Hash, default: {}
|
|
17
|
+
# arg :name, type: String, optional: true
|
|
18
|
+
#
|
|
19
|
+
class ArgumentTypeRequired < Base
|
|
20
|
+
MSG = "Argument `%<name>s` must have a `type:` option."
|
|
21
|
+
|
|
22
|
+
RESTRICT_ON_SEND = [:arg].freeze
|
|
23
|
+
|
|
24
|
+
# @!method arg_call?(node)
|
|
25
|
+
def_node_matcher :arg_call?, <<~PATTERN
|
|
26
|
+
(send nil? :arg (sym $_) ...)
|
|
27
|
+
PATTERN
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
arg_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
|
+
# arg :name (no options)
|
|
41
|
+
return false if node.arguments.size == 1
|
|
42
|
+
|
|
43
|
+
# arg :name, type: Foo or arg :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,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Ensures that symbol conditions in `step` declarations (`:if` and `:unless`)
|
|
7
|
+
# have corresponding methods defined in the same class.
|
|
8
|
+
#
|
|
9
|
+
# This cop automatically recognizes:
|
|
10
|
+
# - Methods defined with `def`
|
|
11
|
+
# - Predicate methods from `arg` and `output` (e.g., `arg :user` creates `user` and `user?`)
|
|
12
|
+
# - Methods from `attr_reader`, `attr_accessor`, and `attr_writer`
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# class MyService < ApplicationService
|
|
17
|
+
# step :process, if: :should_process?
|
|
18
|
+
#
|
|
19
|
+
# private
|
|
20
|
+
#
|
|
21
|
+
# def process
|
|
22
|
+
# # should_process? is missing
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # good - explicit method
|
|
27
|
+
# class MyService < ApplicationService
|
|
28
|
+
# step :process, if: :should_process?
|
|
29
|
+
#
|
|
30
|
+
# private
|
|
31
|
+
#
|
|
32
|
+
# def process; end
|
|
33
|
+
# def should_process?; true; end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# # good - predicate from arg
|
|
37
|
+
# class MyService < ApplicationService
|
|
38
|
+
# arg :user, type: User, optional: true
|
|
39
|
+
#
|
|
40
|
+
# step :greet, if: :user? # user? is auto-generated by arg :user
|
|
41
|
+
#
|
|
42
|
+
# private
|
|
43
|
+
#
|
|
44
|
+
# def greet; end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# # good - attr_reader
|
|
48
|
+
# class MyService < ApplicationService
|
|
49
|
+
# attr_reader :enabled
|
|
50
|
+
#
|
|
51
|
+
# step :process, if: :enabled
|
|
52
|
+
#
|
|
53
|
+
# private
|
|
54
|
+
#
|
|
55
|
+
# def process; end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# @example ExcludedMethods: ['admin?', 'guest?'] (default: [])
|
|
59
|
+
# # good - these methods are excluded from checking
|
|
60
|
+
# class MyService < ApplicationService
|
|
61
|
+
# step :admin_action, if: :admin? # excluded via config
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
class ConditionMethodExists < Base
|
|
65
|
+
MSG = "Condition method `%<name>s` has no corresponding method. " \
|
|
66
|
+
"For inherited methods, disable this line or add to ExcludedMethods."
|
|
67
|
+
|
|
68
|
+
CONDITION_KEYS = [:if, :unless].freeze
|
|
69
|
+
ATTR_METHODS = [:attr_reader, :attr_accessor, :attr_writer].freeze
|
|
70
|
+
|
|
71
|
+
def on_class(_node)
|
|
72
|
+
@condition_methods = []
|
|
73
|
+
@defined_methods = []
|
|
74
|
+
@dsl_predicates = []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_send(node)
|
|
78
|
+
if step_call?(node)
|
|
79
|
+
collect_condition_methods(node)
|
|
80
|
+
elsif arg_or_output_call?(node)
|
|
81
|
+
collect_dsl_predicate(node)
|
|
82
|
+
elsif attr_method_call?(node)
|
|
83
|
+
collect_attr_methods(node)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def on_def(node)
|
|
88
|
+
@defined_methods ||= []
|
|
89
|
+
@defined_methods << node.method_name
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def after_class(_node)
|
|
93
|
+
return unless @condition_methods&.any?
|
|
94
|
+
|
|
95
|
+
@condition_methods.each do |condition|
|
|
96
|
+
next if method_available?(condition[:name])
|
|
97
|
+
next if excluded_method?(condition[:name])
|
|
98
|
+
|
|
99
|
+
add_offense(condition[:node], message: format(MSG, name: condition[:name]))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def step_call?(node)
|
|
106
|
+
node.send_type? &&
|
|
107
|
+
node.method_name == :step &&
|
|
108
|
+
node.receiver.nil? &&
|
|
109
|
+
node.arguments.first&.sym_type?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def arg_or_output_call?(node)
|
|
113
|
+
node.send_type? &&
|
|
114
|
+
[:arg, :output].include?(node.method_name) &&
|
|
115
|
+
node.receiver.nil? &&
|
|
116
|
+
node.arguments.first&.sym_type?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def attr_method_call?(node)
|
|
120
|
+
node.send_type? &&
|
|
121
|
+
ATTR_METHODS.include?(node.method_name) &&
|
|
122
|
+
node.receiver.nil?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def collect_condition_methods(node)
|
|
126
|
+
@condition_methods ||= []
|
|
127
|
+
|
|
128
|
+
opts_node = node.arguments[1]
|
|
129
|
+
return unless opts_node&.hash_type?
|
|
130
|
+
|
|
131
|
+
opts_node.pairs.each do |pair|
|
|
132
|
+
key = pair.key
|
|
133
|
+
value = pair.value
|
|
134
|
+
|
|
135
|
+
next unless key.sym_type? && CONDITION_KEYS.include?(key.value)
|
|
136
|
+
next unless value.sym_type?
|
|
137
|
+
|
|
138
|
+
@condition_methods << { name: value.value, node: value }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def collect_dsl_predicate(node)
|
|
143
|
+
@dsl_predicates ||= []
|
|
144
|
+
|
|
145
|
+
field_name = node.arguments.first.value
|
|
146
|
+
@dsl_predicates += [field_name, :"#{field_name}?"]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def collect_attr_methods(node)
|
|
150
|
+
@defined_methods ||= []
|
|
151
|
+
|
|
152
|
+
node.arguments.each do |arg|
|
|
153
|
+
next unless arg.sym_type?
|
|
154
|
+
|
|
155
|
+
@defined_methods << arg.value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def method_available?(method_name)
|
|
160
|
+
@defined_methods&.include?(method_name) || @dsl_predicates&.include?(method_name)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def excluded_method?(method_name)
|
|
164
|
+
excluded_methods.include?(method_name.to_s)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def excluded_methods
|
|
168
|
+
cop_config.fetch("ExcludedMethods", [])
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Detects deprecated `done!` and `done?` method calls and suggests
|
|
7
|
+
# using `stop!` and `stopped?` instead.
|
|
8
|
+
#
|
|
9
|
+
# This cop checks calls inside service classes that inherit from
|
|
10
|
+
# Light::Services::Base or any configured base service classes.
|
|
11
|
+
#
|
|
12
|
+
# @safety
|
|
13
|
+
# This cop's autocorrection is safe as `done!` and `done?` are
|
|
14
|
+
# direct aliases for `stop!` and `stopped?`.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# class User::Create < ApplicationService
|
|
19
|
+
# step :process
|
|
20
|
+
#
|
|
21
|
+
# private
|
|
22
|
+
#
|
|
23
|
+
# def process
|
|
24
|
+
# done! if condition_met?
|
|
25
|
+
# return if done?
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# class User::Create < ApplicationService
|
|
31
|
+
# step :process
|
|
32
|
+
#
|
|
33
|
+
# private
|
|
34
|
+
#
|
|
35
|
+
# def process
|
|
36
|
+
# stop! if condition_met?
|
|
37
|
+
# return if stopped?
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
class DeprecatedMethods < Base
|
|
42
|
+
extend AutoCorrector
|
|
43
|
+
|
|
44
|
+
MSG_DONE_BANG = "Use `stop!` instead of deprecated `done!`."
|
|
45
|
+
MSG_DONE_QUERY = "Use `stopped?` instead of deprecated `done?`."
|
|
46
|
+
|
|
47
|
+
RESTRICT_ON_SEND = [:done!, :done?].freeze
|
|
48
|
+
|
|
49
|
+
REPLACEMENTS = {
|
|
50
|
+
done!: :stop!,
|
|
51
|
+
done?: :stopped?,
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
DEFAULT_BASE_CLASSES = ["ApplicationService"].freeze
|
|
55
|
+
|
|
56
|
+
def on_class(node)
|
|
57
|
+
@in_service_class = service_class?(node)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def after_class(_node)
|
|
61
|
+
@in_service_class = false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_send(node)
|
|
65
|
+
return unless @in_service_class
|
|
66
|
+
return unless RESTRICT_ON_SEND.include?(node.method_name)
|
|
67
|
+
return if node.receiver && !self_receiver?(node)
|
|
68
|
+
|
|
69
|
+
message = node.method_name == :done! ? MSG_DONE_BANG : MSG_DONE_QUERY
|
|
70
|
+
replacement = REPLACEMENTS[node.method_name]
|
|
71
|
+
|
|
72
|
+
add_offense(node, message: message) do |corrector|
|
|
73
|
+
if node.receiver
|
|
74
|
+
corrector.replace(node, "self.#{replacement}")
|
|
75
|
+
else
|
|
76
|
+
corrector.replace(node, replacement.to_s)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def service_class?(node)
|
|
84
|
+
return false unless node.parent_class
|
|
85
|
+
|
|
86
|
+
parent_class_name = extract_class_name(node.parent_class)
|
|
87
|
+
return false unless parent_class_name
|
|
88
|
+
|
|
89
|
+
# Check for direct Light::Services::Base inheritance
|
|
90
|
+
return true if parent_class_name == "Light::Services::Base"
|
|
91
|
+
|
|
92
|
+
# Check against configured base service classes
|
|
93
|
+
base_classes = cop_config.fetch("BaseServiceClasses", DEFAULT_BASE_CLASSES)
|
|
94
|
+
base_classes.include?(parent_class_name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_class_name(node)
|
|
98
|
+
case node.type
|
|
99
|
+
when :const
|
|
100
|
+
node.const_name
|
|
101
|
+
when :send
|
|
102
|
+
# For namespaced constants like Light::Services::Base
|
|
103
|
+
node.source
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self_receiver?(node)
|
|
108
|
+
node.receiver&.self_type?
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|