light-services 2.2.1 → 3.1.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -4
  3. data/.github/workflows/ci.yml +12 -12
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +83 -7
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +84 -21
  11. data/docs/arguments.md +290 -0
  12. data/docs/best-practices.md +153 -0
  13. data/docs/callbacks.md +476 -0
  14. data/docs/concepts.md +80 -0
  15. data/docs/configuration.md +204 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +280 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +158 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +101 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/rubocop.md +285 -0
  26. data/docs/ruby-lsp.md +133 -0
  27. data/docs/service-rendering.md +222 -0
  28. data/docs/steps.md +391 -0
  29. data/docs/summary.md +21 -0
  30. data/docs/testing.md +549 -0
  31. data/lib/generators/light_services/install/USAGE +15 -0
  32. data/lib/generators/light_services/install/install_generator.rb +41 -0
  33. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  34. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  35. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  36. data/lib/generators/light_services/service/USAGE +21 -0
  37. data/lib/generators/light_services/service/service_generator.rb +68 -0
  38. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  39. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  40. data/lib/light/services/base.rb +134 -122
  41. data/lib/light/services/base_with_context.rb +23 -1
  42. data/lib/light/services/callbacks.rb +157 -0
  43. data/lib/light/services/collection.rb +145 -0
  44. data/lib/light/services/concerns/execution.rb +79 -0
  45. data/lib/light/services/concerns/parent_service.rb +34 -0
  46. data/lib/light/services/concerns/state_management.rb +30 -0
  47. data/lib/light/services/config.rb +82 -16
  48. data/lib/light/services/constants.rb +100 -0
  49. data/lib/light/services/dsl/arguments_dsl.rb +85 -0
  50. data/lib/light/services/dsl/outputs_dsl.rb +81 -0
  51. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  52. data/lib/light/services/dsl/validation.rb +162 -0
  53. data/lib/light/services/exceptions.rb +25 -2
  54. data/lib/light/services/message.rb +28 -3
  55. data/lib/light/services/messages.rb +92 -32
  56. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  57. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  58. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  59. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  60. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  61. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  62. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  63. data/lib/light/services/rspec.rb +15 -0
  64. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  65. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  66. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  67. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  68. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  69. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  70. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  71. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  72. data/lib/light/services/rubocop.rb +12 -0
  73. data/lib/light/services/settings/field.rb +114 -0
  74. data/lib/light/services/settings/step.rb +53 -20
  75. data/lib/light/services/utils.rb +38 -0
  76. data/lib/light/services/version.rb +1 -1
  77. data/lib/light/services.rb +2 -0
  78. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  79. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  80. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  81. data/light-services.gemspec +6 -8
  82. metadata +68 -26
  83. data/lib/light/services/class_based_collection/base.rb +0 -86
  84. data/lib/light/services/class_based_collection/mount.rb +0 -33
  85. data/lib/light/services/collection/arguments.rb +0 -34
  86. data/lib/light/services/collection/base.rb +0 -59
  87. data/lib/light/services/collection/outputs.rb +0 -16
  88. data/lib/light/services/settings/argument.rb +0 -68
  89. data/lib/light/services/settings/output.rb +0 -34
@@ -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"
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Light
4
+ module Services
5
+ module Settings
6
+ # Stores configuration for a single argument or output field.
7
+ # Created automatically when using the `arg` or `output` DSL methods.
8
+ class Field
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
34
+ def initialize(name, service_class, opts = {})
35
+ @name = name
36
+ @service_class = service_class
37
+ @field_type = opts.delete(:field_type) || :argument
38
+
39
+ @type = opts.delete(:type)
40
+ @context = opts.delete(:context)
41
+ @default_exists = opts.key?(:default)
42
+ @default = opts.delete(:default)
43
+ @optional = opts.delete(:optional)
44
+
45
+ define_methods
46
+ end
47
+
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
54
+ def validate_type!(value)
55
+ return value unless @type
56
+
57
+ if dry_type?(@type)
58
+ coerce_and_validate_dry_type!(value)
59
+ else
60
+ validate_ruby_type!(value)
61
+ value
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # Check if the type is a dry-types type
68
+ def dry_type?(type)
69
+ return false unless defined?(Dry::Types::Type)
70
+
71
+ type.is_a?(Dry::Types::Type)
72
+ end
73
+
74
+ # Validate and coerce value against dry-types
75
+ # Returns the coerced value
76
+ def coerce_and_validate_dry_type!(value)
77
+ @type[value]
78
+ rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => e
79
+ raise Light::Services::ArgTypeError,
80
+ "#{@service_class} #{@field_type} `#{@name}` #{e.message}"
81
+ end
82
+
83
+ # Validate value against Ruby class types
84
+ def validate_ruby_type!(value)
85
+ return if [*@type].any? { |type| value.is_a?(type) }
86
+
87
+ raise Light::Services::ArgTypeError, type_error_message(value)
88
+ end
89
+
90
+ def type_error_message(value)
91
+ expected_types = [*@type].map(&:to_s).join(" or ")
92
+ "#{@service_class} #{@field_type} `#{@name}` must be #{expected_types}, \" \\
93
+ \"but got #{value.class} with value: #{value.inspect}"
94
+ end
95
+
96
+ def define_methods
97
+ name = @name
98
+ collection_instance_var = :"@#{@field_type}s"
99
+
100
+ @service_class.define_method(@name) { instance_variable_get(collection_instance_var).get(name) }
101
+ @service_class.define_method(:"#{@name}?") { !!instance_variable_get(collection_instance_var).get(name) }
102
+ @service_class.define_method(:"#{@name}=") do |value|
103
+ instance_variable_get(collection_instance_var).set(name, value)
104
+ end
105
+ @service_class.send(:private, "#{@name}=")
106
+ end
107
+ end
108
+
109
+ # Aliases for backwards compatibility
110
+ Argument = Field
111
+ Output = Field
112
+ end
113
+ end
114
+ end
@@ -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
- # Getters
9
- attr_reader :name, :always
9
+ # @return [Symbol] the step name (method to call)
10
+ attr_reader :name
10
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
@@ -17,35 +30,55 @@ module Light
17
30
  @always = opts[:always]
18
31
 
19
32
  if @if && @unless
20
- raise Light::Services::TwoConditions, "#{service_class} `if` and `unless` cannot be specified " \
21
- "for the step `#{name}` at the same time"
33
+ raise Light::Services::Error, "#{service_class} `if` and `unless` cannot be specified " \
34
+ "for the step `#{name}` at the same time"
22
35
  end
23
36
  end
24
37
 
25
- def run(instance, benchmark: false)
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
43
+ def run(instance) # rubocop:disable Naming/PredicateMethod
26
44
  return false unless run?(instance)
27
45
 
28
- if instance.respond_to?(name, true)
29
- if benchmark
30
- time = Benchmark.ms do
31
- instance.send(name)
32
- end
46
+ unless instance.respond_to?(name, true)
47
+ available_steps = @service_class.steps.keys.join(", ")
48
+ raise Light::Services::Error,
49
+ "Step method `#{name}` is not defined in #{@service_class}. " \
50
+ "Defined steps: [#{available_steps}]"
51
+ end
33
52
 
34
- instance.log "⏱️ Step #{name} took #{time}ms"
35
- else
36
- instance.send(name)
37
- end
53
+ execute_with_callbacks(instance)
54
+ true
55
+ end
38
56
 
39
- true
57
+ private
58
+
59
+ def execute_with_callbacks(instance)
60
+ errors_count_before = instance.errors.count
61
+
62
+ instance.run_callbacks(:before_step_run, instance, name)
63
+
64
+ instance.run_callbacks(:around_step_run, instance, name) do
65
+ instance.send(name)
66
+ end
67
+
68
+ instance.run_callbacks(:after_step_run, instance, name)
69
+
70
+ if instance.errors.count > errors_count_before
71
+ instance.run_callbacks(:on_step_failure, instance, name)
40
72
  else
41
- raise Light::Services::NoStepError, "Cannot find step `#{name}` in service `#{@service_class}`"
73
+ instance.run_callbacks(:on_step_success, instance, name)
42
74
  end
75
+ rescue StandardError => e
76
+ instance.run_callbacks(:on_step_crash, instance, name, e)
77
+ raise e
43
78
  end
44
79
 
45
- private
46
-
47
80
  def run?(instance)
48
- return false if instance.done?
81
+ return false if instance.stopped?
49
82
 
50
83
  if @if
51
84
  check_condition(@if, instance)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Light
4
+ module Services
5
+ # Utility module providing helper methods for the Light Services library
6
+ module Utils
7
+ module_function
8
+
9
+ # Creates a deep copy of an object to prevent mutation of shared references.
10
+ #
11
+ # @param object [Object] the object to duplicate
12
+ # @return [Object] a deep copy of the object
13
+ #
14
+ # @example Deep duping a hash
15
+ # original = { a: { b: 1 } }
16
+ # copy = Utils.deep_dup(original)
17
+ # copy[:a][:b] = 2
18
+ # original[:a][:b] # => 1
19
+ #
20
+ # @example Deep duping an array
21
+ # original = [[1, 2], [3, 4]]
22
+ # copy = Utils.deep_dup(original)
23
+ # copy[0] << 5
24
+ # original[0] # => [1, 2]
25
+ #
26
+ def deep_dup(object)
27
+ # Use ActiveSupport's deep_dup if available (preferred for Rails apps)
28
+ return object.deep_dup if object.respond_to?(:deep_dup)
29
+
30
+ # Fallback to Marshal for objects that support serialization
31
+ Marshal.load(Marshal.dump(object))
32
+ rescue TypeError
33
+ # Last resort: use dup if available, otherwise return original
34
+ object.respond_to?(:dup) ? object.dup : object
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Light
4
4
  module Services
5
- VERSION = "2.2.1"
5
+ VERSION = "3.1.0"
6
6
  end
7
7
  end
@@ -3,6 +3,8 @@
3
3
  require "light/services/config"
4
4
  require "light/services/version"
5
5
  require "light/services/exceptions"
6
+ require "light/services/utils"
7
+ require "light/services/callbacks"
6
8
  require "light/services/base"
7
9
 
8
10
  module Light
@@ -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
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module LightServices
5
+ class Definition
6
+ # Condition options that reference methods
7
+ CONDITION_OPTIONS = [:if, :unless].freeze
8
+
9
+ def initialize(response_builder, uri, node_context, index, dispatcher)
10
+ @response_builder = response_builder
11
+ @uri = uri
12
+ @node_context = node_context
13
+ @index = index
14
+
15
+ # Register for symbol nodes - this is what gets triggered when user clicks on :method_name
16
+ dispatcher.register(self, :on_symbol_node_enter)
17
+ end
18
+
19
+ # Called when cursor is on a symbol node (e.g., :validate in `step :validate`)
20
+ def on_symbol_node_enter(node)
21
+ # Check if this symbol is part of a step call by examining the call context
22
+ call_node = find_parent_step_call
23
+ return unless call_node
24
+
25
+ method_name = determine_method_name(node, call_node)
26
+ return unless method_name
27
+
28
+ find_and_append_method_location(method_name)
29
+ end
30
+
31
+ private
32
+
33
+ # Find the parent step call node from the node context
34
+ # The node_context.call_node returns the enclosing call if cursor is on an argument
35
+ def find_parent_step_call
36
+ call_node = @node_context.call_node
37
+ return unless call_node
38
+ return unless call_node.name == :step
39
+
40
+ call_node
41
+ end
42
+
43
+ # Determine which method name to look up based on where the symbol appears
44
+ # Returns nil if this symbol is not a method reference we should handle
45
+ def determine_method_name(symbol_node, call_node)
46
+ symbol_value = symbol_node.value.to_sym
47
+
48
+ # Check if this is the first argument (step method name)
49
+ first_arg = call_node.arguments&.arguments&.first
50
+ if first_arg.is_a?(Prism::SymbolNode) && first_arg.value.to_sym == symbol_value && same_location?(first_arg,
51
+ symbol_node)
52
+ # Verify the symbol node location matches (same node, not just same value)
53
+ return symbol_value.to_s
54
+ end
55
+
56
+ # Check if this is a condition option (if: or unless:)
57
+ keyword_hash = find_keyword_hash(call_node)
58
+ return unless keyword_hash
59
+
60
+ CONDITION_OPTIONS.each do |option_name|
61
+ condition_symbol = find_symbol_option(keyword_hash, option_name)
62
+ next unless condition_symbol
63
+ next unless same_location?(condition_symbol, symbol_node)
64
+
65
+ return condition_symbol.value.to_s
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ # Check if two nodes have the same location (are the same node)
72
+ def same_location?(node1, node2)
73
+ node1.location.start_offset == node2.location.start_offset &&
74
+ node1.location.end_offset == node2.location.end_offset
75
+ end
76
+
77
+ # Find the keyword hash in call arguments
78
+ def find_keyword_hash(node)
79
+ node.arguments&.arguments&.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
80
+ end
81
+
82
+ # Find a symbol value for a specific option in the keyword hash
83
+ # Returns the SymbolNode if found and value is a symbol, nil otherwise
84
+ def find_symbol_option(keyword_hash, option_name)
85
+ element = keyword_hash.elements.find do |elem|
86
+ elem.is_a?(Prism::AssocNode) &&
87
+ elem.key.is_a?(Prism::SymbolNode) &&
88
+ elem.key.value.to_sym == option_name
89
+ end
90
+
91
+ return unless element
92
+ return unless element.value.is_a?(Prism::SymbolNode)
93
+
94
+ element.value
95
+ end
96
+
97
+ # Find method definition in index and append location to response
98
+ def find_and_append_method_location(method_name)
99
+ owner_name = @node_context.nesting.join("::")
100
+ return if owner_name.empty?
101
+
102
+ # Look up method entries in the index
103
+ method_entries = @index.resolve_method(method_name, owner_name)
104
+ return unless method_entries&.any?
105
+
106
+ method_entries.each { |entry| append_location(entry) }
107
+
108
+ true
109
+ end
110
+
111
+ def append_location(entry)
112
+ @response_builder << Interface::Location.new(
113
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
114
+ range: build_range(entry.location),
115
+ )
116
+ end
117
+
118
+ def build_range(location)
119
+ Interface::Range.new(
120
+ start: Interface::Position.new(
121
+ line: location.start_line - 1,
122
+ character: location.start_column,
123
+ ),
124
+ end: Interface::Position.new(
125
+ line: location.end_line - 1,
126
+ character: location.end_column,
127
+ ),
128
+ )
129
+ end
130
+ end
131
+ end
132
+ end