ruby-lsp-rails-factory-bot 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41ed188af5e2b4b289442274d6538f7105ce3d0e93dd7340923b7f752c7b042b
4
- data.tar.gz: a8b258fb2f0c05d17f614d3614515da65b29c04c95c17d7a39c2cd13bc1df261
3
+ metadata.gz: 28d0eed4aef05099e03470f4aae508ad687ce937db3d080289e5acf44d71b4a1
4
+ data.tar.gz: cfa4ac0edb00841d67a6052a90ecea3c0649420fbe7acc00dcf0d1df0aca8651
5
5
  SHA512:
6
- metadata.gz: dbcb7349c449d4d33076e65c04060ca179c150a204d9d6ce93b274bdd5f25357c98a32263a98092943f7845086d8117266394af00f086db21750bf4d83963f92
7
- data.tar.gz: a2053e5de42267b86d927e7e0aa3fd4ab99475d9d82843086d4d91b1bb08dfa3efccf1a28aa79fbfd17990ee8d5bb330f99418590a561e878003b728f8ba39be
6
+ metadata.gz: 69904e9e37c3a82cbc7f6c098d8c0c72fce15ae5e4c6a6cbf4a596d60b178be7e1577411776c191605ba0d359aef7c685dffc30ba756111f006b9b3873a44489
7
+ data.tar.gz: bbaf4f6cad8a9e394b423c6c49a029fcd768543473e1165676780813e588de3d5518020bd1f0c9b0d1a11cea7d2cd2502e18ad5b0b4e8b90da6700774e639032
data/.rubocop.yml CHANGED
@@ -1,8 +1,28 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ Exclude:
5
+ - '**/*.gemspec'
6
+ - '**/vendor/bundle/**/*'
7
+
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - 'spec/**/*_spec.rb'
11
+
12
+ Style/RedundantReturn:
13
+ AllowMultipleReturnValues: true
3
14
 
4
15
  Style/StringLiterals:
5
16
  EnforcedStyle: double_quotes
6
17
 
7
18
  Style/StringLiteralsInInterpolation:
8
19
  EnforcedStyle: double_quotes
20
+
21
+ Style/TrailingCommaInHashLiteral:
22
+ EnforcedStyleForMultiline: consistent_comma
23
+
24
+ Style/TrailingCommaInArrayLiteral:
25
+ EnforcedStyleForMultiline: consistent_comma
26
+
27
+ Style/TrailingCommaInArguments:
28
+ EnforcedStyleForMultiline: consistent_comma
data/Rakefile CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
5
6
 
6
7
  RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
7
9
 
8
- task default: :rubocop
10
+ task default: %i[rubocop spec]
@@ -1,37 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruby_lsp/addon"
4
- require "ruby_lsp_rails/runner_client"
4
+ require "ruby_lsp/ruby_lsp_rails/addon"
5
5
 
6
6
  require_relative "completion"
7
7
  require_relative "hover"
8
- require_relative "server_extension"
8
+ require_relative "addon_name"
9
+ require_relative "../factory_bot"
10
+ require_relative "../../../ruby_lsp_rails_factory_bot"
9
11
 
10
12
  module RubyLsp
11
13
  module Rails
12
14
  module FactoryBot
15
+ # The addon to be registered with ruby-lsp. See https://shopify.github.io/ruby-lsp/add-ons.html
13
16
  class Addon < ::RubyLsp::Addon
14
- def activate(global_state, *)
17
+ def activate(global_state, outgoing_queue)
15
18
  @ruby_index = global_state.index
19
+
20
+ @outgoing_queue = outgoing_queue
21
+ log "Activating #{name} add-on v#{VERSION}"
16
22
  end
17
23
 
18
24
  def deactivate(*); end
19
25
 
20
26
  def name
21
- "ruby-lsp-rails-factory-bot"
27
+ FactoryBot::ADDON_NAME
22
28
  end
23
29
 
24
- def create_completion_listener(response_builder, node_context, dispatcher, uri)
25
- path = uri.to_standardized_path
26
- return unless path&.end_with?("_test.rb") || path&.end_with?("_spec.rb")
27
- return unless factory_bot_call_args?(node_context)
30
+ # def create_completion_listener(response_builder, node_context, dispatcher, uri)
31
+ # path = uri.to_standardized_path
32
+ # return unless path&.end_with?("_test.rb") || path&.end_with?("_spec.rb")
33
+ # return unless factory_bot_call_args?(node_context)
28
34
 
29
- Completion.new(response_builder, node_context, dispatcher, RubyLsp::Rails::RunnerClient.instance)
30
- end
35
+ # ensure_addon_registered!
36
+
37
+ # Completion.new(response_builder, node_context, dispatcher, runner_client)
38
+ # end
31
39
 
40
+ # TODO: need URI param to be able to filter by file name
32
41
  def create_hover_listener(response_builder, node_context, dispatcher)
33
- # TODO: need URI param
34
- Hover.new(response_builder, node_context, dispatcher, RubyLsp::Rails::RunnerClient.instance, @ruby_index)
42
+ unless @addon_registered
43
+ register_addon!
44
+ return
45
+ end
46
+
47
+ Hover.new(response_builder, node_context, dispatcher, runner_client, @ruby_index)
35
48
  end
36
49
 
37
50
  def workspace_did_change_watched_files(changes)
@@ -39,11 +52,29 @@ module RubyLsp
39
52
  change[:uri].match?(/(?:spec|test).+factor.+\.rb/)
40
53
  end
41
54
 
42
- RubyLsp::Rails::RunnerClient.instance.trigger_reload
55
+ runner_client.trigger_reload
43
56
  end
44
57
 
45
58
  private
46
59
 
60
+ # the addon must be registered as a rails server addon once the server has booted
61
+ def register_addon!
62
+ @addon_registered ||= # rubocop:disable Naming/MemoizedInstanceVariableName
63
+ begin
64
+ addon_path = File.expand_path("server_addon.rb", __dir__)
65
+ runner_client.register_server_addon(addon_path)
66
+ true
67
+ end
68
+ end
69
+
70
+ def runner_client
71
+ @rails_addon ||= ::RubyLsp::Addon.get(
72
+ "Ruby LSP Rails",
73
+ ::RubyLsp::Rails::FactoryBot::REQUIRED_RUBY_LSP_RAILS_VERSION,
74
+ )
75
+ @rails_addon.rails_runner_client
76
+ end
77
+
47
78
  FACTORY_BOT_METHODS = %i[
48
79
  create
49
80
  build
@@ -55,6 +86,12 @@ module RubyLsp
55
86
  node_context.call_node && FACTORY_BOT_METHODS.include?(node_context.call_node.name)
56
87
  true
57
88
  end
89
+
90
+ def log(msg)
91
+ return if !@outgoing_queue || @outgoing_queue.closed?
92
+
93
+ @outgoing_queue << Notification.window_log_message(msg)
94
+ end
58
95
  end
59
96
  end
60
97
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ ADDON_NAME = "ruby-lsp-rails-factory-bot"
7
+ end
8
+ end
9
+ end
@@ -2,20 +2,19 @@
2
2
 
3
3
  require "ruby_lsp/internal"
4
4
 
5
+ require_relative "addon_name"
6
+
5
7
  module RubyLsp
6
8
  module Rails
7
9
  module FactoryBot
10
+ # The listener that is created when the user requests autocomplete at the relevant time.
11
+ #
12
+ # NOTE: autocompletion is only triggered on certain node types - almost exclusively call nodes
13
+ # and constants IIRC, so you cannot currently receive autocomplete options for symbols (eg.
14
+ # factory or trait names) :/
8
15
  class Completion
9
16
  include RubyLsp::Requests::Support::Common
10
17
 
11
- # TODO: Avoid duplication with other class
12
- FACTORY_BOT_METHODS = %i[
13
- create
14
- build
15
- build_stubbed
16
- attributes_for
17
- ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
18
-
19
18
  def initialize(response_builder, node_context, dispatcher, server_client)
20
19
  @response_builder = response_builder
21
20
  @node_context = node_context
@@ -25,103 +24,105 @@ module RubyLsp
25
24
  end
26
25
 
27
26
  def on_call_node_enter(node)
28
- return unless FACTORY_BOT_METHODS.include?(node.name) ||
29
- FACTORY_BOT_METHODS.include?(@node_context.parent.name)
27
+ return unless FactoryBot::FACTORY_BOT_METHODS.include?(node.name) ||
28
+ FactoryBot::FACTORY_BOT_METHODS.include?(@node_context.parent.name)
29
+
30
+ process_arguments_pattern(node, node.arguments)
31
+ rescue StandardError => e
32
+ $stderr.write(e, e.backtrace)
33
+ end
30
34
 
31
- case node.arguments
35
+ private
36
+
37
+ def process_arguments_pattern(node, arguments) # rubocop:disable Metrics/MethodLength
38
+ case arguments
32
39
  in [Prism::SymbolNode => factory_name_node]
33
- handle_factory(factory_name_node, factory_name_node.value.to_s)
40
+ handle_factory(factory_name_node, node_string_value(factory_name_node))
34
41
 
35
42
  in [Prism::SymbolNode => factory_name_node, *, Prism::SymbolNode => trait_node]
36
- handle_trait(factory_name_node.value.to_s, node, trait_node.value.to_s)
37
-
38
- in [Prism::SymbolNode => factory_name_node, *, Prism::CallNode => call_node]
39
- handle_attribute(factory_name_node.value.to_s, node, call_node.message)
40
-
41
- in [Prism::SymbolNode => factory_name_node, *, Prism::KeywordHashNode => kw_node]
42
- handle_attribute(
43
- factory_name_node.value.to_s, node, kw_node.elements.last.key.value&.to_s
44
- )
45
- in [Prism::SymbolNode => factory_name_node, *, Prism::HashNode => hash_node]
46
- handle_attribute(
47
- factory_name_node.value.to_s, node, hash_node.elements.last.key.value&.to_s
48
- )
43
+ handle_trait(node_string_value(factory_name_node), node, node_string_value(trait_node))
44
+
45
+ in [Prism::SymbolNode => _factory_name_node, *, Prism::KeywordHashNode => _kw_node] |
46
+ [Prism::SymbolNode => _factory_name_node, *, Prism::HashNode => _kw_node] |
47
+ [Prism::SymbolNode => _factory_name_node, *, Prism::CallNode => _call_node]
48
+
49
+ attr_name = _call_node ? _call_node.message : _kw_node.elements.last.key.value&.to_s
50
+ handle_attribute(node_string_value(_factory_name_node), node, attr_name)
49
51
  else
50
- $stderr.write node.arguments
51
52
  nil
52
53
  end
53
- rescue => e
54
- $stderr.write(e, e.backtrace)
55
54
  end
56
55
 
57
- private
56
+ def node_string_value(node)
57
+ node.value.to_s
58
+ end
58
59
 
59
60
  def handle_attribute(factory_name, node, value = "")
60
- @server_client.get_attributes(factory_name: factory_name, name: value)&.each do |attr|
61
- $stderr.write "attribute", attr
62
- label_details = Interface::CompletionItemLabelDetails.new(
63
- description: attr[:type],
64
- )
65
- range = range_from_node(node)
61
+ range = range_from_node(node)
62
+ make_request(:attributes, factory_name: factory_name, name: value)&.each do |attr|
63
+ label_details = Interface::CompletionItemLabelDetails.new(description: attr[:type])
66
64
 
67
- @response_builder << Interface::CompletionItem.new(
68
- label: attr[:name],
69
- filter_text: attr[:name],
70
- label_details: label_details,
71
- text_edit: Interface::TextEdit.new(range: range, new_text: attr[:name]),
72
- kind: Constant::CompletionItemKind::PROPERTY,
73
- data: {
74
- owner_name: attr[:owner],
75
- guessed_type: attr[:owner], # the type of the owner, not the attribute
76
- },
77
- )
65
+ @response_builder << serialise_attribute(attr[:name], label_details, attr[:owner], range)
78
66
  end
79
67
  end
80
68
 
69
+ def serialise_attribute(name, label_details, owner, range)
70
+ Interface::CompletionItem.new(
71
+ label: name,
72
+ filter_text: name,
73
+ label_details: label_details,
74
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
75
+ kind: Constant::CompletionItemKind::PROPERTY,
76
+ data: { owner_name: owner, guessed_type: owner }, # the type of the owner, not the attribute
77
+ )
78
+ end
79
+
81
80
  def handle_trait(factory_name, node, value = "")
82
- @server_client.get_traits(factory_name: factory_name, name: value)&.each do |tr|
83
- $stderr.write "trait", tr
84
- label_details = Interface::CompletionItemLabelDetails.new(
85
- description: tr[:owner],
86
- )
81
+ make_request(:traits, factory_name: factory_name, name: value)&.each do |tr|
82
+ label_details = Interface::CompletionItemLabelDetails.new(description: tr[:owner])
87
83
  range = range_from_node(node)
88
-
89
84
  name = tr[:name]
90
85
 
91
- @response_builder << Interface::CompletionItem.new(
92
- label: name,
93
- filter_text: name,
94
- label_details: label_details,
95
- text_edit: Interface::TextEdit.new(range: range, new_text: name),
96
- kind: Constant::CompletionItemKind::PROPERTY,
97
- data: {
98
- owner_name: nil,
99
- guessed_type: tr[:owner] # the type of the owner
100
- },
101
- )
86
+ @response_builder << serialise_trait(name, range, label_details)
102
87
  end
103
88
  end
104
89
 
90
+ def serialise_trait(name, range, label_details)
91
+ Interface::CompletionItem.new(
92
+ label: name,
93
+ filter_text: name,
94
+ label_details: label_details,
95
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
96
+ kind: Constant::CompletionItemKind::PROPERTY,
97
+ data: { owner_name: nil, guessed_type: tr[:owner] },
98
+ )
99
+ end
100
+
105
101
  def handle_factory(node, name)
106
- @server_client.get_factories(name: name)&.each do |fact|
107
- $stderr.write "factory", fact
108
- @response_builder << Interface::CompletionItem.new(
109
- label: fact[:name],
110
- filter_text: fact[:name],
111
- label_details: Interface::CompletionItemLabelDetails.new(
112
- description: fact[:model_class]
113
- ),
114
- text_edit: Interface::TextEdit.new(
115
- range: range_from_node(node),
116
- new_text: fact[:name],
117
- ),
118
- kind: Constant::CompletionItemKind::CLASS,
119
- data: {
120
- guessed_type: fact[:model_class]
121
- }
122
- )
102
+ range = range_from_node(node)
103
+ make_request(:factories, name: name)&.each do |fact|
104
+ @response_builder << serialise_factory(fact[:name], fact[:model_class], range)
123
105
  end
124
106
  end
107
+
108
+ def make_request(request_name, **params)
109
+ @server_client.delegate_request(
110
+ server_addon_name: FactoryBot::ADDON_NAME,
111
+ request_name: request_name.to_s,
112
+ **params,
113
+ )
114
+ end
115
+
116
+ def serialise_factory(name, model_class, range)
117
+ Interface::CompletionItem.new(
118
+ label: name,
119
+ filter_text: name,
120
+ label_details: Interface::CompletionItemLabelDetails.new(description: model_class),
121
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
122
+ kind: Constant::CompletionItemKind::CLASS,
123
+ data: { guessed_type: model_class },
124
+ )
125
+ end
125
126
  end
126
127
  end
127
128
  end
@@ -2,20 +2,15 @@
2
2
 
3
3
  require "ruby_lsp/internal"
4
4
 
5
+ require_relative "addon_name"
6
+
5
7
  module RubyLsp
6
8
  module Rails
7
9
  module FactoryBot
10
+ # The listener that is created during relevant hover actions
8
11
  class Hover
9
12
  include RubyLsp::Requests::Support::Common
10
13
 
11
- # TODO: Avoid duplication with other class
12
- FACTORY_BOT_METHODS = %i[
13
- create
14
- build
15
- build_stubbed
16
- attributes_for
17
- ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
18
-
19
14
  def initialize(response_builder, node_context, dispatcher, server_client, ruby_index)
20
15
  @response_builder = response_builder
21
16
  @node_context = node_context
@@ -34,83 +29,86 @@ module RubyLsp
34
29
  # "parent" isn't strictly speaking the immediate parent as in the AST - the
35
30
  # element it refers to is a bit opinionated... in this case, it is always the call node,
36
31
  # whether the symbol is an argument in the call_node args, or a symbol in a kw hash :/
37
- unless parent.is_a?(Prism::CallNode) || FACTORY_BOT_METHODS.include?(call_node.message.to_sym)
32
+ unless parent.is_a?(Prism::CallNode) || FactoryBot::FACTORY_BOT_METHODS.include?(call_node.message.to_sym)
38
33
  return
39
34
  end
40
35
 
41
- case call_node.arguments.arguments
36
+ process_arguments_pattern(symbol_node, call_node.arguments.arguments)
37
+ end
38
+
39
+ private
40
+
41
+ def process_arguments_pattern(symbol_node, arguments) # rubocop:disable Metrics/MethodLength
42
+ case arguments
42
43
  in [^symbol_node, *]
43
44
  handle_factory(symbol_node)
44
- in [Prism::SymbolNode => factory_node, *, ^symbol_node]
45
- handle_trait(symbol_node, factory_node)
46
- in [Prism::SymbolNode => factory_node, ^symbol_node, *]
47
- handle_trait(symbol_node, factory_node)
48
- in [Prism::SymbolNode => factory_node, Integer, ^symbol_node, *]
49
- handle_trait(symbol_node, factory_node)
50
- in [Prism::SymbolNode => factory_node, *, Prism::KeywordHashNode => kw_hash] if kw_hash.elements.any? { |e| e.key == symbol_node }
51
- handle_attribute(symbol_node, factory_node)
52
- in [Prism::SymbolNode => factory_node, *, Prism::HashNode => kw_hash] if kw_hash.elements.any? { |e| e.key == symbol_node }
53
- handle_attribute(symbol_node, factory_node)
45
+ in [Prism::SymbolNode => _factory_node, *, ^symbol_node] |
46
+ [Prism::SymbolNode => _factory_node, ^symbol_node, *] |
47
+ [Prism::SymbolNode => _factory_node, Integer, ^symbol_node, *]
48
+
49
+ handle_trait(symbol_node, _factory_node)
50
+
51
+ in [Prism::SymbolNode => _factory_node, *, Prism::KeywordHashNode => _kw_hash] |
52
+ [Prism::SymbolNode => _factory_node, *, Prism::HashNode => _kw_hash]
53
+
54
+ handle_attribute(symbol_node, _factory_node) if _kw_hash.elements.any? { |e| e.key == symbol_node }
54
55
  else
55
- $stderr.write "nope fact_or_trait", call_node.arguments.arguments
56
56
  nil
57
57
  end
58
58
  end
59
59
 
60
- private
61
-
62
60
  def handle_attribute(symbol_node, factory_node)
63
61
  name = symbol_node.value.to_s
64
- attribute = @server_client.get_attributes(
65
- factory_name: factory_node.value.to_s, name: name
62
+ attribute = make_request(
63
+ :attributes,
64
+ factory_name: factory_node.value.to_s, name: name,
66
65
  )&.find { |attr| attr[:name] == name }
67
66
 
68
67
  return unless attribute
69
68
 
70
69
  @response_builder.push(
71
70
  "#{attribute[:name]} (#{attribute[:type]})",
72
- category: :documentation
71
+ category: :documentation,
73
72
  )
74
73
  end
75
74
 
76
75
  def handle_factory(symbol_node)
77
76
  name = symbol_node.value.to_s
78
-
79
- factory = @server_client.get_factories(name: name)&.find { |f| f[:name] == name }
80
-
77
+ factory = make_request(:factories, name: name)&.find { |f| f[:name] == name }
81
78
  return unless factory
82
79
 
83
80
  index_entry = @ruby_index.first_unqualified_const(factory[:name])
84
81
 
85
- if index_entry
86
- @response_builder.push(
87
- markdown_from_index_entries(
88
- factory[:model_class],
89
- index_entry
90
- ),
91
- category: :documentation
92
- )
93
- else
94
- @response_builder.push(
95
- "#{factory[:name]} (#{factory[:model_class]})",
96
- category: :documentation
97
- )
98
- end
82
+ hint = if index_entry
83
+ markdown_from_index_entries(factory[:model_class], index_entry)
84
+ else
85
+ "#{factory[:name]} (#{factory[:model_class]})"
86
+ end
87
+
88
+ @response_builder.push(hint, category: :documentation)
99
89
  end
100
90
 
101
91
  def handle_trait(symbol_node, factory_node)
102
92
  factory_name = factory_node.value.to_s
103
93
  trait_name = symbol_node.value.to_s
104
94
 
105
- trait = @server_client.get_traits(
106
- factory_name: factory_name, name: trait_name
107
- )&.find { |tr| tr[:name] == trait_name }
95
+ trait = make_request(:traits, factory_name: factory_name, name: trait_name)&.find do |tr|
96
+ tr[:name] == trait_name
97
+ end
108
98
 
109
99
  return unless trait
110
100
 
111
101
  @response_builder.push(
112
102
  "#{trait[:name]} (trait of #{trait[:owner] || factory_name})",
113
- category: :documentation
103
+ category: :documentation,
104
+ )
105
+ end
106
+
107
+ def make_request(request_name, **params)
108
+ @server_client.delegate_request(
109
+ server_addon_name: FactoryBot::ADDON_NAME,
110
+ request_name: request_name.to_s,
111
+ **params,
114
112
  )
115
113
  end
116
114
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising traits
10
+ class AttributeHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ factory = ::FactoryBot.factories[params[:factory_name]]
15
+ name = params[:name]
16
+
17
+ attributes = factory.definition.declarations.select do |attr|
18
+ name.nil? || name.empty? ? true : attr.name.to_s.include?(name)
19
+ end
20
+ model_class = factory.send :class_name
21
+
22
+ return attributes, model_class
23
+ rescue KeyError
24
+ # FactoryBot throws a KeyError if the factory isn't found, so nothing to do here
25
+ end
26
+
27
+ def serialise(attributes, model_class)
28
+ attributes.map do |attribute|
29
+ source_location = block_for(attribute)&.source_location
30
+ {
31
+ name: attribute.name,
32
+ owner: model_class.name,
33
+ type: guess_attribute_type(attribute, model_class),
34
+ source_location: source_location,
35
+ source: block_source(attribute),
36
+ }
37
+ end
38
+ end
39
+
40
+ def guess_attribute_type(attribute, model_class)
41
+ if model_class.respond_to? :attribute_types
42
+ type = model_class.attribute_types[attribute.name.to_s]
43
+ return nil if type.nil?
44
+
45
+ return ACTIVE_MODEL_TYPE_TO_RUBY_TYPE[type.class] || type.type
46
+ end
47
+
48
+ return unless model_class.respond_to? :reflections
49
+
50
+ association = model_class.reflections[attribute.name.to_s]
51
+
52
+ association&.klass
53
+ end
54
+
55
+ ACTIVE_MODEL_TYPE_TO_RUBY_TYPE = {
56
+ ActiveModel::Type::String => "String",
57
+ ActiveModel::Type::ImmutableString => "String",
58
+ ActiveModel::Type::Float => "Float",
59
+ ActiveModel::Type::Integer => "Integer",
60
+ ActiveModel::Type::Boolean => "boolean",
61
+ ActiveModel::Type::Date => "Date",
62
+ ActiveModel::Type::Time => "Time",
63
+ ActiveModel::Type::DateTime => "DateTime",
64
+ }.freeze
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ class ServerAddon < RubyLsp::Rails::ServerAddon
7
+ # Common handler functionality
8
+ class BaseHandler
9
+ def execute(params)
10
+ collection, *rest = fetch(params)
11
+
12
+ serialise(collection, *rest) if collection
13
+ end
14
+
15
+ private
16
+
17
+ def fetch(_params) = throw("Not implemented")
18
+
19
+ def serialise(_collection, *) = throw("Not implemented")
20
+
21
+ # helper - might be best to live elsewhere?
22
+ def block_for(attr)
23
+ attr.instance_variable_get :@block
24
+ end
25
+
26
+ # helper - might be best to live elsewhere?
27
+ def block_source(attr)
28
+ blk = block_for(attr)
29
+ blk.source if blk.respond_to? :source
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising factories
10
+ class FactoryHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ name = params[:name]
15
+ factories = ::FactoryBot.factories.select do |f|
16
+ name.nil? || name.empty? ? true : f.name.to_s.include?(params[:name])
17
+ end
18
+ return factories, nil
19
+ end
20
+
21
+ def serialise(factories, *)
22
+ factories.map do |fact|
23
+ model_class = fact.send :class_name
24
+
25
+ case model_class
26
+ when String, Symbol
27
+ model_class = model_class.to_s.camelize
28
+ end
29
+
30
+ { name: fact.name, model_class: model_class }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising traits
10
+ class TraitHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ factory = ::FactoryBot.factories[params[:factory_name]]
15
+
16
+ trait_name_partial = params[:name]
17
+ defined_traits = defined_traits(factory, trait_name_partial)
18
+ internal_traits = internal_traits(factory.send(:class_name), trait_name_partial)
19
+
20
+ traits = defined_traits.concat(internal_traits)
21
+ return traits, nil
22
+ rescue KeyError
23
+ # FactoryBot throws a KeyError if the factory isn't found, so nothing to do here
24
+ end
25
+
26
+ def defined_traits(factory, trait_name_partial)
27
+ factory.defined_traits.select { |tr| tr.name.to_s.include? trait_name_partial }
28
+ end
29
+
30
+ def internal_traits(factory_class_name, trait_name_partial)
31
+ ::FactoryBot::Internal.traits.select do |tr|
32
+ (tr.klass == factory_class_name || !tr.klass) && tr.name.to_s.include?(trait_name_partial)
33
+ end
34
+ end
35
+
36
+ def serialise(traits, *)
37
+ traits.map do |tr|
38
+ source_location = block_for(tr)&.source_location
39
+ {
40
+ name: tr.name,
41
+ source_location: source_location,
42
+ source: block_source(tr),
43
+ owner: tr.klass,
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "addon_name"
4
+ require_relative "server_addon/factory_handler"
5
+ require_relative "server_addon/trait_handler"
6
+ require_relative "server_addon/attribute_handler"
7
+
8
+ module RubyLsp
9
+ module Rails
10
+ module FactoryBot
11
+ # The addon for the ruby-lsp-rails server runtime
12
+ class ServerAddon < RubyLsp::Rails::ServerAddon
13
+ def initialize(stdout, stderr, capabilities)
14
+ super
15
+
16
+ # TODO: move to before_start hook
17
+ with_progress "ruby-lsp-rails-factory-bot-1", "initialisation" do
18
+ require "factory_bot"
19
+ ::FactoryBot.find_definitions
20
+ ::FactoryBot.factories.each(&:compile)
21
+ end
22
+ end
23
+
24
+ def name = FactoryBot::ADDON_NAME
25
+
26
+ def execute(request, params) # rubocop:disable Metrics/MethodLength
27
+ case request.to_sym
28
+ when :factories
29
+ collection = FactoryHandler.new.execute(params)
30
+ when :traits
31
+ collection = TraitHandler.new.execute(params)
32
+ when :attributes
33
+ collection = AttributeHandler.new.execute(params)
34
+ else
35
+ return send_error_response("#{request} no supported")
36
+ end
37
+
38
+ send_result(collection || [])
39
+ rescue => e # rubocop:disable Style/RescueStandardError
40
+ send_error_response("An error occurred while fetching #{request} - #{e}")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ FACTORY_BOT_METHODS = %i[
7
+ create
8
+ build
9
+ build_stubbed
10
+ attributes_for
11
+ ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,8 @@
3
3
  module RubyLsp
4
4
  module Rails
5
5
  module FactoryBot
6
- VERSION = "0.1.0".freeze
6
+ VERSION = "0.2.0"
7
+ REQUIRED_RUBY_LSP_RAILS_VERSION = "~> 0.4"
7
8
  end
8
9
  end
9
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rails-factory-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - johansenja
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-11 00:00:00.000000000 Z
11
+ date: 2025-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: factory_bot
@@ -30,22 +30,36 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.17.17
33
+ version: '0.23'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.17.17
40
+ version: '0.23'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: ruby-lsp-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
46
60
  - !ruby/object:Gem::Version
47
61
  version: '0'
48
- type: :runtime
62
+ type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
@@ -64,18 +78,24 @@ files:
64
78
  - LICENSE.txt
65
79
  - README.md
66
80
  - Rakefile
81
+ - lib/ruby_lsp/rails/factory_bot.rb
67
82
  - lib/ruby_lsp/rails/factory_bot/addon.rb
83
+ - lib/ruby_lsp/rails/factory_bot/addon_name.rb
68
84
  - lib/ruby_lsp/rails/factory_bot/completion.rb
69
85
  - lib/ruby_lsp/rails/factory_bot/hover.rb
70
- - lib/ruby_lsp/rails/factory_bot/server_extension.rb
86
+ - lib/ruby_lsp/rails/factory_bot/server_addon.rb
87
+ - lib/ruby_lsp/rails/factory_bot/server_addon/attribute_handler.rb
88
+ - lib/ruby_lsp/rails/factory_bot/server_addon/base_handler.rb
89
+ - lib/ruby_lsp/rails/factory_bot/server_addon/factory_handler.rb
90
+ - lib/ruby_lsp/rails/factory_bot/server_addon/trait_handler.rb
71
91
  - lib/ruby_lsp_rails_factory_bot.rb
72
92
  - sig/ruby_lsp/rails/factory_bot.rbs
73
- homepage: https://github.com/johansenja/ruby-lsp-factory-bot
93
+ homepage: https://github.com/johansenja/ruby-lsp-rails-factory-bot
74
94
  licenses:
75
95
  - MIT
76
96
  metadata:
77
97
  allowed_push_host: https://rubygems.org
78
- homepage_uri: https://github.com/johansenja/ruby-lsp-factory-bot
98
+ homepage_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
79
99
  source_code_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
80
100
  changelog_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
81
101
  post_install_message:
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ruby_lsp_rails/server/extension"
4
-
5
- module RubyLsp
6
- module Rails
7
- module FactoryBot
8
- class ServerExtension < RubyLsp::Rails::Server::Extension
9
-
10
- before_start do
11
- require "factory_bot"
12
- ::FactoryBot.find_definitions
13
- ::FactoryBot.factories.each(&:compile)
14
- end
15
-
16
- after_reload do
17
- ::FactoryBot.find_definitions
18
- ::FactoryBot.factories.each(&:compile)
19
- end
20
-
21
- command :factories do |params|
22
- name = params[:name]
23
- factories = ::FactoryBot.factories.select do |f|
24
- name.nil? || name.empty? ? true : f.name.to_s.include?(params[:name])
25
- end
26
-
27
- factories.map do |fact|
28
- model_class = fact.send :class_name
29
- case model_class
30
- when String, Symbol
31
- model_class = model_class.to_s.camelize
32
- end
33
- {
34
- name: fact.name,
35
- model_class: model_class,
36
- }
37
- end
38
- end
39
-
40
- command :traits do |params|
41
- factory = ::FactoryBot.factories[params[:factory_name]]
42
-
43
- trait_name_partial = params[:name]
44
- traits = factory.defined_traits.select { |tr| tr.name.to_s.include? trait_name_partial }.concat(
45
- ::FactoryBot::Internal.traits.select { |tr|
46
- (tr.klass == factory.send(:class_name) || !tr.klass) && tr.name.to_s.include?(trait_name_partial)
47
- }
48
- )
49
-
50
- traits.map do |tr|
51
- source_location = ServerExtension.block_for(tr)&.source_location
52
- {
53
- name: tr.name,
54
- source_location: source_location,
55
- source: ServerExtension.block_source(tr),
56
- owner: tr.klass,
57
- }
58
- end
59
- rescue KeyError
60
- end
61
-
62
- command :attributes do |params|
63
- factory = ::FactoryBot.factories[params[:factory_name]]
64
- name = params[:name]
65
-
66
- attributes = factory.definition.declarations.select { |attr|
67
- name.nil? || name.empty? ? true : attr.name.to_s.include?(name)
68
- }
69
-
70
- model_class = factory.send :class_name
71
-
72
- attributes.map do |attribute|
73
- source_location = ServerExtension.block_for(attribute)&.source_location
74
- {
75
- name: attribute.name,
76
- owner: model_class.name,
77
- type: ServerExtension.guess_attribute_type(attribute, model_class),
78
- source_location: source_location,
79
- source: ServerExtension.block_source(attribute),
80
- }
81
- end
82
- rescue KeyError
83
- end
84
-
85
- class << self
86
- def block_for(attr)
87
- attr.instance_variable_get :@block
88
- end
89
-
90
- def block_source(attr)
91
- blk = block_for(attr)
92
-
93
- blk.source if blk.respond_to? :source
94
- end
95
-
96
- def guess_attribute_type(attribute, model_class)
97
- if model_class.respond_to? :attribute_types
98
- type = model_class.attribute_types[attribute.name.to_s]
99
-
100
- return case type
101
- when nil then nil
102
- when ActiveModel::Type::String, ActiveModel::Type::ImmutableString
103
- "String"
104
- when ActiveModel::Type::Float
105
- "Float"
106
- when ActiveModel::Type::Integer
107
- "Integer"
108
- when ActiveModel::Type::Boolean
109
- "boolean"
110
- when ActiveModel::Type::Date
111
- "Date"
112
- when ActiveModel::Type::Time
113
- "Time"
114
- when ActiveModel::Type::DateTime
115
- "DateTime"
116
- else
117
- type.type
118
- end
119
- end
120
-
121
- return unless model_class.respond_to? :reflections
122
-
123
- association = model_class.reflections[attribute.name.to_s]
124
-
125
- association&.klass
126
- end
127
- end
128
- end
129
- end
130
- end
131
- end