ai_rspec_writer 1.0.5 → 1.0.6
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be389107c5f70774c3581dbf04170412e8a042adfca34873d9e7d3f73f6b1e33
|
4
|
+
data.tar.gz: 7164b14789a8de01a0e27487f0e66e6c14de572638b956f444bcb262e60a4464
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b38dd7f0a8d2a04e3176a6add0bce16ca0b64daae60305f930f8851129e89ed316c1fbe03164cc980f38be2b38ca32d2ebe2ce583d2df5fb21fc56bfd03bce8e
|
7
|
+
data.tar.gz: d197257e867ae0cdeb5b06f2ec262415140575c1c9d77a16b54dd1e93a25d2182644bb71314afb89d595068564d3e8c49132e51d5ecfbb1af561ae723da2535a
|
data/bin/ai_rspec_writer
CHANGED
@@ -6,7 +6,7 @@ require "optparse"
|
|
6
6
|
require_relative "../lib/ai_rspec_writer"
|
7
7
|
require_relative "../lib/ai_rspec_writer/schema_extractor"
|
8
8
|
|
9
|
-
if AiRspecWriter.configuration.chatgpt_api_key.nil?
|
9
|
+
if AiRspecWriter.configuration.chatgpt_api_key.nil? && AiRspecWriter.configuration.gemini_api_key.nil?
|
10
10
|
puts "❌ API key is missing.".red
|
11
11
|
puts "ℹ️ Set it in `.env`, or use `export CHATGPT_API_KEY=your-key` `export GEMINI_API_KEY=your-key`.".yellow
|
12
12
|
exit 1
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
4
6
|
require 'colorize'
|
5
7
|
require 'tty-spinner'
|
6
8
|
|
@@ -8,20 +10,19 @@ module AIRspecWriter
|
|
8
10
|
module AIProviders
|
9
11
|
# Generates RSpec tests using Google's Gemini AI API
|
10
12
|
class GeminiGenerator
|
13
|
+
BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/models'
|
14
|
+
|
11
15
|
def initialize(ec: nil, schema: nil)
|
12
16
|
@extra_comment = ec
|
13
17
|
@schema = schema
|
14
|
-
@
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
},
|
19
|
-
options: { model: AiRspecWriter.configuration.gemini_model, server_sent_events: true }
|
20
|
-
)
|
18
|
+
@api_key = AiRspecWriter.configuration.gemini_api_key
|
19
|
+
@model = AiRspecWriter.configuration.gemini_model
|
20
|
+
|
21
|
+
validate_configuration
|
21
22
|
end
|
22
23
|
|
23
24
|
def generate_spec(file_content)
|
24
|
-
|
25
|
+
fixed_prompt = "Write RSpec tests for this Ruby code with all factory files and rubocop standards:"
|
25
26
|
my_schema = "Check my table definition details: #{@schema}" if @schema
|
26
27
|
|
27
28
|
# Define loading spinner
|
@@ -31,19 +32,13 @@ module AIRspecWriter
|
|
31
32
|
spinner.auto_spin
|
32
33
|
|
33
34
|
begin
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
text: "#{fixed_promt}\n\n#{file_content}\n\n#{@extra_comment}\n\n#{my_schema}"
|
39
|
-
}
|
40
|
-
}
|
41
|
-
})
|
42
|
-
|
43
|
-
if result["candidates"].first["content"]["parts"].first["text"].present?
|
35
|
+
content_text = build_content_text(fixed_prompt, file_content, my_schema)
|
36
|
+
response_text = call_gemini_api(content_text)
|
37
|
+
|
38
|
+
if response_text&.strip&.length&.positive?
|
44
39
|
# Stop the spinner when AI response is ready
|
45
40
|
spinner.success("✅ Done!")
|
46
|
-
return
|
41
|
+
return response_text
|
47
42
|
else
|
48
43
|
spinner.error('(Gemini AI returned an empty response.)')
|
49
44
|
return nil
|
@@ -53,6 +48,94 @@ module AIRspecWriter
|
|
53
48
|
exit 1
|
54
49
|
end
|
55
50
|
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def validate_configuration
|
55
|
+
raise ConfigurationError, "Gemini API key is required" if @api_key.nil? || @api_key.empty?
|
56
|
+
raise ConfigurationError, "Gemini model is required" if @model.nil? || @model.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_content_text(fixed_prompt, file_content, my_schema)
|
60
|
+
content_parts = [fixed_prompt, file_content, @extra_comment, my_schema].compact
|
61
|
+
content_parts.join("\n\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def call_gemini_api(content_text)
|
65
|
+
url = build_api_url
|
66
|
+
payload = build_payload(content_text)
|
67
|
+
|
68
|
+
response = make_http_request(url, payload)
|
69
|
+
parse_response(response)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_api_url
|
73
|
+
"#{BASE_URL}/#{@model}:generateContent?key=#{@api_key}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_payload(content_text)
|
77
|
+
{
|
78
|
+
contents: [
|
79
|
+
{
|
80
|
+
role: "user",
|
81
|
+
parts: [
|
82
|
+
{ text: content_text }
|
83
|
+
]
|
84
|
+
}
|
85
|
+
],
|
86
|
+
generationConfig: {
|
87
|
+
temperature: 0.7,
|
88
|
+
topK: 40,
|
89
|
+
topP: 0.95,
|
90
|
+
maxOutputTokens: 8192
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def make_http_request(url, payload)
|
96
|
+
uri = URI(url)
|
97
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
98
|
+
http.use_ssl = true
|
99
|
+
http.read_timeout = 60 # Set timeout for long responses
|
100
|
+
|
101
|
+
request = Net::HTTP::Post.new(uri)
|
102
|
+
request['Content-Type'] = 'application/json'
|
103
|
+
request.body = payload.to_json
|
104
|
+
|
105
|
+
response = http.request(request)
|
106
|
+
|
107
|
+
unless response.is_a?(Net::HTTPSuccess)
|
108
|
+
raise ApiError, "HTTP #{response.code}: #{response.body}"
|
109
|
+
end
|
110
|
+
|
111
|
+
response
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_response(response)
|
115
|
+
data = JSON.parse(response.body)
|
116
|
+
|
117
|
+
if data['error']
|
118
|
+
raise ApiError, "API error: #{data['error']['message']}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Extract the generated text following the same pattern as your original code
|
122
|
+
candidates = data['candidates']
|
123
|
+
return nil if candidates.nil? || candidates.empty?
|
124
|
+
|
125
|
+
first_candidate = candidates.first
|
126
|
+
content = first_candidate['content']
|
127
|
+
return nil if content.nil?
|
128
|
+
|
129
|
+
parts = content['parts']
|
130
|
+
return nil if parts.nil? || parts.empty?
|
131
|
+
|
132
|
+
parts.first['text']
|
133
|
+
rescue JSON::ParserError => e
|
134
|
+
raise ApiError, "Failed to parse response: #{e.message}"
|
135
|
+
end
|
136
|
+
|
137
|
+
class ApiError < StandardError; end
|
138
|
+
class ConfigurationError < StandardError; end
|
56
139
|
end
|
57
140
|
end
|
58
|
-
end
|
141
|
+
end
|
data/lib/ai_rspec_writer.rb
CHANGED
@@ -4,6 +4,11 @@ require_relative "ai_rspec_writer/generator"
|
|
4
4
|
require_relative "ai_rspec_writer/spec_file_handler"
|
5
5
|
require "colorize"
|
6
6
|
|
7
|
+
# Load Rails configuration only if inside a Rails application
|
8
|
+
if defined?(Rails::Generators)
|
9
|
+
require_relative "generators/ai_rspec_writer/install_generator"
|
10
|
+
end
|
11
|
+
|
7
12
|
module AiRspecWriter
|
8
13
|
class Error < StandardError; end
|
9
14
|
class << self
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "tty-spinner"
|
3
|
+
|
4
|
+
module AiRspecWriter
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
def install_ai_rspec_writer
|
9
|
+
puts "\n🚀 Starting `ai_rspec_writer` setup...\n".yellow
|
10
|
+
check_rspec_installed
|
11
|
+
configure_spec_helper
|
12
|
+
configure_rails_helper
|
13
|
+
puts "\n✅ `ai_rspec_writer` setup completed successfully!\n".green
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Check if RSpec is installed, otherwise install it
|
19
|
+
def check_rspec_installed
|
20
|
+
spinner = TTY::Spinner.new("🔍 Checking if RSpec is installed... :spinner")
|
21
|
+
spinner.auto_spin
|
22
|
+
|
23
|
+
if File.exist?("spec/rails_helper.rb") && File.exist?("spec/spec_helper.rb")
|
24
|
+
spinner.success("✅ RSpec is already installed.")
|
25
|
+
else
|
26
|
+
spinner.stop("⚠️ RSpec not found. Installing...".red)
|
27
|
+
generate "rspec:install"
|
28
|
+
puts "✅ RSpec installed successfully.".green
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Append content to spec_helper.rb
|
33
|
+
def configure_spec_helper
|
34
|
+
spec_helper_path = "spec/spec_helper.rb"
|
35
|
+
spinner = TTY::Spinner.new("🔧 Configuring `spec_helper.rb`... :spinner")
|
36
|
+
spinner.auto_spin
|
37
|
+
|
38
|
+
if File.exist?(spec_helper_path)
|
39
|
+
original_content = File.read(spec_helper_path)
|
40
|
+
new_content = "#{spec_file_content}\n\n" + original_content
|
41
|
+
File.write(spec_helper_path, new_content)
|
42
|
+
spinner.success("✅ Updated `spec_helper.rb` with AI RSpec settings.")
|
43
|
+
else
|
44
|
+
spinner.error("⚠️ `spec_helper.rb` not found. Skipping...".red)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Append content to rails_helper.rb after `RSpec.configure do |config|`
|
49
|
+
def configure_rails_helper
|
50
|
+
rails_helper_path = "spec/rails_helper.rb"
|
51
|
+
target_line = "RSpec.configure do |config|"
|
52
|
+
spinner = TTY::Spinner.new("🔧 Configuring `rails_helper.rb`... :spinner")
|
53
|
+
spinner.auto_spin
|
54
|
+
|
55
|
+
if File.exist?(rails_helper_path)
|
56
|
+
content = File.readlines(rails_helper_path)
|
57
|
+
index = content.find_index { |line| line.strip == target_line.strip }
|
58
|
+
|
59
|
+
if index
|
60
|
+
content.insert(index + 1, "#{rails_helper_content}\n")
|
61
|
+
File.write(rails_helper_path, content.join)
|
62
|
+
spinner.success("✅ Successfully inserted configurations into `rails_helper.rb`.")
|
63
|
+
else
|
64
|
+
spinner.error("⚠️ `RSpec.configure` block not found in `rails_helper.rb`. Skipping...".red)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
spinner.error("⚠️ `rails_helper.rb` not found. Skipping...".red)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Content for spec_helper.rb
|
72
|
+
def spec_file_content
|
73
|
+
<<~RUBY
|
74
|
+
# Auto-generated by ai_rspec_writer
|
75
|
+
require 'simplecov'
|
76
|
+
SimpleCov.start 'rails' do
|
77
|
+
add_filter '/spec/'
|
78
|
+
add_filter '/config/'
|
79
|
+
add_filter '/vendor/'
|
80
|
+
add_filter '/db/'
|
81
|
+
add_filter '/bin/'
|
82
|
+
end
|
83
|
+
# Auto-generated by ai_rspec_writer
|
84
|
+
RUBY
|
85
|
+
end
|
86
|
+
|
87
|
+
# Content for rails_helper.rb with proper indentation
|
88
|
+
def rails_helper_content
|
89
|
+
<<~RUBY.indent(2)
|
90
|
+
# Auto-generated by ai_rspec_writer
|
91
|
+
config.include FactoryBot::Syntax::Methods
|
92
|
+
|
93
|
+
# Include Shoulda Matchers
|
94
|
+
Shoulda::Matchers.configure do |shoulda_config|
|
95
|
+
shoulda_config.integrate do |with|
|
96
|
+
with.test_framework :rspec
|
97
|
+
with.library :rails
|
98
|
+
end
|
99
|
+
end
|
100
|
+
# Auto-generated by ai_rspec_writer
|
101
|
+
RUBY
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ai_rspec_writer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aniruddha Mirajkar
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-07-07 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rspec-rails
|
@@ -157,6 +156,7 @@ files:
|
|
157
156
|
- lib/ai_rspec_writer/schema_extractor.rb
|
158
157
|
- lib/ai_rspec_writer/spec_file_handler.rb
|
159
158
|
- lib/ai_rspec_writer/version.rb
|
159
|
+
- lib/generators/ai_rspec_writer/install_generator.rb
|
160
160
|
homepage: https://github.com/aniruddhami/ai_rspec_writer
|
161
161
|
licenses:
|
162
162
|
- MIT
|
@@ -165,7 +165,6 @@ metadata:
|
|
165
165
|
source_code_uri: https://github.com/aniruddhami/ai_rspec_writer
|
166
166
|
changelog_uri: https://github.com/aniruddhami/ai_rspec_writer/blob/main/CHANGELOG.md
|
167
167
|
github_repo: https://github.com/aniruddhami/ai_rspec_writer
|
168
|
-
post_install_message:
|
169
168
|
rdoc_options: []
|
170
169
|
require_paths:
|
171
170
|
- lib
|
@@ -180,8 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
180
179
|
- !ruby/object:Gem::Version
|
181
180
|
version: '0'
|
182
181
|
requirements: []
|
183
|
-
rubygems_version: 3.
|
184
|
-
signing_key:
|
182
|
+
rubygems_version: 3.6.6
|
185
183
|
specification_version: 4
|
186
184
|
summary: A gem to generate RSpec tests using AI (ChatGPT, GeminiAI).
|
187
185
|
test_files: []
|